devrites 3.2.12 → 3.2.13
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 +6 -0
- package/README.md +1 -1
- package/engine/hooks_agent_dispatch.go +54 -0
- package/engine/tests/hook_test.go +99 -0
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +3 -2
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +3 -2
- package/pack/generated/codex/hooks.json +10 -0
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +3 -2
- package/package.json +1 -1
- package/scripts/codex-generate.sh +10 -0
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.13](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.12...v3.2.13) (2026-07-26)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **agents:** retain canonical boundary before spawn ([b290aa9](https://github.com/ViktorsBaikers/DevRites/commit/b290aa97851915afcddd85ff43855fc8e07b6860))
|
|
10
|
+
|
|
5
11
|
## [3.2.12](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.11...v3.2.12) (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.
|
|
28
|
+
**Status:** [`v3.2.13`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.13): 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
|
+
_, wrightRequired := armed["devrites-slice-wright"]
|
|
910
|
+
_, conditional := armed[agentDispatchSkillGuard]
|
|
911
|
+
if !wrightRequired && !conditional {
|
|
912
|
+
return nil
|
|
913
|
+
}
|
|
914
|
+
for _, attempt := range attempts {
|
|
915
|
+
if attempt.Role == "devrites-slice-wright" {
|
|
916
|
+
return nil
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
durable, err := durableCodexV2DispatchAttempts(root, in.SessionID, in.TurnID, armed)
|
|
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,104 @@ func TestCodexAgentDispatchCapturesWrightBoundaryBeforeSpawn(t *testing.T) {
|
|
|
1970
1971
|
}
|
|
1971
1972
|
}
|
|
1972
1973
|
|
|
1974
|
+
func TestCodexAgentDispatchRefreshesWrightBoundaryUntilDurableSpawn(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
|
+
writeCodexSkillContract(t, root, "rite-build", role)
|
|
2003
|
+
env := []string{"CODEX_HOME=" + codeHome}
|
|
2004
|
+
runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
2005
|
+
"hook_event_name": "UserPromptSubmit",
|
|
2006
|
+
"session_id": sessionID,
|
|
2007
|
+
"turn_id": turnID,
|
|
2008
|
+
"prompt": "$rite-build",
|
|
2009
|
+
}), env, "hook", "agent-dispatch", "--harness=codex")
|
|
2010
|
+
|
|
2011
|
+
wrightState := filepath.Join(workspace, ".reconcile-wright-devrites")
|
|
2012
|
+
initial, err := os.ReadFile(wrightState)
|
|
2013
|
+
if err != nil {
|
|
2014
|
+
t.Fatalf("read initial wright boundary: %v", err)
|
|
2015
|
+
}
|
|
2016
|
+
for _, path := range []string{
|
|
2017
|
+
"features/auth-tokens/action.log",
|
|
2018
|
+
"features/auth-tokens/browser-evidence.md",
|
|
2019
|
+
"features/auth-tokens/decisions.md",
|
|
2020
|
+
"features/auth-tokens/evidence.md",
|
|
2021
|
+
"features/auth-tokens/footprint.log",
|
|
2022
|
+
"features/auth-tokens/state.md",
|
|
2023
|
+
"features/auth-tokens/touched-files.md",
|
|
2024
|
+
} {
|
|
2025
|
+
if !strings.Contains(string(initial), path) {
|
|
2026
|
+
t.Errorf("pending wright boundary omitted %s: %s", path, initial)
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("Phase: build\n"), 0o600); err != nil {
|
|
2030
|
+
t.Fatal(err)
|
|
2031
|
+
}
|
|
2032
|
+
postTool := hookPayload(t, map[string]any{
|
|
2033
|
+
"hook_event_name": "PostToolUse",
|
|
2034
|
+
"session_id": sessionID,
|
|
2035
|
+
"turn_id": turnID,
|
|
2036
|
+
"tool_name": "apply_patch",
|
|
2037
|
+
"tool_use_id": "root-state-edit",
|
|
2038
|
+
"tool_input": map[string]any{"file_path": "state.md"},
|
|
2039
|
+
})
|
|
2040
|
+
if out, stderr, code := runDevritesIO(t, root, postTool, env,
|
|
2041
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
2042
|
+
t.Fatalf("root post-tool refresh exit=%d out=%s stderr=%s", code, out, stderr)
|
|
2043
|
+
}
|
|
2044
|
+
beforeSpawn, err := os.ReadFile(wrightState)
|
|
2045
|
+
if err != nil {
|
|
2046
|
+
t.Fatalf("read refreshed wright boundary: %v", err)
|
|
2047
|
+
}
|
|
2048
|
+
if bytes.Equal(initial, beforeSpawn) {
|
|
2049
|
+
t.Fatal("root PostToolUse did not refresh the pending wright boundary")
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
writeCodexV2Rollouts(
|
|
2053
|
+
t, codeHome, filepath.Dir(root), sessionID, turnID, role,
|
|
2054
|
+
"devrites_slice_wright_unhooked", true, true, true,
|
|
2055
|
+
)
|
|
2056
|
+
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("Writer mutation\n"), 0o600); err != nil {
|
|
2057
|
+
t.Fatal(err)
|
|
2058
|
+
}
|
|
2059
|
+
if out, stderr, code := runDevritesIO(t, root, postTool, env,
|
|
2060
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
2061
|
+
t.Fatalf("post-spawn PostToolUse exit=%d out=%s stderr=%s", code, out, stderr)
|
|
2062
|
+
}
|
|
2063
|
+
afterSpawn, err := os.ReadFile(wrightState)
|
|
2064
|
+
if err != nil {
|
|
2065
|
+
t.Fatalf("read frozen wright boundary: %v", err)
|
|
2066
|
+
}
|
|
2067
|
+
if !bytes.Equal(beforeSpawn, afterSpawn) {
|
|
2068
|
+
t.Fatal("durable V2 spawn did not freeze the pre-writer canonical boundary")
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1973
2072
|
func TestCodexAgentDispatchBindsDurableV2WrightToReconcileWindow(t *testing.T) {
|
|
1974
2073
|
for _, tc := range []struct {
|
|
1975
2074
|
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.
|
|
127
|
-
|
|
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.
|
|
127
|
-
|
|
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.
|
|
127
|
-
|
|
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.
|
|
3
|
+
"version": "3.2.13",
|
|
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": [
|