devrites 3.2.16 → 3.2.18

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.
Files changed (122) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/engine/Makefile +3 -3
  4. package/engine/hooks.go +20 -4
  5. package/engine/hooks_agent_dispatch.go +297 -48
  6. package/engine/hooks_events_test.go +40 -0
  7. package/engine/hooks_workspace.go +26 -6
  8. package/engine/internal/devritespaths/paths.go +80 -0
  9. package/engine/internal/install/install.go +36 -16
  10. package/engine/internal/install/install_test.go +50 -1
  11. package/engine/internal/iohooks/iohooks.go +5 -3
  12. package/engine/internal/iohooks/iohooks_test.go +11 -0
  13. package/engine/internal/lib/adoption.go +36 -11
  14. package/engine/internal/lib/adoption_test.go +30 -0
  15. package/engine/internal/lib/analyze.go +10 -2
  16. package/engine/internal/lib/archivesearch.go +41 -9
  17. package/engine/internal/lib/archivesearch_test.go +28 -0
  18. package/engine/internal/lib/buildreadiness.go +5 -1
  19. package/engine/internal/lib/closeout.go +34 -11
  20. package/engine/internal/lib/closeout_test.go +99 -0
  21. package/engine/internal/lib/context.go +6 -1
  22. package/engine/internal/lib/dispatchwaive.go +31 -0
  23. package/engine/internal/lib/dispatchwaive_test.go +23 -0
  24. package/engine/internal/lib/extensions.go +5 -1
  25. package/engine/internal/lib/learnings.go +8 -2
  26. package/engine/internal/lib/ledger.go +17 -4
  27. package/engine/internal/lib/ledger_test.go +24 -0
  28. package/engine/internal/lib/mutationgate.go +6 -2
  29. package/engine/internal/lib/reconcile.go +201 -64
  30. package/engine/internal/lib/reconcile_test.go +159 -12
  31. package/engine/internal/lib/resolve.go +8 -3
  32. package/engine/internal/lib/resolve_remediation_test.go +14 -0
  33. package/engine/internal/lib/runbook.go +4 -1
  34. package/engine/internal/lib/runbook_context_test.go +43 -0
  35. package/engine/internal/lib/testintegrity.go +4 -4
  36. package/engine/internal/lib/testintegrity_test.go +15 -0
  37. package/engine/internal/lib/tickafk.go +6 -2
  38. package/engine/internal/lib/util.go +128 -23
  39. package/engine/internal/profile/profile.go +27 -13
  40. package/engine/internal/profile/profile_test.go +13 -0
  41. package/engine/main.go +4 -1
  42. package/engine/testdata/golden/TestParityReconcile/close-window.golden +1 -0
  43. package/engine/testdata/golden/TestParityReconcile/not-git.golden +1 -1
  44. package/engine/testdata/golden/TestParityTestIntegrity/not-git.golden +1 -1
  45. package/engine/tests/hook_test.go +353 -17
  46. package/engine/tests/parity_reconcile_test.go +4 -1
  47. package/engine/tests/parity_testintegrity_test.go +1 -1
  48. package/pack/.claude/agents/devrites-code-reviewer.md +3 -0
  49. package/pack/.claude/agents/devrites-devex-reviewer.md +12 -8
  50. package/pack/.claude/agents/devrites-doubt-reviewer.md +3 -0
  51. package/pack/.claude/agents/devrites-forge-judge.md +4 -1
  52. package/pack/.claude/agents/devrites-frontend-reviewer.md +3 -0
  53. package/pack/.claude/agents/devrites-performance-reviewer.md +10 -9
  54. package/pack/.claude/agents/devrites-plan-reviewer.md +3 -0
  55. package/pack/.claude/agents/devrites-proof-runner.md +14 -14
  56. package/pack/.claude/agents/devrites-retrospector.md +3 -0
  57. package/pack/.claude/agents/devrites-security-auditor.md +3 -0
  58. package/pack/.claude/agents/devrites-simplifier-reviewer.md +3 -0
  59. package/pack/.claude/agents/devrites-slice-wright.md +4 -2
  60. package/pack/.claude/agents/devrites-spec-reviewer.md +3 -0
  61. package/pack/.claude/agents/devrites-strategy-reviewer.md +3 -0
  62. package/pack/.claude/agents/devrites-test-analyst.md +3 -0
  63. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
  64. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +10 -0
  65. package/pack/.claude/skills/devrites-source-driven/SKILL.md +1 -1
  66. package/pack/.claude/skills/rite-build/reference/phase-contract.md +17 -3
  67. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +3 -1
  68. package/pack/.claude/skills/rite-prove/SKILL.md +25 -14
  69. package/pack/.claude/skills/rite-temper/SKILL.md +1 -1
  70. package/pack/.claude/skills/rite-upgrade/SKILL.md +1 -1
  71. package/pack/.claude/skills/rite-vet/SKILL.md +18 -15
  72. package/pack/generated/claude/agents/devrites-code-reviewer.md +3 -0
  73. package/pack/generated/claude/agents/devrites-devex-reviewer.md +12 -8
  74. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +3 -0
  75. package/pack/generated/claude/agents/devrites-forge-judge.md +4 -1
  76. package/pack/generated/claude/agents/devrites-frontend-reviewer.md +3 -0
  77. package/pack/generated/claude/agents/devrites-performance-reviewer.md +10 -9
  78. package/pack/generated/claude/agents/devrites-plan-reviewer.md +3 -0
  79. package/pack/generated/claude/agents/devrites-proof-runner.md +14 -14
  80. package/pack/generated/claude/agents/devrites-retrospector.md +3 -0
  81. package/pack/generated/claude/agents/devrites-security-auditor.md +3 -0
  82. package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +3 -0
  83. package/pack/generated/claude/agents/devrites-slice-wright.md +4 -2
  84. package/pack/generated/claude/agents/devrites-spec-reviewer.md +3 -0
  85. package/pack/generated/claude/agents/devrites-strategy-reviewer.md +3 -0
  86. package/pack/generated/claude/agents/devrites-test-analyst.md +3 -0
  87. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
  88. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +10 -0
  89. package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +1 -1
  90. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +17 -3
  91. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +3 -1
  92. package/pack/generated/claude/skills/rite-prove/SKILL.md +25 -14
  93. package/pack/generated/claude/skills/rite-temper/SKILL.md +1 -1
  94. package/pack/generated/claude/skills/rite-upgrade/SKILL.md +1 -1
  95. package/pack/generated/claude/skills/rite-vet/SKILL.md +18 -15
  96. package/pack/generated/codex/agents/devrites-code-reviewer.toml +3 -0
  97. package/pack/generated/codex/agents/devrites-devex-reviewer.toml +11 -7
  98. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +3 -0
  99. package/pack/generated/codex/agents/devrites-forge-judge.toml +4 -1
  100. package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +3 -0
  101. package/pack/generated/codex/agents/devrites-performance-reviewer.toml +9 -8
  102. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +3 -0
  103. package/pack/generated/codex/agents/devrites-proof-runner.toml +13 -13
  104. package/pack/generated/codex/agents/devrites-retrospector.toml +3 -0
  105. package/pack/generated/codex/agents/devrites-security-auditor.toml +3 -0
  106. package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +3 -0
  107. package/pack/generated/codex/agents/devrites-slice-wright.toml +4 -2
  108. package/pack/generated/codex/agents/devrites-spec-reviewer.toml +3 -0
  109. package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +3 -0
  110. package/pack/generated/codex/agents/devrites-test-analyst.toml +3 -0
  111. package/pack/generated/codex/hooks.json +1 -1
  112. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
  113. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +10 -0
  114. package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +1 -1
  115. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +17 -3
  116. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +3 -1
  117. package/pack/generated/codex/skills/rite-prove/SKILL.md +25 -14
  118. package/pack/generated/codex/skills/rite-temper/SKILL.md +1 -1
  119. package/pack/generated/codex/skills/rite-upgrade/SKILL.md +1 -1
  120. package/pack/generated/codex/skills/rite-vet/SKILL.md +18 -15
  121. package/package.json +1 -1
  122. package/scripts/codex-generate.sh +1 -1
