devrites 3.2.12 → 3.2.14

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.14](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.13...v3.2.14) (2026-07-26)
6
+
7
+ ### Fixed
8
+
9
+ * **agents:** track unarmed retained retries ([44cf3cd](https://github.com/ViktorsBaikers/DevRites/commit/44cf3cd2d111060c76c53095ae76615ddba29c53))
10
+
11
+ ## [3.2.13](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.12...v3.2.13) (2026-07-26)
12
+
13
+ ### Fixed
14
+
15
+ * **agents:** retain canonical boundary before spawn ([b290aa9](https://github.com/ViktorsBaikers/DevRites/commit/b290aa97851915afcddd85ff43855fc8e07b6860))
16
+
5
17
  ## [3.2.12](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.11...v3.2.12) (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.12`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.12): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
28
+ **Status:** [`v3.2.14`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.14): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -897,6 +897,41 @@ func stopDispatchFailure(reason string, stdout, stderr io.Writer) int {
897
897
  return exitOK
898
898
  }
899
899
 
900
+ func refreshPendingWrightBoundary(
901
+ root string,
902
+ in agentDispatchHookInput,
903
+ armed map[string]struct{},
904
+ attempts []*agentDispatchAttempt,
905
+ ) error {
906
+ if in.AgentID != "" || in.AgentType != "" || currentReconcileWindowID() == "" {
907
+ return nil
908
+ }
909
+ for _, attempt := range attempts {
910
+ if attempt.Role == "devrites-slice-wright" {
911
+ return nil
912
+ }
913
+ }
914
+ boundaryArmed := make(map[string]struct{}, len(armed)+1)
915
+ for role := range armed {
916
+ boundaryArmed[role] = struct{}{}
917
+ }
918
+ boundaryArmed["devrites-slice-wright"] = struct{}{}
919
+ durable, err := durableCodexV2DispatchAttempts(root, in.SessionID, in.TurnID, boundaryArmed)
920
+ if err != nil {
921
+ return err
922
+ }
923
+ for _, attempt := range durable {
924
+ if attempt.Role == "devrites-slice-wright" {
925
+ return nil
926
+ }
927
+ }
928
+ workspaceRoot, slug, _, ok := resolveWorkspace()
929
+ if !ok || !sameResolvedPath(workspaceRoot, root) {
930
+ return fmt.Errorf("active reconcile window is unavailable")
931
+ }
932
+ return lib.CaptureReconcileWrightBoundary(root, slug)
933
+ }
934
+
900
935
  func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) int {
901
936
  if h != harness.Codex {
902
937
  _, _ = io.Copy(io.Discard, stdin)
@@ -942,6 +977,13 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
942
977
  return stopDispatchBlock(h, "DevRites could not arm the required agent dispatch: "+err.Error(), stdout, stderr)
943
978
  }
944
979
  }
980
+ events, stateErr := readAgentDispatchEvents(root, in.SessionID)
981
+ if stateErr == nil {
982
+ armed, attempts := dispatchTurnState(events, in.TurnID)
983
+ if err := refreshPendingWrightBoundary(root, in, armed, attempts); err != nil {
984
+ debugf(stderr, "agent-dispatch pending wright boundary: %v", err)
985
+ }
986
+ }
945
987
  if len(roles) > 0 {
946
988
  instructions := make([]string, 0, len(roles))
947
989
  for _, role := range roles {
@@ -964,6 +1006,18 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
964
1006
  case "PreToolUse":
965
1007
  return hookAgentDispatchPreTool(h, root, in, stdout, stderr)
966
1008
 
1009
+ case "PostToolUse":
1010
+ events, err := readAgentDispatchEvents(root, in.SessionID)
1011
+ if err != nil {
1012
+ debugf(stderr, "agent-dispatch post-tool state: %v", err)
1013
+ return exitOK
1014
+ }
1015
+ armed, attempts := dispatchTurnState(events, in.TurnID)
1016
+ if err := refreshPendingWrightBoundary(root, in, armed, attempts); err != nil {
1017
+ debugf(stderr, "agent-dispatch post-tool wright boundary: %v", err)
1018
+ }
1019
+ return exitOK
1020
+
967
1021
  case "SubagentStop":
968
1022
  if in.AgentID == "" {
969
1023
  return exitOK
@@ -5,6 +5,7 @@ package main_test
5
5
  // check stdout and exit status. parity_test.go owns the parity oracle.
6
6
 
7
7
  import (
8
+ "bytes"
8
9
  "encoding/json"
9
10
  "os"
10
11
  "path/filepath"
@@ -1970,6 +1971,103 @@ func TestCodexAgentDispatchCapturesWrightBoundaryBeforeSpawn(t *testing.T) {
1970
1971
  }
1971
1972
  }
1972
1973
 
1974
+ func TestCodexAgentDispatchRefreshesUnarmedWrightBoundaryUntilDurableSpawn(t *testing.T) {
1975
+ root := newWorkspace(t)
1976
+ codeHome := t.TempDir()
1977
+ writeActive(t, root, "auth-tokens")
1978
+ workspace := filepath.Join(root, "features", "auth-tokens")
1979
+ for name, body := range map[string]string{
1980
+ ".reconcile-base": ".reconcile-base\n",
1981
+ ".reconcile-allowlist": ".reconcile-allowlist\n",
1982
+ ".reconcile-devrites": "[]\n",
1983
+ "action.log": "Prior root action\n",
1984
+ "browser-evidence.md": "Prior root evidence\n",
1985
+ "decisions.md": "Prior root decision\n",
1986
+ "evidence.md": "Prior root evidence\n",
1987
+ "footprint.log": "Prior root footprint\n",
1988
+ "state.md": "Phase: ready\n",
1989
+ "touched-files.md": "Prior root manifest\n",
1990
+ } {
1991
+ if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
1992
+ t.Fatal(err)
1993
+ }
1994
+ }
1995
+ if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
1996
+ t.Fatal(err)
1997
+ }
1998
+
1999
+ sessionID, turnID := "session-wright-durable", "turn-wright-durable"
2000
+ role := "devrites-slice-wright"
2001
+ writeCodexAgentContract(t, root, role)
2002
+ env := []string{"CODEX_HOME=" + codeHome}
2003
+ runDevritesIO(t, root, hookPayload(t, map[string]any{
2004
+ "hook_event_name": "UserPromptSubmit",
2005
+ "session_id": sessionID,
2006
+ "turn_id": turnID,
2007
+ "prompt": "Retry the retained reconciliation check.",
2008
+ }), env, "hook", "agent-dispatch", "--harness=codex")
2009
+
2010
+ wrightState := filepath.Join(workspace, ".reconcile-wright-devrites")
2011
+ initial, err := os.ReadFile(wrightState)
2012
+ if err != nil {
2013
+ t.Fatalf("read initial wright boundary: %v", err)
2014
+ }
2015
+ for _, path := range []string{
2016
+ "features/auth-tokens/action.log",
2017
+ "features/auth-tokens/browser-evidence.md",
2018
+ "features/auth-tokens/decisions.md",
2019
+ "features/auth-tokens/evidence.md",
2020
+ "features/auth-tokens/footprint.log",
2021
+ "features/auth-tokens/state.md",
2022
+ "features/auth-tokens/touched-files.md",
2023
+ } {
2024
+ if !strings.Contains(string(initial), path) {
2025
+ t.Errorf("pending wright boundary omitted %s: %s", path, initial)
2026
+ }
2027
+ }
2028
+ if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("Phase: build\n"), 0o600); err != nil {
2029
+ t.Fatal(err)
2030
+ }
2031
+ postTool := hookPayload(t, map[string]any{
2032
+ "hook_event_name": "PostToolUse",
2033
+ "session_id": sessionID,
2034
+ "turn_id": turnID,
2035
+ "tool_name": "apply_patch",
2036
+ "tool_use_id": "root-state-edit",
2037
+ "tool_input": map[string]any{"file_path": "state.md"},
2038
+ })
2039
+ if out, stderr, code := runDevritesIO(t, root, postTool, env,
2040
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2041
+ t.Fatalf("root post-tool refresh exit=%d out=%s stderr=%s", code, out, stderr)
2042
+ }
2043
+ beforeSpawn, err := os.ReadFile(wrightState)
2044
+ if err != nil {
2045
+ t.Fatalf("read refreshed wright boundary: %v", err)
2046
+ }
2047
+ if bytes.Equal(initial, beforeSpawn) {
2048
+ t.Fatal("root PostToolUse did not refresh the pending wright boundary")
2049
+ }
2050
+
2051
+ writeCodexV2Rollouts(
2052
+ t, codeHome, filepath.Dir(root), sessionID, turnID, role,
2053
+ "devrites_slice_wright_unhooked", true, true, true,
2054
+ )
2055
+ if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("Writer mutation\n"), 0o600); err != nil {
2056
+ t.Fatal(err)
2057
+ }
2058
+ if out, stderr, code := runDevritesIO(t, root, postTool, env,
2059
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2060
+ t.Fatalf("post-spawn PostToolUse exit=%d out=%s stderr=%s", code, out, stderr)
2061
+ }
2062
+ afterSpawn, err := os.ReadFile(wrightState)
2063
+ if err != nil {
2064
+ t.Fatalf("read frozen wright boundary: %v", err)
2065
+ }
2066
+ if !bytes.Equal(beforeSpawn, afterSpawn) {
2067
+ t.Fatal("durable V2 spawn did not freeze the pre-writer canonical boundary")
2068
+ }
2069
+ }
2070
+
1973
2071
  func TestCodexAgentDispatchBindsDurableV2WrightToReconcileWindow(t *testing.T) {
1974
2072
  for _, tc := range []struct {
1975
2073
  name string
@@ -123,8 +123,9 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
123
123
  Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
124
124
  dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
125
125
  `fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
126
- result in the current window. The accepted spawn fingerprints canonical state apart
127
- from the retained source baseline: pre-start root recovery records are excluded, while
126
+ result in the current window. Root lifecycle hooks keep the pending canonical
127
+ boundary current until the accepted or durable spawn, then freeze it apart from the
128
+ retained source baseline: pre-start root recovery records are excluded, while
128
129
  post-start canonical changes fail.
129
130
  Any generic worker still needs the exact allowlist or an isolated checkout;
130
131
  reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
@@ -123,8 +123,9 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
123
123
  Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
124
124
  dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
125
125
  `fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
126
- result in the current window. The accepted spawn fingerprints canonical state apart
127
- from the retained source baseline: pre-start root recovery records are excluded, while
126
+ result in the current window. Root lifecycle hooks keep the pending canonical
127
+ boundary current until the accepted or durable spawn, then freeze it apart from the
128
+ retained source baseline: pre-start root recovery records are excluded, while
128
129
  post-start canonical changes fail.
129
130
  Any generic worker still needs the exact allowlist or an isolated checkout;
130
131
  reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
@@ -57,6 +57,16 @@
57
57
  "statusMessage": "DevRites: checking test/build result"
58
58
  }
59
59
  ]
60
+ },
61
+ {
62
+ "matcher": "Bash|Shell|sh|exec_command|run_command|Edit|Write|MultiEdit|NotebookEdit|apply_patch|exec|js|python|computer|computer_use|write_stdin|run_code|Agent|Task|spawn_agent|delegate|dispatch_agent|create_agent|wait|wait_agent",
63
+ "hooks": [
64
+ {
65
+ "type": "command",
66
+ "command": "command -v devrites-engine >/dev/null 2>&1 || exit 0; cd \"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\" 2>/dev/null || exit 0; exec devrites-engine hook agent-dispatch --harness=codex",
67
+ "statusMessage": "DevRites: retaining the pre-writer canonical boundary"
68
+ }
69
+ ]
60
70
  }
61
71
  ],
