devrites 3.2.21 → 3.2.22

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.22](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.21...v3.2.22) (2026-07-28)
6
+
7
+ ### Fixed
8
+
9
+ * **devrites:** prevent receipt-only agent spawns ([4cf4c19](https://github.com/ViktorsBaikers/DevRites/commit/4cf4c19cf8671b8fdd497b8875c50bd50faf85d9))
10
+
5
11
  ## [3.2.21](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.20...v3.2.21) (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.21`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.21): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
28
+ **Status:** [`v3.2.22`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.22): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -853,6 +853,10 @@ func conditionalDispatchInstruction() string {
853
853
  return "CONDITIONAL DISPATCH RULE — If this skill reaches a child-agent step, call spawn_agent with the exact named agent_type=devrites-<role> specified by the skill, a unique task_name, and fork_turns=\"none\". GPT-5.6 V2 may omit agent_type from the visible tool schema; send agent_type anyway and never use a default child. Wait for the returned child and use its non-empty result."
854
854
  }
855
855
 
856
+ func preDispatchStopInstruction() string {
857
+ return "PRE-DISPATCH STOP RULE — If a deterministic prerequisite forces STOP before any spawn attempt, do not spawn only to satisfy this receipt. Run exactly one successful standalone devrites-engine dispatch-waive <reason> before replying. Use only the matching observed predicate: no-active-workspace, wrong-phase, readiness-failed, no-eligible-work, or human-gate-before-dispatch. The successful PostToolUse receipt is required; never waive after a spawn attempt."
858
+ }
859
+
856
860
  func incompleteDispatchReason(armed map[string]struct{}, attempts []*agentDispatchAttempt) string {
857
861
  roles := make([]string, 0, len(armed))
858
862
  for role := range armed {
@@ -871,7 +875,11 @@ func incompleteDispatchReason(armed map[string]struct{}, attempts []*agentDispat
871
875
  if role == agentDispatchMetadataRole {
872
876
  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."
873
877
  }
874
- return fmt.Sprintf("DevRites dispatch for %s is not complete. %s", role, dispatchInstruction(role))
878
+ reason := fmt.Sprintf("DevRites dispatch for %s is not complete. %s", role, dispatchInstruction(role))
879
+ if len(attempts) == 0 {
880
+ reason += " " + preDispatchStopInstruction()
881
+ }
882
+ return reason
875
883
  }
876
884
 
877
885
  func preToolDeny(h harness.Harness, reason string, stdout, stderr io.Writer) int {
@@ -1022,8 +1030,9 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
1022
1030
  }
1023
1031
  fmt.Fprintf(
1024
1032
  stdout,
1025
- "MANDATORY DISPATCH THIS TURN — At the skill's dispatch step, execute every required child before finishing or claiming completion; a success phrase is not evidence.\n%s",
1033
+ "MANDATORY DISPATCH THIS TURN — At the skill's dispatch step, execute every required child before finishing or claiming completion; a success phrase is not evidence.\n%s\n%s",
1026
1034
  strings.Join(instructions, "\n"),
1035
+ preDispatchStopInstruction(),
1027
1036
  )
1028
1037
  }
1029
1038
  if skillInvoked {
@@ -1441,6 +1441,12 @@ func TestCodexAgentDispatchPromptGivesExactNamedCallBeforeCompletion(t *testing.
1441
1441
  ".codex/agents/" + role + ".toml",
1442
1442
  "send agent_type anyway",
1443
1443
  "Wait for the returned child",
1444
+ "PRE-DISPATCH STOP RULE",
1445
+ "do not spawn only to satisfy this receipt",
1446
+ "devrites-engine dispatch-waive <reason>",
1447
+ "readiness-failed",
1448
+ "human-gate-before-dispatch",
1449
+ "never waive after a spawn attempt",
1444
1450
  } {
1445
1451
  if !strings.Contains(out, want) {
1446
1452
  t.Fatalf("initial dispatch guidance omitted %q:\n%s", want, out)
@@ -1692,7 +1698,9 @@ func TestCodexAgentDispatchBlocksFalseWaitAndStop(t *testing.T) {
1692
1698
  if stopDecision.Decision != "block" ||
1693
1699
  !strings.Contains(stopDecision.Reason, "spawn_agent") ||
1694
1700
  !strings.Contains(stopDecision.Reason, "visible tool schema") ||
1695
- !strings.Contains(stopDecision.Reason, "send agent_type anyway") {
1701
+ !strings.Contains(stopDecision.Reason, "send agent_type anyway") ||
1702
+ !strings.Contains(stopDecision.Reason, "do not spawn only to satisfy this receipt") ||
1703
+ !strings.Contains(stopDecision.Reason, "dispatch-waive <reason>") {
1696
1704
  t.Fatalf("false completion not blocked: %#v", stopDecision)
1697
1705
  }
1698
1706
 
@@ -1745,7 +1753,7 @@ func TestCodexAgentDispatchRequiresSuccessfulReasonBoundWaiver(t *testing.T) {
1745
1753
  t.Fatalf("chained waiver was not denied: %s", out)
1746
1754
  }
1747
1755
 
1748
- command := "devrites-engine dispatch-waive wrong-phase"
1756
+ command := "devrites-engine dispatch-waive readiness-failed"
1749
1757
  pre := hookPayload(t, map[string]any{
1750
1758
  "hook_event_name": "PreToolUse",
1751
1759
  "session_id": sessionID,
@@ -1775,7 +1783,7 @@ func TestCodexAgentDispatchRequiresSuccessfulReasonBoundWaiver(t *testing.T) {
1775
1783
  "turn_id": turnID,
1776
1784
  "tool_name": "functions.exec_command",
1777
1785
  "tool_input": map[string]any{"cmd": command},
1778
- "tool_response": "dispatch-waive: accepted wrong-phase",
1786
+ "tool_response": "dispatch-waive: accepted readiness-failed",
1779
1787
  })
1780
1788
  if out, stderr, code := runDevritesIO(t, root, post, nil,
1781
1789
  "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
@@ -1887,6 +1895,7 @@ func TestCodexAgentDispatchConfirmsGenericRoleAndResult(t *testing.T) {
1887
1895
  }
1888
1896
  var blocked struct {
1889
1897
  Decision string `json:"decision"`
1898
+ Reason string `json:"reason"`
1890
1899
  }
1891
1900
  if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &blocked); err != nil {
1892
1901
  t.Fatalf("invalid unawaited Stop response: %v\n%s", err, out)
@@ -1894,6 +1903,9 @@ func TestCodexAgentDispatchConfirmsGenericRoleAndResult(t *testing.T) {
1894
1903
  if blocked.Decision != "block" {
1895
1904
  t.Fatalf("unawaited child result passed Stop: %s", out)
1896
1905
  }
1906
+ if strings.Contains(blocked.Reason, "dispatch-waive") {
1907
+ t.Fatalf("unavailable post-spawn waiver was suggested: %s", out)
1908
+ }
1897
1909
 
1898
1910
  wait := hookPayload(t, map[string]any{
1899
1911
  "hook_event_name": "PreToolUse",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "3.2.21",
3
+ "version": "3.2.22",
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",