@@ -236,6 +236,18 @@ func parsePermissionDecision(t *testing.T, stdout string) (decision, reason stri
236
236
  return env.HookSpecificOutput.PermissionDecision, env.HookSpecificOutput.PermissionDecisionReason
237
237
  }
238
238
 
239
+ func parseStopDecision(t *testing.T, stdout string) (decision, reason string) {
240
+ t.Helper()
241
+ var env struct {
242
+ Decision string `json:"decision"`
243
+ Reason string `json:"reason"`
244
+ }
245
+ if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &env); err != nil {
246
+ t.Fatalf("stdout is not a valid Stop decision: %v\n%s", err, stdout)
247
+ }
248
+ return env.Decision, env.Reason
249
+ }
250
+
239
251
  func TestHookReviewerReadonlyEnforceDeniesMutatingBash(t *testing.T) {
240
252
  for _, h := range []string{"claude", "codex"} {
241
253
  h := h
@@ -260,14 +272,22 @@ func TestHookReviewerReadonlyEnforceDeniesMutatingBash(t *testing.T) {
260
272
 
261
273
  func TestHookReviewerReadonlyAllowsSafeBash(t *testing.T) {
262
274
  root := newWorkspace(t)
263
- in := `{"tool_name":"Bash","tool_input":{"command":"grep -rn foo ."}}`
264
- out, _, code := runDevritesIO(t, root, in, []string{"DEVRITES_REVIEWER_RO=enforce"},
265
- "hook", "reviewer-readonly", "--harness=claude")
266
- if code != 0 {
267
- t.Fatalf("exit = %d, want 0", code)
268
- }
269
- if strings.TrimSpace(out) != "" {
270
- t.Errorf("stdout = %q, want silent for a read-only command", out)
275
+ for _, command := range []string{
276
+ "grep -rn foo .",
277
+ `cd "` + filepath.Dir(root) + `" && git diff --binary --full-index --no-renames HEAD HEAD --`,
278
+ } {
279
+ in := hookPayload(t, map[string]any{
280
+ "tool_name": "Bash",
281
+ "tool_input": map[string]any{"command": command},
282
+ })
283
+ out, _, code := runDevritesIO(t, root, in, []string{"DEVRITES_REVIEWER_RO=enforce"},
284
+ "hook", "reviewer-readonly", "--harness=claude")
285
+ if code != 0 {
286
+ t.Fatalf("%q exit = %d, want 0", command, code)
287
+ }
288
+ if strings.TrimSpace(out) != "" {
289
+ t.Errorf("%q stdout = %q, want silent for a read-only command", command, out)
290
+ }
271
291
  }
272
292
  }
273
293
 
@@ -440,20 +460,87 @@ func TestHookReviewerReadonlyActiveLeafDeniesEveryMutationSurface(t *testing.T)
440
460
 
441
461
  func TestHookReviewerReadonlyActiveLeafAllowsBoundedProof(t *testing.T) {
442
462
  root := newWorkspace(t)
443
- in := `{"tool_name":"Bash","tool_input":{"command":"go test ./... -count=1"}}`
444
- out, _, code := runDevritesIO(t, root, in, []string{
463
+ env := []string{
445
464
  "DEVRITES_AGENT_RUN=1",
446
465
  "DEVRITES_ACTIVE_AGENT=devrites-proof-runner",
447
- }, "hook", "reviewer-readonly", "--harness=claude")
466
+ }
467
+ for _, command := range []string{
468
+ "go test ./... -count=1",
469
+ "command -v devrites-engine",
470
+ "devrites-engine conventions orient",
471
+ "devrites-engine learnings mine",
472
+ } {
473
+ in := fmt.Sprintf(`{"tool_name":"Bash","tool_input":{"command":%q}}`, command)
474
+ out, _, code := runDevritesIO(t, root, in, env,
475
+ "hook", "reviewer-readonly", "--harness=claude")
476
+ if code != 0 || strings.TrimSpace(out) != "" {
477
+ t.Fatalf("%q should be allowed: exit=%d out=%q", command, code, out)
478
+ }
479
+ }
480
+ in := `{"tool_name":"Bash","tool_input":{"command":"go test ./... -count=1"}}`
481
+ out, _, code := runDevritesIO(t, root, in, env,
482
+ "hook", "wright-scope", "--harness=claude")
448
483
  if code != 0 || strings.TrimSpace(out) != "" {
449
- t.Fatalf("bounded proof should be allowed: exit=%d out=%q", code, out)
484
+ t.Fatalf("wright hook must not shadow reviewer policy: exit=%d out=%q", code, out)
450
485
  }
451
- out, _, code = runDevritesIO(t, root, in, []string{
486
+ }
487
+
488
+ func TestHookReviewerReadonlyProofRunnerKeepsArtifactProofRootOwned(t *testing.T) {
489
+ root := newWorkspace(t)
490
+ env := []string{
452
491
  "DEVRITES_AGENT_RUN=1",
453
492
  "DEVRITES_ACTIVE_AGENT=devrites-proof-runner",
454
- }, "hook", "wright-scope", "--harness=claude")
493
+ }
494
+ for _, command := range []string{
495
+ "npm run build",
496
+ "npm run test:e2e -- tests/admin-locations.e2e.spec.ts",
497
+ "go build ./...",
498
+ "bash scripts/mutate-repository.sh",
499
+ "npm run deploy",
500
+ "make deploy",
501
+ "docker compose up -d",
502
+ "printf x > src/app.go",
503
+ "npm install left-pad",
504
+ "python -c 'open(\"src/app.go\", \"w\").write(\"x\")'",
505
+ } {
506
+ in := fmt.Sprintf(`{"tool_name":"Bash","tool_input":{"command":%q}}`, command)
507
+ out, errOut, code := runDevritesIO(t, root, in, env,
508
+ "hook", "reviewer-readonly", "--harness=claude")
509
+ if code != 0 {
510
+ t.Fatalf("root-owned proof command guard exit=%d stderr=%q", code, errOut)
511
+ }
512
+ if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
513
+ t.Fatalf("proof leaf command was not denied: command=%q out=%q", command, out)
514
+ }
515
+ }
516
+ }
517
+
518
+ func TestHookRedwatchRecognizesCodexShellAliases(t *testing.T) {
519
+ root := newWorkspace(t)
520
+ writeActive(t, root, "auth-tokens")
521
+ red := filepath.Join(root, "features", "auth-tokens", ".red")
522
+
523
+ fail := `{"tool_name":"functions.exec_command","tool_input":{"cmd":"npm test"},"tool_response":"Tests: 1 failed, 3 passed"}`
524
+ out, errOut, code := runDevritesIO(t, root, fail, nil,
525
+ "hook", "redwatch", "--harness=codex")
526
+ if code != 0 {
527
+ t.Fatalf("failing exec_command exit=%d stderr=%q", code, errOut)
528
+ }
529
+ if !strings.Contains(out, "tests/build are RED") {
530
+ t.Fatalf("failing exec_command omitted RED context: %q", out)
531
+ }
532
+ if _, err := os.Stat(red); err != nil {
533
+ t.Fatalf("failing exec_command did not create .red: %v", err)
534
+ }
535
+
536
+ pass := `{"tool_name":"exec_command","tool_input":{"cmd":"npm test"},"tool_response":"PASS: all tests passed"}`
537
+ out, errOut, code = runDevritesIO(t, root, pass, nil,
538
+ "hook", "redwatch", "--harness=codex")
455
539
  if code != 0 || strings.TrimSpace(out) != "" {
456
- t.Fatalf("wright hook must not shadow reviewer policy: exit=%d out=%q", code, out)
540
+ t.Fatalf("passing exec_command exit=%d out=%q stderr=%q", code, out, errOut)
541
+ }
542
+ if _, err := os.Stat(red); !os.IsNotExist(err) {
543
+ t.Fatalf("passing exec_command retained .red: %v", err)
457
544
  }
458
545
  }
459
546
 
@@ -502,7 +589,7 @@ func TestHookWrightScopeUsesExactOrchestratorAllowlist(t *testing.T) {
502
589
  }
503
590
  }
504
591
 
505
- func TestHookWrightScopeBindsCodexWorkerDuringReconcileWindow(t *testing.T) {
592
+ func TestHookWrightScopeRequiresBoundCodexWorkerDuringReconcileWindow(t *testing.T) {
506
593
  root := newWorkspace(t)
507
594
  writeActive(t, root, "auth-tokens")
508
595
  workspace := filepath.Join(root, "features", "auth-tokens")
@@ -519,7 +606,7 @@ func TestHookWrightScopeBindsCodexWorkerDuringReconcileWindow(t *testing.T) {
519
606
  input string
520
607
  deny bool
521
608
  }{
522
- {"listed edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"},"agent_type":"worker"}`, false},
609
+ {"unbound listed edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"},"agent_type":"worker"}`, true},
523
610
  {"unlisted edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/other.go"},"agent_type":"worker"}`, true},
524
611
  } {
525
612
  t.Run(test.name, func(t *testing.T) {
@@ -549,6 +636,90 @@ func TestHookWrightScopeBindsCodexWorkerDuringReconcileWindow(t *testing.T) {
549
636
  }
550
637
  }
551
638
 
639
+ func TestHookWrightScopeAllowsOnlyBoundCodexV1WorkerPaths(t *testing.T) {
640
+ root := newWorkspace(t)
641
+ writeActive(t, root, "auth-tokens")
642
+ workspace := filepath.Join(root, "features", "auth-tokens")
643
+ for name, body := range map[string]string{
644
+ ".wright-allowlist": "src/app.go\n",
645
+ ".reconcile-base": "snapshot\n",
646
+ ".reconcile-allowlist": "src/app.go\n",
647
+ ".reconcile-devrites": "[]\n",
648
+ } {
649
+ if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
650
+ t.Fatal(err)
651
+ }
652
+ }
653
+ if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
654
+ t.Fatal(err)
655
+ }
656
+ sessionID, turnID := "session-bound-worker", "turn-bound-worker"
657
+ writeCodexAgentContract(t, root, "devrites-slice-wright")
658
+ writeCodexSkillContract(t, root, "rite-build", "devrites-slice-wright")
659
+ runDevritesIO(t, root, hookPayload(t, map[string]any{
660
+ "hook_event_name": "UserPromptSubmit",
661
+ "session_id": sessionID,
662
+ "turn_id": turnID,
663
+ "prompt": "$rite-build",
664
+ }), nil, "hook", "agent-dispatch", "--harness=codex")
665
+ if out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
666
+ "hook_event_name": "PreToolUse",
667
+ "session_id": sessionID,
668
+ "turn_id": turnID,
669
+ "tool_name": "spawn_agent",
670
+ "tool_use_id": "spawn-bound-worker",
671
+ "tool_input": map[string]any{
672
+ "agent_type": "worker",
673
+ "fork_turns": "none",
674
+ "message": "Read .codex/agents/devrites-slice-wright.toml.",
675
+ },
676
+ }), nil, "hook", "agent-dispatch", "--harness=codex"); code != 0 {
677
+ t.Fatalf("spawn exit=%d out=%s stderr=%s", code, out, stderr)
678
+ } else if decision, reason := parsePermissionDecision(t, out); decision != "allow" {
679
+ t.Fatalf("spawn denied: %s (%s)", decision, reason)
680
+ }
681
+ if out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
682
+ "hook_event_name": "SubagentStart",
683
+ "session_id": sessionID,
684
+ "turn_id": "child-turn",
685
+ "agent_id": "bound-worker",
686
+ "agent_type": "worker",
687
+ }), nil, "hook", "subagent-orient", "--harness=codex"); code != 0 ||
688
+ !strings.Contains(out, "Confirmed role") {
689
+ t.Fatalf("start exit=%d out=%s stderr=%s", code, out, stderr)
690
+ }
691
+
692
+ env := []string{"DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1", "DEVRITES_HOOK_PROFILE=strict"}
693
+ for _, test := range []struct {
694
+ path string
695
+ deny bool
696
+ }{
697
+ {"src/app.go", false},
698
+ {"src/other.go", true},
699
+ } {
700
+ input := hookPayload(t, map[string]any{
701
+ "hook_event_name": "PreToolUse",
702
+ "session_id": sessionID,
703
+ "agent_id": "bound-worker",
704
+ "agent_type": "worker",
705
+ "tool_name": "Edit",
706
+ "tool_input": map[string]any{"file_path": test.path},
707
+ })
708
+ out, stderr, code := runDevritesIO(t, root, input, env,
709
+ "hook", "wright-scope", "--harness=codex")
710
+ if code != 0 {
711
+ t.Fatalf("%s exit=%d stderr=%s", test.path, code, stderr)
712
+ }
713
+ if test.deny {
714
+ if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
715
+ t.Fatalf("%s should be denied: %s", test.path, out)
716
+ }
717
+ } else if strings.TrimSpace(out) != "" {
718
+ t.Fatalf("%s should be allowed: %s", test.path, out)
719
+ }
720
+ }
721
+ }
722
+
552
723
  func TestHookWrightScopeAcceptsCodexV2NamedRole(t *testing.T) {
553
724
  root := newWorkspace(t)
554
725
  writeActive(t, root, "auth-tokens")
@@ -629,6 +800,8 @@ func TestHookWrightScopeRequiresSpawnedCodexWorkerForSourceWrites(t *testing.T)
629
800
  runGuard(`{"tool_name":"Write","tool_input":{"file_path":"src/app.go"}}`, true)
630
801
  runGuard(`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, true)
631
802
  runGuard(`{"tool_name":"js","tool_input":{"code":"writeFile()"}}`, true)
803
+ runGuard(`{"tool_name":"Bash","tool_input":{"command":"devrites-engine reconcile restore-check auth-tokens"}}`, false)
804
+ runGuard(`{"tool_name":"Bash","tool_input":{"command":"devrites-engine reconcile restore-check auth-tokens && printf x > src/app.go"}}`, true)
632
805
  out, errOut, code := runDevritesIO(t, root,
633
806
  `{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`,
634
807
  nil, "hook", "wright-scope", "--harness=claude")
@@ -1544,6 +1717,74 @@ func TestCodexAgentDispatchBlocksFalseWaitAndStop(t *testing.T) {
1544
1717
  }
1545
1718
  }
