devrites 3.2.20 → 3.2.21
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/hookpolicy.go +1 -0
- package/engine/hookpolicy_test.go +6 -0
- package/engine/hooks_agent_dispatch.go +11 -3
- package/engine/tests/hook_test.go +91 -0
- package/package.json +1 -1
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.21](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.20...v3.2.21) (2026-07-28)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **devrites:** bound Codex dispatch stop retries ([4ef9d5b](https://github.com/ViktorsBaikers/DevRites/commit/4ef9d5baf3ca74b1d494f535e8820447e9c37129))
|
|
10
|
+
|
|
5
11
|
## [3.2.20](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.19...v3.2.20) (2026-07-28)
|
|
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.21`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.21): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
package/engine/hookpolicy.go
CHANGED
|
@@ -49,6 +49,7 @@ var hookRegistry = map[string]hookDefinition{
|
|
|
49
49
|
|
|
50
50
|
// Standard hooks add the default gates, guards, sentinels, and caches.
|
|
51
51
|
"a1-guard": {tier: tierStandard, canBlock: true},
|
|
52
|
+
"agent-dispatch": {tier: tierStandard, canBlock: true},
|
|
52
53
|
"git-guard": {tier: tierStandard, canBlock: true},
|
|
53
54
|
"stop-gate": {tier: tierStandard, canBlock: true},
|
|
54
55
|
"reviewer-readonly": {tier: tierStandard, canBlock: true},
|
|
@@ -81,6 +81,12 @@ func TestBlockingHookExitAudit(t *testing.T) {
|
|
|
81
81
|
bound: "one block before the host re-enters with stop_hook_active=true",
|
|
82
82
|
killPath: "DEVRITES_DISABLED_HOOKS=stop-gate or DEVRITES_HOOK_PROFILE=minimal",
|
|
83
83
|
},
|
|
84
|
+
"agent-dispatch": {
|
|
85
|
+
reentry: "explicit stop_hook_active=true terminates the re-entered Stop with the same failure",
|
|
86
|
+
malformed: "missing or malformed event, session, or turn identity fails open",
|
|
87
|
+
bound: "one block before the host re-enters with stop_hook_active=true",
|
|
88
|
+
killPath: "DEVRITES_DISABLED_HOOKS=agent-dispatch or DEVRITES_HOOK_PROFILE=minimal",
|
|
89
|
+
},
|
|
84
90
|
"reviewer-readonly": {
|
|
85
91
|
reentry: "not applicable: each PreToolUse decision applies to one tool request",
|
|
86
92
|
malformed: "missing or malformed tool identity fails open",
|
|
@@ -527,7 +527,7 @@ func durableCodexV2DispatchAttempts(
|
|
|
527
527
|
}
|
|
528
528
|
if !devritesAgentNameRe.MatchString(role) || role == agentDispatchSkillGuard {
|
|
529
529
|
if strings.TrimSpace(spawn.Result) != "" {
|
|
530
|
-
return nil, fmt.Errorf("codex V2 completed a default or non-DevRites child during a DevRites skill turn; %s", conditionalDispatchInstruction())
|
|
530
|
+
return nil, fmt.Errorf("codex V2 completed a default or non-DevRites child during a DevRites skill turn; this turn is tainted and cannot be repaired by another dispatch. Do not use the child result or advance workflow state. End this turn, then retry the skill in a fresh turn; %s", conditionalDispatchInstruction())
|
|
531
531
|
}
|
|
532
532
|
continue
|
|
533
533
|
}
|
|
@@ -1105,7 +1105,11 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
|
|
|
1105
1105
|
case "Stop":
|
|
1106
1106
|
events, err := readAgentDispatchEvents(root, in.SessionID)
|
|
1107
1107
|
if err != nil {
|
|
1108
|
-
|
|
1108
|
+
reason := "DevRites could not verify the required agent dispatch: " + err.Error()
|
|
1109
|
+
if in.StopHookActive {
|
|
1110
|
+
return stopDispatchFailure(reason, stdout, stderr)
|
|
1111
|
+
}
|
|
1112
|
+
return stopDispatchBlock(h, reason, stdout, stderr)
|
|
1109
1113
|
}
|
|
1110
1114
|
armed, attempts := dispatchTurnState(events, in.TurnID)
|
|
1111
1115
|
_, guarded := armed[agentDispatchSkillGuard]
|
|
@@ -1114,7 +1118,11 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
|
|
|
1114
1118
|
root, in.SessionID, in.TurnID, armed,
|
|
1115
1119
|
)
|
|
1116
1120
|
if durableErr != nil {
|
|
1117
|
-
|
|
1121
|
+
reason := "DevRites could not verify the Codex V2 agent dispatch: " + durableErr.Error()
|
|
1122
|
+
if in.StopHookActive {
|
|
1123
|
+
return stopDispatchFailure(reason, stdout, stderr)
|
|
1124
|
+
}
|
|
1125
|
+
return stopDispatchBlock(h, reason, stdout, stderr)
|
|
1118
1126
|
}
|
|
1119
1127
|
attempts = append(attempts, durable...)
|
|
1120
1128
|
}
|
|
@@ -6,6 +6,8 @@ package main_test
|
|
|
6
6
|
|
|
7
7
|
import (
|
|
8
8
|
"bytes"
|
|
9
|
+
"crypto/sha256"
|
|
10
|
+
"encoding/hex"
|
|
9
11
|
"encoding/json"
|
|
10
12
|
"fmt"
|
|
11
13
|
"os"
|
|
@@ -2142,9 +2144,98 @@ func TestCodexAgentDispatchRejectsConditionalDurableV2DefaultChild(t *testing.T)
|
|
|
2142
2144
|
}
|
|
2143
2145
|
if decision.Decision != "block" ||
|
|
2144
2146
|
!strings.Contains(decision.Reason, "default or non-DevRites child") ||
|
|
2147
|
+
!strings.Contains(decision.Reason, "cannot be repaired by another dispatch") ||
|
|
2148
|
+
!strings.Contains(decision.Reason, "retry the skill in a fresh turn") ||
|
|
2145
2149
|
!strings.Contains(decision.Reason, "never use a default child") {
|
|
2146
2150
|
t.Fatalf("conditional V2 default child passed: %#v", decision)
|
|
2147
2151
|
}
|
|
2152
|
+
|
|
2153
|
+
stop = hookPayload(t, map[string]any{
|
|
2154
|
+
"hook_event_name": "Stop",
|
|
2155
|
+
"session_id": sessionID,
|
|
2156
|
+
"turn_id": turnID,
|
|
2157
|
+
"stop_hook_active": true,
|
|
2158
|
+
})
|
|
2159
|
+
out, stderr, code = runDevritesIO(t, root, stop,
|
|
2160
|
+
[]string{"CODEX_HOME=" + codeHome},
|
|
2161
|
+
"hook", "agent-dispatch", "--harness=codex")
|
|
2162
|
+
if code != 0 {
|
|
2163
|
+
t.Fatalf("conditional default reentry exit=%d stderr=%s", code, stderr)
|
|
2164
|
+
}
|
|
2165
|
+
var terminal struct {
|
|
2166
|
+
Continue bool `json:"continue"`
|
|
2167
|
+
StopReason string `json:"stopReason"`
|
|
2168
|
+
}
|
|
2169
|
+
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &terminal); err != nil {
|
|
2170
|
+
t.Fatalf("invalid conditional default reentry response: %v\n%s", err, out)
|
|
2171
|
+
}
|
|
2172
|
+
if terminal.Continue ||
|
|
2173
|
+
!strings.Contains(terminal.StopReason, "default or non-DevRites child") ||
|
|
2174
|
+
!strings.Contains(terminal.StopReason, "retry the skill in a fresh turn") {
|
|
2175
|
+
t.Fatalf("conditional V2 default child reentry did not stop: %#v", terminal)
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
func TestCodexAgentDispatchTerminatesCorruptStateReentry(t *testing.T) {
|
|
2180
|
+
root := newWorkspace(t)
|
|
2181
|
+
sessionID, turnID := "session-corrupt-state", "turn-corrupt-state"
|
|
2182
|
+
|
|
2183
|
+
runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
2184
|
+
"hook_event_name": "UserPromptSubmit",
|
|
2185
|
+
"session_id": sessionID,
|
|
2186
|
+
"turn_id": turnID,
|
|
2187
|
+
"prompt": "$rite-status",
|
|
2188
|
+
}), nil, "hook", "agent-dispatch", "--harness=codex")
|
|
2189
|
+
|
|
2190
|
+
resolvedRoot, err := filepath.EvalSymlinks(root)
|
|
2191
|
+
if err != nil {
|
|
2192
|
+
t.Fatal(err)
|
|
2193
|
+
}
|
|
2194
|
+
rootSum := sha256.Sum256([]byte(filepath.Clean(resolvedRoot)))
|
|
2195
|
+
sessionSum := sha256.Sum256([]byte(sessionID))
|
|
2196
|
+
statePath := filepath.Join(
|
|
2197
|
+
os.TempDir(),
|
|
2198
|
+
"devrites-agent-dispatch-v1",
|
|
2199
|
+
hex.EncodeToString(rootSum[:]),
|
|
2200
|
+
hex.EncodeToString(sessionSum[:])+".jsonl",
|
|
2201
|
+
)
|
|
2202
|
+
if err := os.MkdirAll(filepath.Dir(statePath), 0o700); err != nil {
|
|
2203
|
+
t.Fatal(err)
|
|
2204
|
+
}
|
|
2205
|
+
if err := os.WriteFile(statePath, []byte("not-json\n"), 0o600); err != nil {
|
|
2206
|
+
t.Fatal(err)
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
stop := func(active bool) string {
|
|
2210
|
+
out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
2211
|
+
"hook_event_name": "Stop",
|
|
2212
|
+
"session_id": sessionID,
|
|
2213
|
+
"turn_id": turnID,
|
|
2214
|
+
"stop_hook_active": active,
|
|
2215
|
+
}), nil, "hook", "agent-dispatch", "--harness=codex")
|
|
2216
|
+
if code != 0 {
|
|
2217
|
+
t.Fatalf("corrupt state stop active=%t exit=%d stderr=%s", active, code, stderr)
|
|
2218
|
+
}
|
|
2219
|
+
return out
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
var blocked struct {
|
|
2223
|
+
Decision string `json:"decision"`
|
|
2224
|
+
}
|
|
2225
|
+
if out := stop(false); json.Unmarshal([]byte(strings.TrimSpace(out)), &blocked) != nil ||
|
|
2226
|
+
blocked.Decision != "block" {
|
|
2227
|
+
t.Fatalf("corrupt state first Stop did not block: %s", out)
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
var terminal struct {
|
|
2231
|
+
Continue bool `json:"continue"`
|
|
2232
|
+
StopReason string `json:"stopReason"`
|
|
2233
|
+
}
|
|
2234
|
+
if out := stop(true); json.Unmarshal([]byte(strings.TrimSpace(out)), &terminal) != nil ||
|
|
2235
|
+
terminal.Continue ||
|
|
2236
|
+
!strings.Contains(terminal.StopReason, "invalid agent dispatch state") {
|
|
2237
|
+
t.Fatalf("corrupt state reentry did not stop: %s", out)
|
|
2238
|
+
}
|
|
2148
2239
|
}
|
|
2149
2240
|
|
|
2150
2241
|
func TestCodexAgentDispatchRejectsIncompleteDurableV2(t *testing.T) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.21",
|
|
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",
|