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 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.3`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.3): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
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
- return preToolDeny(h, "DevRites reconcile check/close requires a confirmed, awaited devrites-slice-wright result bound to the active reconcile snapshot.", stdout, stderr)
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" || !strings.Contains(stopDecision.Reason, "spawn_agent") {
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 uses `Agent` (`Task` alias); Codex uses `spawn_agent`. V2 sends the exact named
71
- role, a unique `task_name`, and `fork_turns="none"`; Codex loads its TOML natively.
72
- Because V2 lifecycle calls bypass hooks, Stop/reconcile verify the durable rollout's
73
- role, instructions, wait, completion, and delivered result. V1 uses guarded
74
- `explorer`/`worker` with injected rules and a lifecycle receipt. Prose isn't evidence.
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 uses `Agent` (`Task` alias); Codex uses `spawn_agent`. V2 sends the exact named
71
- role, a unique `task_name`, and `fork_turns="none"`; Codex loads its TOML natively.
72
- Because V2 lifecycle calls bypass hooks, Stop/reconcile verify the durable rollout's
73
- role, instructions, wait, completion, and delivered result. V1 uses guarded
74
- `explorer`/`worker` with injected rules and a lifecycle receipt. Prose isn't evidence.
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 uses `Agent` (`spawn_agent` alias); Codex uses `spawn_agent`. V2 sends the exact named
71
- role, a unique `task_name`, and `fork_turns="none"`; Codex loads its TOML natively.
72
- Because V2 lifecycle calls bypass hooks, Stop/reconcile verify the durable rollout's
73
- role, instructions, wait, completion, and delivered result. V1 uses guarded
74
- `explorer`/`worker` with injected rules and a lifecycle receipt. Prose isn't evidence.
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",
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",