1546
1719
 
1720
+ func TestCodexAgentDispatchRequiresSuccessfulReasonBoundWaiver(t *testing.T) {
1721
+ root := newWorkspace(t)
1722
+ role := "devrites-plan-reviewer"
1723
+ writeCodexAgentContract(t, root, role)
1724
+ writeCodexSkillContract(t, root, "rite-vet", role)
1725
+ sessionID, turnID := "session-waiver", "turn-waiver"
1726
+ runDevritesIO(t, root, hookPayload(t, map[string]any{
1727
+ "hook_event_name": "UserPromptSubmit",
1728
+ "session_id": sessionID,
1729
+ "turn_id": turnID,
1730
+ "prompt": "$rite-vet",
1731
+ }), nil, "hook", "agent-dispatch", "--harness=codex")
1732
+
1733
+ invalid := hookPayload(t, map[string]any{
1734
+ "hook_event_name": "PreToolUse",
1735
+ "session_id": sessionID,
1736
+ "turn_id": turnID,
1737
+ "tool_name": "functions.exec_command",
1738
+ "tool_input": map[string]any{"cmd": "devrites-engine dispatch-waive wrong-phase && true"},
1739
+ })
1740
+ out, _, _ := runDevritesIO(t, root, invalid, nil,
1741
+ "hook", "agent-dispatch", "--harness=codex")
1742
+ if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
1743
+ t.Fatalf("chained waiver was not denied: %s", out)
1744
+ }
1745
+
1746
+ command := "devrites-engine dispatch-waive wrong-phase"
1747
+ pre := hookPayload(t, map[string]any{
1748
+ "hook_event_name": "PreToolUse",
1749
+ "session_id": sessionID,
1750
+ "turn_id": turnID,
1751
+ "tool_name": "functions.exec_command",
1752
+ "tool_input": map[string]any{"cmd": command},
1753
+ })
1754
+ if out, stderr, code := runDevritesIO(t, root, pre, nil,
1755
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1756
+ t.Fatalf("waiver pre exit=%d out=%s stderr=%s", code, out, stderr)
1757
+ }
1758
+
1759
+ stop := hookPayload(t, map[string]any{
1760
+ "hook_event_name": "Stop",
1761
+ "session_id": sessionID,
1762
+ "turn_id": turnID,
1763
+ })
1764
+ out, _, _ = runDevritesIO(t, root, stop, nil,
1765
+ "hook", "agent-dispatch", "--harness=codex")
1766
+ if decision, _ := parseStopDecision(t, out); decision != "block" {
1767
+ t.Fatalf("PreToolUse waiver falsely satisfied dispatch: %s", out)
1768
+ }
1769
+
1770
+ post := hookPayload(t, map[string]any{
1771
+ "hook_event_name": "PostToolUse",
1772
+ "session_id": sessionID,
1773
+ "turn_id": turnID,
1774
+ "tool_name": "functions.exec_command",
1775
+ "tool_input": map[string]any{"cmd": command},
1776
+ "tool_response": "dispatch-waive: accepted wrong-phase",
1777
+ })
1778
+ if out, stderr, code := runDevritesIO(t, root, post, nil,
1779
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1780
+ t.Fatalf("waiver post exit=%d out=%s stderr=%s", code, out, stderr)
1781
+ }
1782
+ if out, stderr, code := runDevritesIO(t, root, stop, nil,
1783
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1784
+ t.Fatalf("waived stop exit=%d out=%s stderr=%s", code, out, stderr)
1785
+ }
1786
+ }
1787
+
1547
1788
  func TestCodexAgentDispatchConfirmsGenericRoleAndResult(t *testing.T) {
1548
1789
  root := newWorkspace(t)
1549
1790
  sessionID, turnID := "session-complete", "turn-complete"
@@ -1664,12 +1905,99 @@ func TestCodexAgentDispatchConfirmsGenericRoleAndResult(t *testing.T) {
1664
1905
  t.Fatalf("wait exit=%d out=%s stderr=%s", code, out, stderr)
1665
1906
  }
1666
1907
 
1908
+ if out, stderr, code := runDevritesIO(t, root, stop, nil,
1909
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 {
1910
+ t.Fatalf("pre-wait stop exit=%d out=%s stderr=%s", code, out, stderr)
1911
+ } else if decision, _ := parseStopDecision(t, out); decision != "block" {
1912
+ t.Fatalf("PreToolUse wait falsely satisfied dispatch: %s", out)
1913
+ }
1914
+
1915
+ waitTimeout := hookPayload(t, map[string]any{
1916
+ "hook_event_name": "PostToolUse",
1917
+ "session_id": sessionID,
1918
+ "turn_id": turnID,
1919
+ "tool_name": "wait",
1920
+ "tool_input": map[string]any{"ids": []string{"agent-1"}},
1921
+ "tool_response": map[string]any{"timed_out": true},
1922
+ })
1923
+ if out, stderr, code := runDevritesIO(t, root, waitTimeout, nil,
1924
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1925
+ t.Fatalf("timed-out wait post exit=%d out=%s stderr=%s", code, out, stderr)
1926
+ }
1927
+ if out, stderr, code := runDevritesIO(t, root, stop, nil,
1928
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 {
1929
+ t.Fatalf("post-timeout stop exit=%d out=%s stderr=%s", code, out, stderr)
1930
+ } else if decision, _ := parseStopDecision(t, out); decision != "block" {
1931
+ t.Fatalf("timed-out wait falsely satisfied dispatch: %s", out)
1932
+ }
1933
+
1934
+ waitDone := hookPayload(t, map[string]any{
1935
+ "hook_event_name": "PostToolUse",
1936
+ "session_id": sessionID,
1937
+ "turn_id": turnID,
1938
+ "tool_name": "wait",
1939
+ "tool_input": map[string]any{"ids": []string{"agent-1"}},
1940
+ "tool_response": map[string]any{"status": "completed"},
1941
+ })
1942
+ if out, stderr, code := runDevritesIO(t, root, waitDone, nil,
1943
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1944
+ t.Fatalf("wait post exit=%d out=%s stderr=%s", code, out, stderr)
1945
+ }
1946
+
1667
1947
  if out, stderr, code := runDevritesIO(t, root, stop, nil,
1668
1948
  "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1669
1949
  t.Fatalf("resolved stop exit=%d out=%s stderr=%s", code, out, stderr)
1670
1950
  }
1671
1951
  }
1672
1952
 
1953
+ func TestCodexAgentDispatchRefusesAmbiguousGenericStart(t *testing.T) {
1954
+ root := newWorkspace(t)
1955
+ sessionID, turnID := "session-ambiguous-start", "turn-ambiguous-start"
1956
+ roles := []string{"devrites-security-auditor", "devrites-plan-reviewer"}
1957
+ for _, role := range roles {
1958
+ writeCodexAgentContract(t, root, role)
1959
+ }
1960
+ writeCodexSkillContract(t, root, "devrites-audit", strings.Join(roles, ","))
1961
+ runDevritesIO(t, root, hookPayload(t, map[string]any{
1962
+ "hook_event_name": "UserPromptSubmit",
1963
+ "session_id": sessionID,
1964
+ "turn_id": turnID,
1965
+ "prompt": "$devrites-audit",
1966
+ }), nil, "hook", "agent-dispatch", "--harness=codex")
1967
+
1968
+ for i, role := range roles {
1969
+ out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1970
+ "hook_event_name": "PreToolUse",
1971
+ "session_id": sessionID,
1972
+ "turn_id": turnID,
1973
+ "tool_name": "spawn_agent",
1974
+ "tool_use_id": fmt.Sprintf("spawn-%d", i),
1975
+ "tool_input": map[string]any{
1976
+ "agent_type": "explorer",
1977
+ "fork_turns": "none",
1978
+ "message": "Read .codex/agents/" + role + ".toml.",
1979
+ },
1980
+ }), nil, "hook", "agent-dispatch", "--harness=codex")
1981
+ if code != 0 {
1982
+ t.Fatalf("spawn %s exit=%d out=%s stderr=%s", role, code, out, stderr)
1983
+ }
1984
+ }
1985
+
1986
+ out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1987
+ "hook_event_name": "SubagentStart",
1988
+ "session_id": sessionID,
1989
+ "turn_id": "child-turn",
1990
+ "agent_id": "ambiguous-agent",
1991
+ "agent_type": "explorer",
1992
+ }), nil, "hook", "subagent-orient", "--harness=codex")
1993
+ if code != 0 {
1994
+ t.Fatalf("start exit=%d stderr=%s", code, stderr)
1995
+ }
1996
+ if !strings.Contains(out, "Dispatch binding failed") {
1997
+ t.Fatalf("ambiguous generic start was bound to an arbitrary role: %s", out)
1998
+ }
1999
+ }
2000
+
1673
2001
  func TestCodexAgentDispatchConfirmsDurableV2NamedRole(t *testing.T) {
1674
2002
  root := newWorkspace(t)
1675
2003
  codeHome := t.TempDir()
@@ -1960,6 +2288,14 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
1960
2288
  "tool_name": "wait",
1961
2289
  "tool_input": map[string]any{"ids": []string{"agent-wright"}},
1962
2290
  },
