devrites 3.2.3 → 3.2.5
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 +12 -0
- package/README.md +1 -1
- package/engine/hooks_agent_dispatch.go +40 -10
- package/engine/tests/hook_test.go +46 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +6 -5
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +6 -5
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +6 -5
- package/package.json +1 -1
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.5](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.4...v3.2.5) (2026-07-25)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **agents:** retain v2 wright dispatch receipt ([84f0c05](https://github.com/ViktorsBaikers/DevRites/commit/84f0c05c9d91d85194dfd4c97593ca1c07183282))
|
|
10
|
+
|
|
11
|
+
## [3.2.4](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.3...v3.2.4) (2026-07-25)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **agents:** handle hidden v2 agent_type schema ([31ece9a](https://github.com/ViktorsBaikers/DevRites/commit/31ece9aad686fa3a3fa0308d5a3ede11f5b58c07))
|
|
16
|
+
|
|
5
17
|
## [3.2.3](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.2...v3.2.3) (2026-07-25)
|
|
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.
|
|
28
|
+
**Status:** [`v3.2.5`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.5): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
|
@@ -457,6 +457,17 @@ func dispatchTurnState(events []agentDispatchEvent, turnID string) (map[string]s
|
|
|
457
457
|
attempt.Stopped = true
|
|
458
458
|
attempt.ResultSHA256 = event.ResultSHA256
|
|
459
459
|
}
|
|
460
|
+
case "verified":
|
|
461
|
+
attempts = append(attempts, &agentDispatchAttempt{
|
|
462
|
+
Role: event.Role,
|
|
463
|
+
AgentType: event.AgentType,
|
|
464
|
+
AgentID: event.AgentID,
|
|
465
|
+
WindowID: event.WindowID,
|
|
466
|
+
Started: true,
|
|
467
|
+
Stopped: true,
|
|
468
|
+
Waited: true,
|
|
469
|
+
ResultSHA256: event.ResultSHA256,
|
|
470
|
+
})
|
|
460
471
|
}
|
|
461
472
|
}
|
|
462
473
|
return armed, attempts
|
|
@@ -771,7 +782,7 @@ func reconcileWindowPredates(at time.Time) bool {
|
|
|
771
782
|
}
|
|
772
783
|
|
|
773
784
|
func dispatchAttemptComplete(attempt *agentDispatchAttempt) bool {
|
|
774
|
-
return attempt.Started && attempt.Stopped && attempt.ResultSHA256 != ""
|
|
785
|
+
return attempt.Started && attempt.Stopped && attempt.Waited && attempt.ResultSHA256 != ""
|
|
775
786
|
}
|
|
776
787
|
|
|
777
788
|
func roleSatisfied(role, windowID string, attempts []*agentDispatchAttempt) bool {
|
|
@@ -802,7 +813,7 @@ func incompleteDispatchReason(armed map[string]struct{}, attempts []*agentDispat
|
|
|
802
813
|
return "DevRites could not determine this skill's required agents because its installed required-agent-roles contract is invalid. Reinstall or repair the DevRites skill pack before continuing."
|
|
803
814
|
}
|
|
804
815
|
return fmt.Sprintf(
|
|
805
|
-
"DevRites dispatch for %s is not complete. On MultiAgent V2 call spawn_agent with agent_type=%s, a unique task_name, and fork_turns=\"none\" so Codex loads .codex/agents/%s.toml natively. On V1 use agent_type=%s with fork_turns=\"none\" and name that role TOML in the message. Wait for the returned child and use its non-empty result. Do not call wait before spawn_agent and do not synthesize the agent result.",
|
|
816
|
+
"DevRites dispatch for %s is not complete. On MultiAgent V2 call spawn_agent with agent_type=%s, a unique task_name, and fork_turns=\"none\" so Codex loads .codex/agents/%s.toml natively. GPT-5.6 V2 may omit agent_type from the visible tool schema even though the runtime accepts it; send agent_type anyway rather than using a default child. On V1 use agent_type=%s with fork_turns=\"none\" and name that role TOML in the message. Wait for the returned child and use its non-empty result. Do not call wait before spawn_agent and do not synthesize the agent result.",
|
|
806
817
|
role, role, role, expectedAgentType(role),
|
|
807
818
|
)
|
|
808
819
|
}
|
|
@@ -1092,15 +1103,34 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
|
|
|
1092
1103
|
if windowID == "" {
|
|
1093
1104
|
return exitOK
|
|
1094
1105
|
}
|
|
1095
|
-
durable, durableErr := durableCodexV2DispatchAttempts(
|
|
1096
|
-
root, in.SessionID, in.TurnID, armed,
|
|
1097
|
-
)
|
|
1098
|
-
if durableErr != nil {
|
|
1099
|
-
return preToolDeny(h, "DevRites could not verify the Codex V2 wright result: "+durableErr.Error(), stdout, stderr)
|
|
1100
|
-
}
|
|
1101
|
-
attempts = append(attempts, durable...)
|
|
1102
1106
|
if !roleSatisfied("devrites-slice-wright", windowID, attempts) {
|
|
1103
|
-
|
|
1107
|
+
durable, durableErr := durableCodexV2DispatchAttempts(
|
|
1108
|
+
root, in.SessionID, in.TurnID, armed,
|
|
1109
|
+
)
|
|
1110
|
+
if durableErr != nil {
|
|
1111
|
+
return preToolDeny(h, "DevRites could not verify the Codex V2 wright result: "+durableErr.Error(), stdout, stderr)
|
|
1112
|
+
}
|
|
1113
|
+
attempts = append(attempts, durable...)
|
|
1114
|
+
if !roleSatisfied("devrites-slice-wright", windowID, attempts) {
|
|
1115
|
+
return preToolDeny(h, "DevRites reconcile check/close requires a confirmed, awaited devrites-slice-wright result bound to the active reconcile snapshot.", stdout, stderr)
|
|
1116
|
+
}
|
|
1117
|
+
for _, attempt := range durable {
|
|
1118
|
+
if attempt.Role != "devrites-slice-wright" || attempt.WindowID != windowID ||
|
|
1119
|
+
!dispatchAttemptComplete(attempt) {
|
|
1120
|
+
continue
|
|
1121
|
+
}
|
|
1122
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
1123
|
+
Event: "verified",
|
|
1124
|
+
TurnID: in.TurnID,
|
|
1125
|
+
AgentID: attempt.AgentID,
|
|
1126
|
+
AgentType: attempt.AgentType,
|
|
1127
|
+
Role: attempt.Role,
|
|
1128
|
+
WindowID: attempt.WindowID,
|
|
1129
|
+
ResultSHA256: attempt.ResultSHA256,
|
|
1130
|
+
}); err != nil {
|
|
1131
|
+
return preToolDeny(h, "DevRites could not persist the verified Codex V2 wright result: "+err.Error(), stdout, stderr)
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1104
1134
|
}
|
|
1105
1135
|
}
|
|
1106
1136
|
return exitOK
|
|
@@ -1218,7 +1218,10 @@ func TestCodexAgentDispatchBlocksFalseWaitAndStop(t *testing.T) {
|
|
|
1218
1218
|
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &stopDecision); err != nil {
|
|
1219
1219
|
t.Fatalf("invalid Stop response: %v\n%s", err, out)
|
|
1220
1220
|
}
|
|
1221
|
-
if stopDecision.Decision != "block" ||
|
|
1221
|
+
if stopDecision.Decision != "block" ||
|
|
1222
|
+
!strings.Contains(stopDecision.Reason, "spawn_agent") ||
|
|
1223
|
+
!strings.Contains(stopDecision.Reason, "visible tool schema") ||
|
|
1224
|
+
!strings.Contains(stopDecision.Reason, "send agent_type anyway") {
|
|
1222
1225
|
t.Fatalf("false completion not blocked: %#v", stopDecision)
|
|
1223
1226
|
}
|
|
1224
1227
|
|
|
@@ -1338,6 +1341,33 @@ func TestCodexAgentDispatchConfirmsGenericRoleAndResult(t *testing.T) {
|
|
|
1338
1341
|
"turn_id": turnID,
|
|
1339
1342
|
"stop_hook_active": false,
|
|
1340
1343
|
})
|
|
1344
|
+
out, stderr, code = runDevritesIO(t, root, stop, nil,
|
|
1345
|
+
"hook", "agent-dispatch", "--harness=codex")
|
|
1346
|
+
if code != 0 {
|
|
1347
|
+
t.Fatalf("unawaited stop exit=%d stderr=%s", code, stderr)
|
|
1348
|
+
}
|
|
1349
|
+
var blocked struct {
|
|
1350
|
+
Decision string `json:"decision"`
|
|
1351
|
+
}
|
|
1352
|
+
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &blocked); err != nil {
|
|
1353
|
+
t.Fatalf("invalid unawaited Stop response: %v\n%s", err, out)
|
|
1354
|
+
}
|
|
1355
|
+
if blocked.Decision != "block" {
|
|
1356
|
+
t.Fatalf("unawaited child result passed Stop: %s", out)
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
wait := hookPayload(t, map[string]any{
|
|
1360
|
+
"hook_event_name": "PreToolUse",
|
|
1361
|
+
"session_id": sessionID,
|
|
1362
|
+
"turn_id": turnID,
|
|
1363
|
+
"tool_name": "wait",
|
|
1364
|
+
"tool_input": map[string]any{"ids": []string{"agent-1"}},
|
|
1365
|
+
})
|
|
1366
|
+
if out, stderr, code := runDevritesIO(t, root, wait, nil,
|
|
1367
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1368
|
+
t.Fatalf("wait exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1341
1371
|
if out, stderr, code := runDevritesIO(t, root, stop, nil,
|
|
1342
1372
|
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1343
1373
|
t.Fatalf("resolved stop exit=%d out=%s stderr=%s", code, out, stderr)
|
|
@@ -1576,6 +1606,21 @@ func TestCodexAgentDispatchBindsDurableV2WrightToReconcileWindow(t *testing.T) {
|
|
|
1576
1606
|
if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
|
|
1577
1607
|
t.Fatalf("changed-window V2 wright was accepted: %s", out)
|
|
1578
1608
|
}
|
|
1609
|
+
return
|
|
1610
|
+
}
|
|
1611
|
+
for _, name := range []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"} {
|
|
1612
|
+
if err := os.Remove(filepath.Join(workspace, name)); err != nil {
|
|
1613
|
+
t.Fatal(err)
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
stop := hookPayload(t, map[string]any{
|
|
1617
|
+
"hook_event_name": "Stop",
|
|
1618
|
+
"session_id": sessionID,
|
|
1619
|
+
"turn_id": turnID,
|
|
1620
|
+
})
|
|
1621
|
+
if out, stderr, code := runDevritesIO(t, root, stop, nil,
|
|
1622
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1623
|
+
t.Fatalf("persisted V2 wright receipt was lost after close: exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1579
1624
|
}
|
|
1580
1625
|
})
|
|
1581
1626
|
}
|
|
@@ -67,11 +67,12 @@ field from the installed skill at `UserPromptSubmit` and arms a fail-closed comp
|
|
|
67
67
|
receipt for every listed role; the engine derives roles from skill metadata.
|
|
68
68
|
Conditional scouts and reviewers remain owned by their explicit phase triggers.
|
|
69
69
|
|
|
70
|
-
Claude
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
role, instructions, wait, completion, and
|
|
74
|
-
`explorer`/`worker` with injected rules and
|
|
70
|
+
Claude: `Agent`; Codex: `spawn_agent`. V2 calls the named `agent_type` with
|
|
71
|
+
unique `task_name` and `fork_turns="none"`. GPT-5.6 may hide `agent_type` from the
|
|
72
|
+
schema, but runtime accepts it and loads the TOML; never use `default`. V2 bypasses
|
|
73
|
+
hooks, so Stop/reconcile verify role, native instructions, wait, completion, and result.
|
|
74
|
+
V1 uses guarded `explorer`/`worker` with injected rules and receipt. Prose
|
|
75
|
+
isn't evidence.
|
|
75
76
|
|
|
76
77
|
## File-backed dispatch contract
|
|
77
78
|
|
|
@@ -67,11 +67,12 @@ field from the installed skill at `UserPromptSubmit` and arms a fail-closed comp
|
|
|
67
67
|
receipt for every listed role; the engine derives roles from skill metadata.
|
|
68
68
|
Conditional scouts and reviewers remain owned by their explicit phase triggers.
|
|
69
69
|
|
|
70
|
-
Claude
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
role, instructions, wait, completion, and
|
|
74
|
-
`explorer`/`worker` with injected rules and
|
|
70
|
+
Claude: `Agent`; Codex: `spawn_agent`. V2 calls the named `agent_type` with
|
|
71
|
+
unique `task_name` and `fork_turns="none"`. GPT-5.6 may hide `agent_type` from the
|
|
72
|
+
schema, but runtime accepts it and loads the TOML; never use `default`. V2 bypasses
|
|
73
|
+
hooks, so Stop/reconcile verify role, native instructions, wait, completion, and result.
|
|
74
|
+
V1 uses guarded `explorer`/`worker` with injected rules and receipt. Prose
|
|
75
|
+
isn't evidence.
|
|
75
76
|
|
|
76
77
|
## File-backed dispatch contract
|
|
77
78
|
|
|
@@ -67,11 +67,12 @@ field from the installed skill at `UserPromptSubmit` and arms a fail-closed comp
|
|
|
67
67
|
receipt for every listed role; the engine derives roles from skill metadata.
|
|
68
68
|
Conditional scouts and reviewers remain owned by their explicit phase triggers.
|
|
69
69
|
|
|
70
|
-
Claude
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
role, instructions, wait, completion, and
|
|
74
|
-
`explorer`/`worker` with injected rules and
|
|
70
|
+
Claude: `Agent`; Codex: `spawn_agent`. V2 calls the named `agent_type` with
|
|
71
|
+
unique `task_name` and `fork_turns="none"`. GPT-5.6 may hide `agent_type` from the
|
|
72
|
+
schema, but runtime accepts it and loads the TOML; never use `default`. V2 bypasses
|
|
73
|
+
hooks, so Stop/reconcile verify role, native instructions, wait, completion, and result.
|
|
74
|
+
V1 uses guarded `explorer`/`worker` with injected rules and receipt. Prose
|
|
75
|
+
isn't evidence.
|
|
75
76
|
|
|
76
77
|
## File-backed dispatch contract
|
|
77
78
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.5",
|
|
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",
|