devrites 3.2.27 → 3.2.28

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,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.28](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.27...v3.2.28) (2026-07-29)
6
+
7
+ ### Fixed
8
+
9
+ * **agents:** bind durable V2 agent starts ([1da2cef](https://github.com/ViktorsBaikers/DevRites/commit/1da2cefd2a1e6b2ecb6a626224cc94fa0fc3662e))
10
+
5
11
  ## [3.2.27](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.26...v3.2.27) (2026-07-29)
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.27`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.27): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
28
+ **Status:** [`v3.2.28`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.28): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -517,7 +517,7 @@ func durableCodexV2DispatchAttempts(
517
517
  var childMeta codexRolloutRecord
518
518
  if strings.TrimSpace(spawn.Result) != "" {
519
519
  childPath, childMeta, err = findCodexV2ChildRollout(
520
- sessionsDir, parentPath, projectRoot, sessionID, spawn.AgentPath,
520
+ sessionsDir, parentPath, projectRoot, sessionID, spawn.AgentPath, "",
521
521
  )
522
522
  if err != nil {
523
523
  return nil, err
@@ -687,10 +687,14 @@ func readCodexV2ParentRollout(
687
687
  }
688
688
 
689
689
  func findCodexV2ChildRollout(
690
- sessionsDir, parentPath, projectRoot, sessionID, agentPath string,
690
+ sessionsDir, parentPath, projectRoot, sessionID, agentPath, agentID string,
691
691
  ) (string, codexRolloutRecord, error) {
692
692
  var childPath string
693
693
  var childMeta codexRolloutRecord
694
+ selector := agentPath
695
+ if selector == "" {
696
+ selector = agentID
697
+ }
694
698
  err := filepath.WalkDir(sessionsDir, func(path string, entry fs.DirEntry, walkErr error) error {
695
699
  if walkErr != nil {
696
700
  if os.IsNotExist(walkErr) {
@@ -707,12 +711,13 @@ func findCodexV2ChildRollout(
707
711
  }
708
712
  if record.Type != "session_meta" ||
709
713
  record.Payload.ParentThreadID != sessionID ||
710
- record.Payload.AgentPath != agentPath ||
714
+ (agentPath != "" && record.Payload.AgentPath != agentPath) ||
715
+ (agentID != "" && record.Payload.ID != agentID) ||
711
716
  !sameResolvedPath(record.Payload.CWD, projectRoot) {
712
717
  return nil
713
718
  }
714
719
  if childPath != "" {
715
- return fmt.Errorf("multiple Codex child rollouts match %s", agentPath)
720
+ return fmt.Errorf("multiple Codex child rollouts match %s", selector)
716
721
  }
717
722
  childPath = path
718
723
  childMeta = record
@@ -1540,6 +1545,12 @@ func bindAgentDispatchStart(root string, in agentDispatchHookInput) (string, boo
1540
1545
  started := map[string]struct{}{}
1541
1546
  for _, event := range events {
1542
1547
  if event.Event == "started" {
1548
+ if event.AgentID == in.AgentID {
1549
+ if event.AgentType != in.AgentType || event.Role == "" {
1550
+ return "", false, fmt.Errorf("subagent %s is already bound to a different dispatch", in.AgentID)
1551
+ }
1552
+ return event.Role, true, nil
1553
+ }
1543
1554
  started[event.ToolUseID] = struct{}{}
1544
1555
  }
1545
1556
  }
@@ -1558,7 +1569,7 @@ func bindAgentDispatchStart(root string, in agentDispatchHookInput) (string, boo
1558
1569
  candidates = append(candidates, event)
1559
1570
  }
1560
1571
  if len(candidates) == 0 {
1561
- return "", false, nil
1572
+ return bindDurableCodexV2Start(root, in, events)
1562
1573
  }
1563
1574
  if len(candidates) != 1 {
1564
1575
  return "", false, fmt.Errorf("ambiguous subagent start: %d pending %s dispatches lack a parent tool-use binding", len(candidates), in.AgentType)
@@ -1578,6 +1589,84 @@ func bindAgentDispatchStart(root string, in agentDispatchHookInput) (string, boo
1578
1589
  return event.Role, true, nil
1579
1590
  }
1580
1591
 
1592
+ func bindDurableCodexV2Start(
1593
+ root string, in agentDispatchHookInput, events []agentDispatchEvent,
1594
+ ) (string, bool, error) {
1595
+ if !devritesAgentNameRe.MatchString(in.AgentType) || in.AgentType == agentDispatchSkillGuard {
1596
+ return "", false, nil
1597
+ }
1598
+ parentPath, sessionsDir, err := findCodexParentRollout(in.SessionID)
1599
+ if err != nil || parentPath == "" {
1600
+ return "", false, err
1601
+ }
1602
+ projectRoot := filepath.Dir(root)
1603
+ childPath, childMeta, err := findCodexV2ChildRollout(
1604
+ sessionsDir, parentPath, projectRoot, in.SessionID, "", in.AgentID,
1605
+ )
1606
+ if err != nil || childPath == "" || childMeta.Payload.AgentRole != in.AgentType {
1607
+ return "", false, err
1608
+ }
1609
+
1610
+ turns := map[string]struct{}{}
1611
+ for _, event := range events {
1612
+ if event.TurnID != "" {
1613
+ turns[event.TurnID] = struct{}{}
1614
+ }
1615
+ }
1616
+ type binding struct {
1617
+ turnID, windowID string
1618
+ }
1619
+ var matches []binding
1620
+ for turnID := range turns {
1621
+ armed, _ := dispatchTurnState(events, turnID)
1622
+ _, required := armed[in.AgentType]
1623
+ _, guarded := armed[agentDispatchSkillGuard]
1624
+ if !required && !guarded {
1625
+ continue
1626
+ }
1627
+ spawns, _, err := readCodexV2ParentRollout(
1628
+ parentPath, projectRoot, in.SessionID, turnID, armed,
1629
+ )
1630
+ if err != nil {
1631
+ return "", false, err
1632
+ }
1633
+ spawn := spawns[childMeta.Payload.AgentPath]
1634
+ if spawn == nil || spawn.Role != in.AgentType {
1635
+ continue
1636
+ }
1637
+ windowID := ""
1638
+ if in.AgentType == "devrites-slice-wright" {
1639
+ windowID = currentReconcileWindowID()
1640
+ if windowID == "" || !reconcileWindowPredates(spawn.SpawnedAt) {
1641
+ continue
1642
+ }
1643
+ }
1644
+ matches = append(matches, binding{turnID: turnID, windowID: windowID})
1645
+ }
1646
+ if len(matches) == 0 {
1647
+ return "", false, nil
1648
+ }
1649
+ if len(matches) != 1 {
1650
+ return "", false, fmt.Errorf(
1651
+ "ambiguous durable subagent start: %d parent spawns match %s",
1652
+ len(matches), in.AgentID,
1653
+ )
1654
+ }
1655
+ match := matches[0]
1656
+ if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
1657
+ Event: "started",
1658
+ TurnID: match.turnID,
1659
+ ToolUseID: "codex-v2:" + in.AgentID,
1660
+ AgentID: in.AgentID,
1661
+ AgentType: in.AgentType,
1662
+ Role: in.AgentType,
1663
+ WindowID: match.windowID,
1664
+ }); err != nil {
1665
+ return "", false, err
1666
+ }
1667
+ return in.AgentType, true, nil
1668
+ }
1669
+
1581
1670
  type boundAgentDispatchAttempt struct {
1582
1671
  TurnID string
1583
1672
  AgentType string
@@ -2082,6 +2082,52 @@ func TestCodexAgentDispatchConfirmsDurableV2NamedRole(t *testing.T) {
2082
2082
  }
2083
2083
  }
2084
2084
 
2085
+ func TestCodexAgentDispatchBindsDurableV2NamedStartWithoutPreToolReceipt(t *testing.T) {
2086
+ root := newWorkspace(t)
2087
+ codeHome := t.TempDir()
2088
+ sessionID, turnID := "session-v2-start", "turn-v2-start"
2089
+ role := "devrites-plan-reviewer"
2090
+ taskName := "devrites_plan_reviewer_named"
2091
+ writeCodexAgentContract(t, root, role)
2092
+ writeCodexSkillContract(t, root, "rite-vet", role)
2093
+
2094
+ runDevritesIO(t, root, hookPayload(t, map[string]any{
2095
+ "hook_event_name": "UserPromptSubmit",
2096
+ "session_id": sessionID,
2097
+ "turn_id": turnID,
2098
+ "prompt": "$rite-vet",
2099
+ }), nil, "hook", "agent-dispatch", "--harness=codex")
2100
+ writeCodexV2Rollouts(
2101
+ t, codeHome, filepath.Dir(root), sessionID, turnID, role,
2102
+ taskName, false, false, false,
2103
+ )
2104
+
2105
+ mismatched, _, _ := runDevritesIO(t, root, hookPayload(t, map[string]any{
2106
+ "hook_event_name": "SubagentStart",
2107
+ "session_id": sessionID,
2108
+ "turn_id": "child-turn",
2109
+ "agent_id": "child-" + taskName,
2110
+ "agent_type": "devrites-plan-drafter",
2111
+ }), []string{"CODEX_HOME=" + codeHome}, "hook", "subagent-orient", "--harness=codex")
2112
+ if !strings.Contains(mismatched, "Unbound named start") {
2113
+ t.Fatalf("mismatched named role was bound: %s", mismatched)
2114
+ }
2115
+
2116
+ out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
2117
+ "hook_event_name": "SubagentStart",
2118
+ "session_id": sessionID,
2119
+ "turn_id": "child-turn",
2120
+ "agent_id": "child-" + taskName,
2121
+ "agent_type": role,
2122
+ }), []string{"CODEX_HOME=" + codeHome}, "hook", "subagent-orient", "--harness=codex")
2123
+ if code != 0 {
2124
+ t.Fatalf("start exit=%d stderr=%s", code, stderr)
2125
+ }
2126
+ if !strings.Contains(out, "Confirmed role") || strings.Contains(out, "Unbound named start") {
2127
+ t.Fatalf("durable named start was not bound: %s", out)
2128
+ }
2129
+ }
2130
+
2085
2131
  func TestCodexAgentDispatchConfirmsDurableV2ChildRoleWhenParentOmitsAgentType(t *testing.T) {
2086
2132
  root := newWorkspace(t)
2087
2133
  codeHome := t.TempDir()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "3.2.27",
3
+ "version": "3.2.28",
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",