devrites 3.2.26 → 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 +12 -0
- package/README.md +1 -1
- package/engine/hooks_agent_dispatch.go +94 -5
- package/engine/tests/hook_test.go +46 -0
- package/pack/.claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +7 -7
- package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +3 -3
- package/pack/generated/claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +7 -7
- package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +3 -3
- package/pack/generated/codex/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +7 -7
- package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +3 -3
- 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.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
|
+
|
|
11
|
+
## [3.2.27](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.26...v3.2.27) (2026-07-29)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **rite:** separate recovery causes from symptoms ([c01f516](https://github.com/ViktorsBaikers/DevRites/commit/c01f5164ce349e5caad6ce450cfa6d76c7ce522b))
|
|
16
|
+
|
|
5
17
|
## [3.2.26](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.25...v3.2.26) (2026-07-29)
|
|
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.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",
|
|
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
|
|
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()
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Classify one causal fingerprint
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
the
|
|
5
|
+
Fingerprint the causal diagnosis, never the failing test or symptom. Reuse it while
|
|
6
|
+
plausible. If a discriminating fix proves the cause absent but the symptom remains,
|
|
7
|
+
preserve that diagnosis as a dead end and classify a new fingerprint. Failure alone
|
|
8
|
+
never resets a budget.
|
|
7
9
|
|
|
8
10
|
- `intent_gap`: desired behavior, scope, policy, or risk choice is unsettled.
|
|
9
11
|
- `spec_gap`: an acceptance outcome or product decision is missing.
|
|
@@ -20,11 +22,9 @@ Ask the engine for the canonical owner and action:
|
|
|
20
22
|
devrites-engine recovery route <class>
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
Follow
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
retry. A credential, external quota, irreversible action, or user-owned process that only
|
|
27
|
-
the human can handle gets an exact human-intervention gate; it is not retry authorization.
|
|
25
|
+
Follow `recovery-route/v1`. `humanPause: true` routes unresolved intent/spec through
|
|
26
|
+
Clarify. Technical routes stay agent-owned. Gate only an exact human credential, quota,
|
|
27
|
+
irreversible action, or user-owned process—never retry authorization.
|
|
28
28
|
|
|
29
29
|
Record the class with every new failed attempt and green clear:
|
|
30
30
|
|
|
@@ -33,9 +33,9 @@ errors: `devrites-api-interface`), then split into a backend slice (can land wit
|
|
|
33
33
|
stub consumer) and a frontend slice (can land against a mock/real contract).
|
|
34
34
|
|
|
35
35
|
## unblock
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
An exhausted fingerprint blocks diagnosis, not symptom. Route around/reslice; escalate
|
|
37
|
+
only for scope change. If proof removed that cause but symptom remains, keep the dead
|
|
38
|
+
end and plan a new diagnosis/proof fingerprint. Never clear/reuse old one.
|
|
39
39
|
|
|
40
40
|
## Always
|
|
41
41
|
Update `state.md` (phase, next step) and append a dated line to `decisions.md`
|
package/pack/generated/claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Classify one causal fingerprint
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
the
|
|
5
|
+
Fingerprint the causal diagnosis, never the failing test or symptom. Reuse it while
|
|
6
|
+
plausible. If a discriminating fix proves the cause absent but the symptom remains,
|
|
7
|
+
preserve that diagnosis as a dead end and classify a new fingerprint. Failure alone
|
|
8
|
+
never resets a budget.
|
|
7
9
|
|
|
8
10
|
- `intent_gap`: desired behavior, scope, policy, or risk choice is unsettled.
|
|
9
11
|
- `spec_gap`: an acceptance outcome or product decision is missing.
|
|
@@ -20,11 +22,9 @@ Ask the engine for the canonical owner and action:
|
|
|
20
22
|
devrites-engine recovery route <class>
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
Follow
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
retry. A credential, external quota, irreversible action, or user-owned process that only
|
|
27
|
-
the human can handle gets an exact human-intervention gate; it is not retry authorization.
|
|
25
|
+
Follow `recovery-route/v1`. `humanPause: true` routes unresolved intent/spec through
|
|
26
|
+
Clarify. Technical routes stay agent-owned. Gate only an exact human credential, quota,
|
|
27
|
+
irreversible action, or user-owned process—never retry authorization.
|
|
28
28
|
|
|
29
29
|
Record the class with every new failed attempt and green clear:
|
|
30
30
|
|
|
@@ -33,9 +33,9 @@ errors: `devrites-api-interface`), then split into a backend slice (can land wit
|
|
|
33
33
|
stub consumer) and a frontend slice (can land against a mock/real contract).
|
|
34
34
|
|
|
35
35
|
## unblock
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
An exhausted fingerprint blocks diagnosis, not symptom. Route around/reslice; escalate
|
|
37
|
+
only for scope change. If proof removed that cause but symptom remains, keep the dead
|
|
38
|
+
end and plan a new diagnosis/proof fingerprint. Never clear/reuse old one.
|
|
39
39
|
|
|
40
40
|
## Always
|
|
41
41
|
Update `state.md` (phase, next step) and append a dated line to `decisions.md`
|
package/pack/generated/codex/skills/devrites-debug-recovery/reference/cleanup-and-classify.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Classify one causal fingerprint
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
the
|
|
5
|
+
Fingerprint the causal diagnosis, never the failing test or symptom. Reuse it while
|
|
6
|
+
plausible. If a discriminating fix proves the cause absent but the symptom remains,
|
|
7
|
+
preserve that diagnosis as a dead end and classify a new fingerprint. Failure alone
|
|
8
|
+
never resets a budget.
|
|
7
9
|
|
|
8
10
|
- `intent_gap`: desired behavior, scope, policy, or risk choice is unsettled.
|
|
9
11
|
- `spec_gap`: an acceptance outcome or product decision is missing.
|
|
@@ -20,11 +22,9 @@ Ask the engine for the canonical owner and action:
|
|
|
20
22
|
devrites-engine recovery route <class>
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
Follow
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
retry. A credential, external quota, irreversible action, or user-owned process that only
|
|
27
|
-
the human can handle gets an exact human-intervention gate; it is not retry authorization.
|
|
25
|
+
Follow `recovery-route/v1`. `humanPause: true` routes unresolved intent/spec through
|
|
26
|
+
Clarify. Technical routes stay agent-owned. Gate only an exact human credential, quota,
|
|
27
|
+
irreversible action, or user-owned process—never retry authorization.
|
|
28
28
|
|
|
29
29
|
Record the class with every new failed attempt and green clear:
|
|
30
30
|
|
|
@@ -33,9 +33,9 @@ errors: `devrites-api-interface`), then split into a backend slice (can land wit
|
|
|
33
33
|
stub consumer) and a frontend slice (can land against a mock/real contract).
|
|
34
34
|
|
|
35
35
|
## unblock
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
An exhausted fingerprint blocks diagnosis, not symptom. Route around/reslice; escalate
|
|
37
|
+
only for scope change. If proof removed that cause but symptom remains, keep the dead
|
|
38
|
+
end and plan a new diagnosis/proof fingerprint. Never clear/reuse old one.
|
|
39
39
|
|
|
40
40
|
## Always
|
|
41
41
|
Update `state.md` (phase, next step) and append a dated line to `decisions.md`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "3.2.
|
|
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",
|