62
72
  "PreCompact": [
@@ -123,8 +123,9 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
123
123
  Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
124
124
  dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
125
125
  `fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
126
- result in the current window. The accepted spawn fingerprints canonical state apart
127
- from the retained source baseline: pre-start root recovery records are excluded, while
126
+ result in the current window. Root lifecycle hooks keep the pending canonical
127
+ boundary current until the accepted or durable spawn, then freeze it apart from the
128
+ retained source baseline: pre-start root recovery records are excluded, while
128
129
  post-start canonical changes fail.
129
130
  Any generic worker still needs the exact allowlist or an isolated checkout;
130
131
  reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "3.2.12",
3
+ "version": "3.2.14",
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",
@@ -246,6 +246,16 @@ gen_codex_hooks_json() {
246
246
  "statusMessage": "DevRites: checking test/build result"
247
247
  }
248
248
  ]
249
+ },
250
+ {
251
+ "matcher": "Bash|Shell|sh|exec_command|run_command|Edit|Write|MultiEdit|NotebookEdit|apply_patch|exec|js|python|computer|computer_use|write_stdin|run_code|Agent|Task|spawn_agent|delegate|dispatch_agent|create_agent|wait|wait_agent",
252
+ "hooks": [
253
+ {
254
+ "type": "command",
255
+ "command": "command -v devrites-engine >/dev/null 2>&1 || exit 0; cd \"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\" 2>/dev/null || exit 0; exec devrites-engine hook agent-dispatch --harness=codex",
256
+ "statusMessage": "DevRites: retaining the pre-writer canonical boundary"
257
+ }
258
+ ]
249
259
  }
250
260
  ],
251
261
  "PreCompact": [