2291
+ {
2292
+ "hook_event_name": "PostToolUse",
2293
+ "session_id": sessionID,
2294
+ "turn_id": turnID,
2295
+ "tool_name": "wait",
2296
+ "tool_input": map[string]any{"ids": []string{"agent-wright"}},
2297
+ "tool_response": map[string]any{"status": "completed"},
2298
+ },
1963
2299
  } {
1964
2300
  hook := "agent-dispatch"
1965
2301
  if payload["hook_event_name"] == "SubagentStart" {
@@ -71,6 +71,9 @@ func TestParityReconcile(t *testing.T) {
71
71
  t.Run("close-window", func(t *testing.T) {
72
72
  work := newRepo(t)
73
73
  snapshot(t, work)
74
+ if out, code := runArgv(t, work, libRootEnv(work), "", binPath, "reconcile", "check", "feat"); code != 0 {
75
+ t.Fatalf("pre-close reconcile check = %d, want 0\n%s", code, out)
76
+ }
74
77
  out, code := runArgv(t, work, libRootEnv(work), "", binPath, "reconcile", "close", "feat")
75
78
  assertGolden(t, out, code)
76
79
  })
@@ -78,7 +81,7 @@ func TestParityReconcile(t *testing.T) {
78
81
  t.Run("not-git", func(t *testing.T) {
79
82
  work := t.TempDir()
80
83
  makeFeatureDir(t, work, "feat")
81
- check(t, work) // exit 0
84
+ check(t, work) // fail closed outside Git
82
85
  })
83
86
 
84
87
  t.Run("bad-mode", func(t *testing.T) {
@@ -58,7 +58,7 @@ func TestParityTestIntegrity(t *testing.T) {
58
58
  runCase(t, newRepo(t), "ghost")
59
59
  })
60
60
  t.Run("not-git", func(t *testing.T) {
61
- work := t.TempDir() // no initGitRepo -> gate skipped
61
+ work := t.TempDir() // no initGitRepo -> gate fails closed
62
62
  makeFeatureDir(t, work, "feat")
63
63
  runCase(t, work, "feat")
64
64
  })
@@ -98,6 +98,9 @@ not as a blocker on this diff.
98
98
  - If you can't verify something, say so explicitly rather than assuming it's fine.
99
99
 
100
100
  ## Output
101
+
102
+ Wrap the report in the standards `agent-result/v1` envelope with
103
+ `payload.type: review-findings`; never return raw prose.
101
104
  ```
102
105
  Code review (<slug>) — independent
103
106
  [Critical] file:line — problem. fix.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: devrites-devex-reviewer
3
3
  description: Reviews the developer experience of one DevRites feature with fresh context. At /rite-vet it predicts the experience; at /rite-seal it measures and reconciles it. Use for public APIs, CLIs, SDKs, libraries, webhooks, config or environment contracts, error messages, and getting-started flows. Finds where the next developer gets stuck instead of rubber-stamping the surface.
4
- tools: Read, Grep, Glob, Bash
4
+ tools: Read, Grep, Glob
5
5
  hooks:
6
6
  PreToolUse:
7
7
  - matcher: Edit|Write|MultiEdit|NotebookEdit|Bash|Agent|Task
@@ -48,10 +48,10 @@ You receive a feature slug or workspace path (`.devrites/work/<slug>/`) and the
48
48
  diff scope. Read `spec.md`, `plan.md`, `decisions.md`, `touched-files.md`, and
49
49
  `.devrites/principles.md` if present. Public API invariants in the principles file
50
50
  are binding. Read `devex.md` if present for predicted or measured scores, plus
51
- `evidence.md` and `browser-evidence.md` for the measured run. Run `git diff` for
52
- the feature scope and inspect the developer-facing files it touches, including
53
- route handlers, CLI entry points, exported signatures, README or quickstart
54
- instructions, and error or exit paths.
51
+ `evidence.md`, `browser-evidence.md`, and the root-supplied immutable diff and
52
+ quickstart log for the measured run. Inspect the developer-facing files it
53
+ touches, including route handlers, CLI entry points, exported signatures, README
54
+ or quickstart instructions, and error or exit paths.
55
55
 
56
56
  ## Review (developer-facing surface, feature scope only)
57
57
 
@@ -62,9 +62,10 @@ consistently:
62
62
  - **Time-to-hello-world:** in measure mode, record the wall-clock time to one
63
63
  successful call or response. In predict mode, estimate it. This is the headline
64
64
  number.
65
- - **Getting-started friction:** run the quickstart exactly as written on a clean
66
- checkout. An undocumented prerequisite, wrong command, or missing step is a
67
- finding.
65
+ - **Getting-started friction:** validate the root-owned clean-checkout quickstart
66
+ transcript, exact commands, timings, and candidate identity. An undocumented
67
+ prerequisite, wrong command, missing step, or stale/mismatched transcript is a
68
+ finding. Never execute the quickstart yourself.
68
69
  - **Error-message quality:** a failure must say what failed, why, and how to
69
70
  recover, include the relevant IDs, and expose **no secrets** under `security.md`.
70
71
  A bare trace, silent exit, or "an error occurred" on a developer-facing path is a
@@ -96,6 +97,9 @@ consistently:
96
97
  invent a DX problem to justify the pass.
97
98
 
98
99
  ## Output
100
+
101
+ Wrap the report in the standards `agent-result/v1` envelope with
102
+ `payload.type: review-findings`; never return raw prose.
99
103
  ```
100
104
  DevEx review (<slug>) — independent · mode: predict | measure
101
105
  Scorecard:
@@ -54,6 +54,9 @@ Critical. Treat overrides as review input, not permission.
54
54
  - Be concrete: the exact scenario that breaks it, with `file:line` where relevant.
55
55
 
56
56
  ## Output
57
+
58
+ Wrap the report in the standards `agent-result/v1` envelope with
59
+ `payload.type: review-findings`; never return raw prose.
57
60
  ```
58
61
  Doubt review
59
62
  Claim: <restated>
@@ -36,7 +36,7 @@ Read the review bar once: `spec.md` and the slice acceptance criteria;
36
36
  For each candidate, read only the immutable extracted delta:
37
37
 
38
38
  ```bash
39
- git -C "<manifest primary_root>" diff --binary --full-index --no-renames \
39
+ cd "<validated manifest primary_root>" && git diff --binary --full-index --no-renames \
40
40
  "<candidate initial_base>" "<candidate commit>" --
41
41
  ```
42
42
 
@@ -94,6 +94,9 @@ Cite the diff line before the mark, and never rank candidates by general impress
94
94
  - The strategy a candidate was assigned is context, not credit: score the code it produced.
95
95
 
96
96
  ## Output
97
+
98
+ Wrap the report in the standards `agent-result/v1` envelope with
99
+ `payload.type: review-findings`; never return raw prose.
97
100
  ```
98
101
  Forge verdict (<slug> / <slice id>) — independent, comparative
99
102
  Candidates scored (each, evidence → mark):
@@ -72,6 +72,9 @@ project's tokens, shared components, and neighboring screens.
72
72
  with `file:line` and a concrete fix. Feature scope only.
73
73
 
74
74
  ## Output
75
+
76
+ Wrap the report in the standards `agent-result/v1` envelope with
77
+ `payload.type: review-findings`; never return raw prose.
75
78
  ```
76
79
  Frontend review (<slug>) — independent
77
80
  System alignment: <drift by root cause>
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: devrites-performance-reviewer
3
3
  description: Reviews one DevRites feature for /rite-seal from a fresh context, starting with measurement. Checks N+1 queries, hot-path work, payload and bundle size, and Core Web Vitals risks. Source mode reports potential findings from a static scan; Measured mode grades real Lighthouse, PSI, CrUX, or trace results with a source-labeled scorecard. Never claims a slowdown without a number or a concrete measurement, and never presents lab data as field data.
4
- tools: Read, Grep, Glob, Bash
4
+ tools: Read, Grep, Glob
5
5
  hooks:
6
6
  PreToolUse:
7
7
  - matcher: Edit|Write|MultiEdit|NotebookEdit|Bash|Agent|Task
@@ -29,16 +29,14 @@ remains a Critical. Treat overrides as review input, not permission.
29
29
 
30
30
  ## Inputs
31
31
  In workspace `.devrites/work/<slug>/`, read `spec.md` for any performance budget,
32
- then `evidence.md` and `touched-files.md`. Run `git diff` and inspect the touched
33
- files. Look for Core Web Vitals evidence in `evidence.md`, a saved Lighthouse,
32
+ then `evidence.md`, `touched-files.md`, and the immutable diff supplied by the root.
33
+ Look for Core Web Vitals evidence in `evidence.md`, a saved Lighthouse,
34
34
  PageSpeed Insights, or CrUX JSON artifact, and `browser-evidence.md`.
35
35
 
36
- Read the baseline checklist on demand (resolve the path like the readonly hook):
37
- ```
38
- C=.claude/skills/rite-review/reference/performance-checklist.md
39
- [ -f "$C" ] || C="$CLAUDE_PLUGIN_ROOT/pack/.claude/skills/rite-review/reference/performance-checklist.md"
40
- [ -f "$C" ] || C=pack/.claude/skills/rite-review/reference/performance-checklist.md
41
- ```
36
+ Read the first existing baseline checklist in this order with `Read`:
37
+ `.claude/skills/rite-review/reference/performance-checklist.md`,
38
+ the packet-provided plugin-root copy, then
39
+ `pack/.claude/skills/rite-review/reference/performance-checklist.md`.
42
40
 
43
41
  ## Two modes (the inputs set the mode, not a flag)
44
42
  - **Source mode:** use this default when there are no performance artifacts. Scan
@@ -87,6 +85,9 @@ when a scorecard is allowed.
87
85
 
88
86
  ## Output
89
87
 
88
+ Wrap the report in the standards `agent-result/v1` envelope with
89
+ `payload.type: review-findings`; never return raw prose.
90
+
90
91
  **Measured mode**: lead with a compact scorecard, then the line findings:
91
92
  ```
92
93
  Performance review (<slug>) — independent
@@ -102,6 +102,9 @@ gap remains.
102
102
  lower its confidence.
103
103
 
104
104
  ## Output
105
+
106
+ Wrap the report in the standards `agent-result/v1` envelope with
107
+ `payload.type: review-findings`; never return raw prose.
105
108
  ```
106
109
  Plan review (<slug>) — independent, pre-build
107
110
  Dimension bands (evidence → band):