devrites 3.2.8 → 3.2.10

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.10](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.9...v3.2.10) (2026-07-26)
6
+
7
+ ### Fixed
8
+
9
+ * **devrites:** allow external dispatch scratch ([fdaac3f](https://github.com/ViktorsBaikers/DevRites/commit/fdaac3f9aaf19af897c6d3d70ce01fc046a60d3e))
10
+
11
+ ## [3.2.9](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.8...v3.2.9) (2026-07-26)
12
+
13
+ ### Fixed
14
+
15
+ * **devrites:** unblock retained reconcile checks ([c1093b0](https://github.com/ViktorsBaikers/DevRites/commit/c1093b00c3e32ba3cc22e0b9a2da548618ead621))
16
+
5
17
  ## [3.2.8](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.7...v3.2.8) (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.8`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.8): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
28
+ **Status:** [`v3.2.10`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.10): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -69,6 +69,16 @@ 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. The standalone A1 hook and wright-scope hook share this classification;
79
+ Codex `cmd` and Claude `command` payloads may create a literal external packet
80
+ with a patch, heredoc, or copy and may hash it without gaining source authority.
81
+
72
82
  The canonical-state fingerprint excludes root-owned operational records:
73
83
  `timeline.jsonl`, feature `events.jsonl`, a valid `recovery-attempts.jsonl`,
74
84
  the redwatch sentinel, automatic handoff snapshots, and hidden hook observation
package/engine/hooks.go CHANGED
@@ -377,7 +377,7 @@ func safeReadonlyShellSegment(segment string) bool {
377
377
  "grep", "rg", "wc", "sort", "uniq", "cut", "tr", "stat", "file",
378
378
  "readlink", "realpath", "basename", "dirname", "cmp", "diff", "jq", "yq",
379
379
  "tree", "du", "df", "printenv", "which", "date", "uname", "id", "whoami",
380
- "ps", "echo", "printf", "test", "[", "cd":
380
+ "ps", "echo", "printf", "test", "[", "cd", "sha256sum", "shasum":
381
381
  return true
382
382
  case "find":
383
383
  return !hasAnyArg(args, "-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprintf")
@@ -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 ||
@@ -325,6 +325,8 @@ func redwatchSafe(cmd string) string {
325
325
  // node regex the shell guards use.
326
326
  var patchPathRe = regexp.MustCompile(`(?m)^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$`)
327
327
 
328
+ var shellHeredocStartRe = regexp.MustCompile(`<<(-?)[ \t]*(?:'([A-Za-z_][A-Za-z0-9_]*)'|"([A-Za-z_][A-Za-z0-9_]*)"|([A-Za-z_][A-Za-z0-9_]*))`)
329
+
328
330
  // isEditTool reports whether a tool can write source. toolBaseName also handles
329
331
  // host-qualified names such as functions.apply_patch and MCP symbol editors.
330
332
  func isEditTool(tool string) bool {
@@ -346,6 +348,39 @@ func patchPaths(command string) []string {
346
348
  return paths
347
349
  }
348
350
 
351
+ func stripShellHeredocBodies(command string) (string, bool) {
352
+ lines := strings.Split(strings.ReplaceAll(command, "\r\n", "\n"), "\n")
353
+ out := make([]string, 0, len(lines))
354
+ for i := 0; i < len(lines); i++ {
355
+ line := lines[i]
356
+ out = append(out, line)
357
+ for _, match := range shellHeredocStartRe.FindAllStringSubmatch(line, -1) {
358
+ delimiter := match[2]
359
+ if delimiter == "" {
360
+ delimiter = match[3]
361
+ }
362
+ if delimiter == "" {
363
+ return "", false
364
+ }
365
+ found := false
366
+ for i++; i < len(lines); i++ {
367
+ candidate := lines[i]
368
+ if match[1] == "-" {
369
+ candidate = strings.TrimLeft(candidate, "\t")
370
+ }
371
+ if candidate == delimiter {
372
+ found = true
373
+ break
374
+ }
375
+ }
376
+ if !found {
377
+ return "", false
378
+ }
379
+ }
380
+ }
381
+ return strings.Join(out, "\n"), true
382
+ }
383
+
349
384
  // shellOutputRedirections removes harmless fd redirects and returns every file
350
385
  // target of >, >>, or >|. Dynamic targets are opaque and therefore unsafe.
351
386
  func shellOutputRedirections(command string) (sanitized string, paths []string, opaque bool) {
@@ -471,44 +506,13 @@ func hookA1Guard(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) i
471
506
  }
472
507
 
473
508
  in := h.ParseGuardInput(bytes.NewReader(data))
474
- if !isEditTool(in.ToolName) {
475
- return exitOK
476
- }
477
509
  // The wright may edit. Claude sends agent_id; Codex sends agent_type.
478
510
  if in.AgentID != "" || in.AgentType != "" {
479
511
  return exitOK
480
512
  }
481
-
482
- projectDir := filepath.Dir(root)
483
- target := ""
484
- if in.ToolName == "apply_patch" {
485
- sourcePatch := false
486
- for _, p := range patchPaths(in.Command) {
487
- abs := p
488
- if !filepath.IsAbs(abs) {
489
- abs = filepath.Join(projectDir, abs)
490
- }
491
- if !underDevrites(abs, root) {
492
- sourcePatch = true
493
- break
494
- }
495
- }
496
- if !sourcePatch {
497
- return exitOK
498
- }
499
- target = "apply_patch"
500
- } else {
501
- if in.FilePath == "" {
502
- return exitOK
503
- }
504
- abs := in.FilePath
505
- if !filepath.IsAbs(abs) {
506
- abs = filepath.Join(projectDir, abs)
507
- }
508
- if underDevrites(abs, root) {
509
- return exitOK // orchestrator editing its own .devrites bookkeeping
510
- }
511
- target = abs
513
+ handled, violation, target := classifyRootBuildWindowOperation(root, data, in)
514
+ if !handled || !violation {
515
+ return exitOK
512
516
  }
513
517
 
514
518
  mode := os.Getenv("DEVRITES_A1_HOOK")
@@ -842,7 +846,11 @@ func uniqueStrings(values []string) []string {
842
846
  }
843
847
 
844
848
  func wrightShellWritePaths(command string) ([]string, bool) {
845
- if strings.TrimSpace(command) == "" || wrightForbiddenShellRe.MatchString(command) {
849
+ patches := patchPaths(command)
850
+ var ok bool
851
+ command, ok = stripShellHeredocBodies(command)
852
+ if !ok || strings.TrimSpace(command) == "" || strings.Contains(command, "$(") ||
853
+ strings.ContainsRune(command, '`') || wrightForbiddenShellRe.MatchString(command) {
846
854
  return nil, true
847
855
  }
848
856
  sanitized, paths, opaque := shellOutputRedirections(command)
@@ -850,7 +858,6 @@ func wrightShellWritePaths(command string) ([]string, bool) {
850
858
  return nil, true
851
859
  }
852
860
 
853
- patches := patchPaths(command)
854
861
  if strings.Contains(command, "apply_patch") {
855
862
  if len(patches) == 0 {
856
863
  return nil, true
@@ -896,6 +903,15 @@ func wrightShellWritePaths(command string) ([]string, bool) {
896
903
  found = nonFlagArgs(args)
897
904
  case "rm", "unlink", "touch", "mkdir", "rmdir", "truncate":
898
905
  found = nonFlagArgs(args)
906
+ case "cp":
907
+ if hasAnyArg(args, "-t", "--target-directory") || hasArgPrefix(args, "--target-directory=") {
908
+ return nil, true
909
+ }
910
+ operands := nonFlagArgs(args)
911
+ if len(operands) < 2 {
912
+ return nil, true
913
+ }
914
+ found = operands[len(operands)-1:]
899
915
  default:
900
916
  return nil, true
901
917
  }
@@ -1014,36 +1030,50 @@ func guardRootBuildWindow(h harness.Harness, data []byte, in harness.GuardInput,
1014
1030
  return exitOK, false
1015
1031
  }
1016
1032
 
1017
- if isAgentDispatchTool(in.ToolName) {
1018
- return exitOK, true
1033
+ handled, violation, _ := classifyRootBuildWindowOperation(root, data, in)
1034
+ if violation {
1035
+ return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
1019
1036
  }
1037
+ return exitOK, handled
1038
+ }
1020
1039
 
1021
- projectDir := filepath.Dir(root)
1040
+ func classifyRootBuildWindowOperation(root string, data []byte, in harness.GuardInput) (handled, violation bool, target string) {
1041
+ if isAgentDispatchTool(in.ToolName) {
1042
+ return true, false, ""
1043
+ }
1022
1044
  if isOpaqueExecutionTool(in.ToolName) {
1023
- return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
1045
+ return true, true, in.ToolName
1024
1046
  }
1047
+
1048
+ projectDir := filepath.Dir(root)
1025
1049
  targets, suspicious := rootMutationTargets(data, in)
1026
1050
  for _, target := range targets {
1051
+ absoluteTarget := filepath.IsAbs(target)
1027
1052
  abs := target
1028
- if !filepath.IsAbs(abs) {
1053
+ if !absoluteTarget {
1029
1054
  abs = filepath.Join(projectDir, abs)
1030
1055
  }
1031
1056
  abs = filepath.Clean(abs)
1032
1057
  resolved, err := resolveRootMutationTarget(abs)
1033
1058
  if err != nil {
1034
- return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
1059
+ return true, true, target
1035
1060
  }
1036
- if !underDevrites(resolved, root) {
1037
- return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
1061
+ insideProject := pathWithin(resolved, projectDir)
1062
+ if (insideProject && !underDevrites(resolved, root)) ||
1063
+ (!insideProject && !absoluteTarget) {
1064
+ return true, true, target
1038
1065
  }
1039
1066
  }
1040
1067
  if suspicious {
1041
- return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
1068
+ return true, true, in.ToolName
1042
1069
  }
1043
1070
  if len(targets) > 0 {
1044
- return exitOK, true
1071
+ return true, false, strings.Join(targets, ",")
1072
+ }
1073
+ if isShellTool(in.ToolName) && safeReadonlyShellCommand(in.Command) {
1074
+ return true, false, ""
1045
1075
  }
1046
- return exitOK, false
1076
+ return false, false, ""
1047
1077
  }
1048
1078
 
1049
1079
  func resolveRootMutationTarget(filename string) (string, error) {
@@ -1071,7 +1101,8 @@ func rootMutationTargets(data []byte, in harness.GuardInput) ([]string, bool) {
1071
1101
  if len(targets) > 0 {
1072
1102
  return targets, false
1073
1103
  }
1074
- return nil, opaque && (reviewerMutateRe.MatchString(in.Command) || opaqueInterpreterRe.MatchString(in.Command))
1104
+ _, completeHeredoc := stripShellHeredocBodies(in.Command)
1105
+ return nil, opaque && (!completeHeredoc || reviewerMutateRe.MatchString(in.Command) || opaqueInterpreterRe.MatchString(in.Command))
1075
1106
  default:
1076
1107
  return nil, false
1077
1108
  }
@@ -214,6 +214,7 @@ func (h Harness) ParseGuardInput(r io.Reader) GuardInput {
214
214
  ToolName string `json:"tool_name"`
215
215
  ToolInput struct {
216
216
  Command string `json:"command"`
217
+ Cmd string `json:"cmd"`
217
218
  FilePath string `json:"file_path"`
218
219
  Path string `json:"path"`
219
220
  } `json:"tool_input"`
@@ -237,7 +238,7 @@ func (h Harness) ParseGuardInput(r io.Reader) GuardInput {
237
238
  }
238
239
  return GuardInput{
239
240
  ToolName: raw.ToolName,
240
- Command: raw.ToolInput.Command,
241
+ Command: firstNonEmpty(raw.ToolInput.Command, raw.ToolInput.Cmd),
241
242
  FilePath: firstNonEmpty(raw.ToolInput.FilePath, raw.ToolInput.Path),
242
243
  SessionID: raw.SessionID,
243
244
  AgentType: raw.AgentType,
@@ -113,6 +113,10 @@ func TestHarnessParsersFailOpenAndFallbacks(t *testing.T) {
113
113
  if guard.ToolName != "Edit" || guard.FilePath != "src/app.go" || guard.AgentID != "a1" || guard.ToolResponse != `{"ok":true}` {
114
114
  t.Fatalf("ParseGuardInput = %#v", guard)
115
115
  }
116
+ guard = Codex.ParseGuardInput(strings.NewReader(`{"tool_name":"functions.exec_command","tool_input":{"cmd":"sha256sum /tmp/agent-packet.yaml"}}`))
117
+ if guard.ToolName != "functions.exec_command" || guard.Command != "sha256sum /tmp/agent-packet.yaml" {
118
+ t.Fatalf("ParseGuardInput Codex cmd = %#v", guard)
119
+ }
116
120
 
117
121
  if got := Codex.SubagentAgentType(strings.NewReader(`{"agentType":"executor"}`)); got != "executor" {
118
122
  t.Fatalf("SubagentAgentType fallback = %q, want executor", got)
@@ -583,6 +583,13 @@ 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
+ scratch := t.TempDir()
587
+ packet := filepath.Join(scratch, "agent-packet.yaml")
588
+ runGuard(hookPayload(t, map[string]any{
589
+ "tool_name": "Write",
590
+ "tool_input": map[string]any{"file_path": packet},
591
+ }), false)
592
+ runGuard(`{"tool_name":"Write","tool_input":{"file_path":"../outside.txt"}}`, true)
586
593
  runGuard(`{"tool_name":"Write","tool_input":{"file_path":"src/app.go"}}`, true)
587
594
  runGuard(`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, true)
588
595
  runGuard(`{"tool_name":"js","tool_input":{"code":"writeFile()"}}`, true)
@@ -644,6 +651,59 @@ func TestHookA1GuardIgnoresLegacyInlineMarker(t *testing.T) {
644
651
  }
645
652
  }
646
653
 
654
+ func TestHookA1GuardAllowsExternalDispatchScratchOnly(t *testing.T) {
655
+ root := newWorkspace(t)
656
+ writeActive(t, root, "auth-tokens")
657
+ workspace := filepath.Join(root, "features", "auth-tokens")
658
+ if err := os.WriteFile(filepath.Join(workspace, ".reconcile-base"), []byte("snapshot\n"), 0o600); err != nil {
659
+ t.Fatal(err)
660
+ }
661
+ scratch := t.TempDir()
662
+ packet := filepath.Join(scratch, "agent-packet.yaml")
663
+ env := []string{"DEVRITES_A1_HOOK=enforce"}
664
+ patch := func(tool, path string, freeform bool) string {
665
+ body := "*** Begin Patch\n*** Add File: " + path + "\n+x\n*** End Patch\n"
666
+ if freeform {
667
+ return hookPayload(t, map[string]any{"tool_name": tool, "tool_input": body})
668
+ }
669
+ return hookPayload(t, map[string]any{"tool_name": tool, "tool_input": map[string]any{"patch": body}})
670
+ }
671
+ shell := func(command string) string {
672
+ return hookPayload(t, map[string]any{
673
+ "tool_name": "functions.exec_command",
674
+ "tool_input": map[string]any{"cmd": command},
675
+ })
676
+ }
677
+
678
+ runGuard := func(input string, deny bool) {
679
+ t.Helper()
680
+ out, errOut, code := runDevritesIO(t, root, input, env,
681
+ "hook", "a1-guard", "--harness=codex")
682
+ if code != 0 {
683
+ t.Fatalf("exit=%d out=%q stderr=%q", code, out, errOut)
684
+ }
685
+ if deny {
686
+ if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
687
+ t.Fatalf("expected deny, got %q", out)
688
+ }
689
+ } else if strings.TrimSpace(out) != "" {
690
+ t.Fatalf("expected allow, got %q", out)
691
+ }
692
+ }
693
+
694
+ runGuard(patch("apply_patch", packet, false), false)
695
+ runGuard(patch("functions.apply_patch", packet, true), false)
696
+ runGuard(shell("cat <<'EOF' > "+packet+"\npacket_version: agent-packet/v1\nEOF"), false)
697
+ runGuard(shell("sha256sum "+packet), false)
698
+ runGuard(shell("cp "+packet+" "+filepath.Join(scratch, "agent-packet.copy.yaml")), false)
699
+ runGuard(patch("apply_patch", "src/app.go", false), true)
700
+ runGuard(patch("functions.apply_patch", "src/app.go", true), true)
701
+ runGuard(shell("cat <<'EOF' > src/app.go\npackage src\nEOF"), true)
702
+ runGuard(shell("cat <<'EOF' > "+packet+"\nunterminated"), true)
703
+ runGuard(shell("cat <<EOF > "+packet+"\n$(cp "+packet+" src/app.go)\nEOF"), true)
704
+ runGuard(shell("cp "+packet+" src/app.go"), true)
705
+ }
706
+
647
707
  func TestHookWrightScopeFailsClosedWhenWorkspaceResolutionFails(t *testing.T) {
648
708
  root := newWorkspace(t)
649
709
  if err := os.Remove(filepath.Join(root, "ACTIVE")); err != nil && !os.IsNotExist(err) {
@@ -1695,7 +1755,7 @@ func TestCodexAgentDispatchRejectsIncompleteDurableV2(t *testing.T) {
1695
1755
  }
1696
1756
  }
1697
1757
 
1698
- func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
1758
+ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T) {
1699
1759
  root := newWorkspace(t)
1700
1760
  writeActive(t, root, "auth-tokens")
1701
1761
  workspace := filepath.Join(root, "features", "auth-tokens")
@@ -1714,14 +1774,26 @@ func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
1714
1774
  "prompt": "$rite-build",
1715
1775
  }), nil, "hook", "agent-dispatch", "--harness=codex")
1716
1776
 
1717
- reconcile := hookPayload(t, map[string]any{
1777
+ check := hookPayload(t, map[string]any{
1778
+ "hook_event_name": "PreToolUse",
1779
+ "session_id": sessionID,
1780
+ "turn_id": turnID,
1781
+ "tool_name": "Bash",
1782
+ "tool_input": map[string]any{"command": "rtk devrites-engine reconcile check"},
1783
+ })
1784
+ if out, stderr, code := runDevritesIO(t, root, check, nil,
1785
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1786
+ t.Fatalf("ordinary reconcile check exit=%d out=%s stderr=%s", code, out, stderr)
1787
+ }
1788
+
1789
+ closeWindow := hookPayload(t, map[string]any{
1718
1790
  "hook_event_name": "PreToolUse",
1719
1791
  "session_id": sessionID,
1720
1792
  "turn_id": turnID,
1721
1793
  "tool_name": "Bash",
1722
1794
  "tool_input": map[string]any{"command": "rtk devrites-engine reconcile close"},
1723
1795
  })
1724
- out, _, _ := runDevritesIO(t, root, reconcile, nil,
1796
+ out, _, _ := runDevritesIO(t, root, closeWindow, nil,
1725
1797
  "hook", "agent-dispatch", "--harness=codex")
1726
1798
  if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
1727
1799
  !strings.Contains(reason, "confirmed") {
@@ -1774,7 +1846,7 @@ func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
1774
1846
  }
1775
1847
  }
1776
1848
 
1777
- if out, stderr, code := runDevritesIO(t, root, reconcile, nil,
1849
+ if out, stderr, code := runDevritesIO(t, root, closeWindow, nil,
1778
1850
  "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1779
1851
  t.Fatalf("completed wright reconcile exit=%d out=%s stderr=%s", code, out, stderr)
1780
1852
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "3.2.8",
3
+ "version": "3.2.10",
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",