devrites 3.2.8 → 3.2.10
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/docs/orchestration.md +10 -0
- package/engine/hooks.go +1 -1
- package/engine/hooks_agent_dispatch.go +3 -3
- package/engine/hooks_workspace.go +79 -48
- package/engine/internal/harness/harness.go +2 -1
- package/engine/internal/harness/harness_test.go +4 -0
- package/engine/tests/hook_test.go +76 -4
- 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.10](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.9...v3.2.10) (2026-07-26)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **devrites:** allow external dispatch scratch ([fdaac3f](https://github.com/ViktorsBaikers/DevRites/commit/fdaac3f9aaf19af897c6d3d70ce01fc046a60d3e))
|
|
10
|
+
|
|
11
|
+
## [3.2.9](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.8...v3.2.9) (2026-07-26)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **devrites:** unblock retained reconcile checks ([c1093b0](https://github.com/ViktorsBaikers/DevRites/commit/c1093b00c3e32ba3cc22e0b9a2da548618ead621))
|
|
16
|
+
|
|
5
17
|
## [3.2.8](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.7...v3.2.8) (2026-07-26)
|
|
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.10`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.10): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
package/docs/orchestration.md
CHANGED
|
@@ -69,6 +69,16 @@ Each dispatch follows the retained-baseline sequence:
|
|
|
69
69
|
5. After proof and decision checks pass, `reconcile close` retires the private
|
|
70
70
|
window; only then does the root write canonical records.
|
|
71
71
|
|
|
72
|
+
An ordinary `reconcile check` may revalidate an existing retained window
|
|
73
|
+
without a fresh current-turn wright receipt; it does not retire or widen that
|
|
74
|
+
window. `reconcile close` still requires the confirmed, awaited wright result
|
|
75
|
+
bound to the active snapshot. While the window is armed, the root guard permits
|
|
76
|
+
inspectable writes only under `.devrites` or at an absolute scratch path outside
|
|
77
|
+
the project; source-tree writes, relative escapes, and opaque execution remain
|
|
78
|
+
blocked. The standalone A1 hook and wright-scope hook share this classification;
|
|
79
|
+
Codex `cmd` and Claude `command` payloads may create a literal external packet
|
|
80
|
+
with a patch, heredoc, or copy and may hash it without gaining source authority.
|
|
81
|
+
|
|
72
82
|
The canonical-state fingerprint excludes root-owned operational records:
|
|
73
83
|
`timeline.jsonl`, feature `events.jsonl`, a valid `recovery-attempts.jsonl`,
|
|
74
84
|
the redwatch sentinel, automatic handoff snapshots, and hidden hook observation
|
package/engine/hooks.go
CHANGED
|
@@ -377,7 +377,7 @@ func safeReadonlyShellSegment(segment string) bool {
|
|
|
377
377
|
"grep", "rg", "wc", "sort", "uniq", "cut", "tr", "stat", "file",
|
|
378
378
|
"readlink", "realpath", "basename", "dirname", "cmp", "diff", "jq", "yq",
|
|
379
379
|
"tree", "du", "df", "printenv", "which", "date", "uname", "id", "whoami",
|
|
380
|
-
"ps", "echo", "printf", "test", "[", "cd":
|
|
380
|
+
"ps", "echo", "printf", "test", "[", "cd", "sha256sum", "shasum":
|
|
381
381
|
return true
|
|
382
382
|
case "find":
|
|
383
383
|
return !hasAnyArg(args, "-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprintf")
|
|
@@ -33,7 +33,7 @@ var (
|
|
|
33
33
|
devritesAgentPathRe = regexp.MustCompile(`\.codex/agents/(devrites-[a-z0-9-]+)\.toml`)
|
|
34
34
|
devritesAgentNameRe = regexp.MustCompile(`^devrites-[a-z0-9-]+$`)
|
|
35
35
|
skillInvocationRe = regexp.MustCompile(`(?i)(?:^|[^a-z0-9_./-])(?:\$|/)([a-z][a-z0-9-]*)\b`)
|
|
36
|
-
|
|
36
|
+
reconcileCloseRe = regexp.MustCompile(`(?m)(?:^|[;&|]\s*)(?:rtk\s+)?(?:[A-Za-z0-9_./-]+/)?devrites-engine\s+reconcile\s+close\b`)
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
type agentDispatchHookInput struct {
|
|
@@ -1163,7 +1163,7 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
|
|
|
1163
1163
|
return exitOK
|
|
1164
1164
|
}
|
|
1165
1165
|
|
|
1166
|
-
if isShellTool(in.ToolName) &&
|
|
1166
|
+
if isShellTool(in.ToolName) && reconcileCloseRe.MatchString(in.ToolInput.Command) {
|
|
1167
1167
|
windowID := currentReconcileWindowID()
|
|
1168
1168
|
if windowID == "" {
|
|
1169
1169
|
return exitOK
|
|
@@ -1177,7 +1177,7 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
|
|
|
1177
1177
|
}
|
|
1178
1178
|
attempts = append(attempts, durable...)
|
|
1179
1179
|
if !roleSatisfied("devrites-slice-wright", windowID, attempts) {
|
|
1180
|
-
return preToolDeny(h, "DevRites reconcile
|
|
1180
|
+
return preToolDeny(h, "DevRites reconcile close requires a confirmed, awaited devrites-slice-wright result bound to the active reconcile snapshot.", stdout, stderr)
|
|
1181
1181
|
}
|
|
1182
1182
|
for _, attempt := range durable {
|
|
1183
1183
|
if attempt.Role != "devrites-slice-wright" || attempt.WindowID != windowID ||
|
|
@@ -325,6 +325,8 @@ func redwatchSafe(cmd string) string {
|
|
|
325
325
|
// node regex the shell guards use.
|
|
326
326
|
var patchPathRe = regexp.MustCompile(`(?m)^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$`)
|
|
327
327
|
|
|
328
|
+
var shellHeredocStartRe = regexp.MustCompile(`<<(-?)[ \t]*(?:'([A-Za-z_][A-Za-z0-9_]*)'|"([A-Za-z_][A-Za-z0-9_]*)"|([A-Za-z_][A-Za-z0-9_]*))`)
|
|
329
|
+
|
|
328
330
|
// isEditTool reports whether a tool can write source. toolBaseName also handles
|
|
329
331
|
// host-qualified names such as functions.apply_patch and MCP symbol editors.
|
|
330
332
|
func isEditTool(tool string) bool {
|
|
@@ -346,6 +348,39 @@ func patchPaths(command string) []string {
|
|
|
346
348
|
return paths
|
|
347
349
|
}
|
|
348
350
|
|
|
351
|
+
func stripShellHeredocBodies(command string) (string, bool) {
|
|
352
|
+
lines := strings.Split(strings.ReplaceAll(command, "\r\n", "\n"), "\n")
|
|
353
|
+
out := make([]string, 0, len(lines))
|
|
354
|
+
for i := 0; i < len(lines); i++ {
|
|
355
|
+
line := lines[i]
|
|
356
|
+
out = append(out, line)
|
|
357
|
+
for _, match := range shellHeredocStartRe.FindAllStringSubmatch(line, -1) {
|
|
358
|
+
delimiter := match[2]
|
|
359
|
+
if delimiter == "" {
|
|
360
|
+
delimiter = match[3]
|
|
361
|
+
}
|
|
362
|
+
if delimiter == "" {
|
|
363
|
+
return "", false
|
|
364
|
+
}
|
|
365
|
+
found := false
|
|
366
|
+
for i++; i < len(lines); i++ {
|
|
367
|
+
candidate := lines[i]
|
|
368
|
+
if match[1] == "-" {
|
|
369
|
+
candidate = strings.TrimLeft(candidate, "\t")
|
|
370
|
+
}
|
|
371
|
+
if candidate == delimiter {
|
|
372
|
+
found = true
|
|
373
|
+
break
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if !found {
|
|
377
|
+
return "", false
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return strings.Join(out, "\n"), true
|
|
382
|
+
}
|
|
383
|
+
|
|
349
384
|
// shellOutputRedirections removes harmless fd redirects and returns every file
|
|
350
385
|
// target of >, >>, or >|. Dynamic targets are opaque and therefore unsafe.
|
|
351
386
|
func shellOutputRedirections(command string) (sanitized string, paths []string, opaque bool) {
|
|
@@ -471,44 +506,13 @@ func hookA1Guard(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) i
|
|
|
471
506
|
}
|
|
472
507
|
|
|
473
508
|
in := h.ParseGuardInput(bytes.NewReader(data))
|
|
474
|
-
if !isEditTool(in.ToolName) {
|
|
475
|
-
return exitOK
|
|
476
|
-
}
|
|
477
509
|
// The wright may edit. Claude sends agent_id; Codex sends agent_type.
|
|
478
510
|
if in.AgentID != "" || in.AgentType != "" {
|
|
479
511
|
return exitOK
|
|
480
512
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if in.ToolName == "apply_patch" {
|
|
485
|
-
sourcePatch := false
|
|
486
|
-
for _, p := range patchPaths(in.Command) {
|
|
487
|
-
abs := p
|
|
488
|
-
if !filepath.IsAbs(abs) {
|
|
489
|
-
abs = filepath.Join(projectDir, abs)
|
|
490
|
-
}
|
|
491
|
-
if !underDevrites(abs, root) {
|
|
492
|
-
sourcePatch = true
|
|
493
|
-
break
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if !sourcePatch {
|
|
497
|
-
return exitOK
|
|
498
|
-
}
|
|
499
|
-
target = "apply_patch"
|
|
500
|
-
} else {
|
|
501
|
-
if in.FilePath == "" {
|
|
502
|
-
return exitOK
|
|
503
|
-
}
|
|
504
|
-
abs := in.FilePath
|
|
505
|
-
if !filepath.IsAbs(abs) {
|
|
506
|
-
abs = filepath.Join(projectDir, abs)
|
|
507
|
-
}
|
|
508
|
-
if underDevrites(abs, root) {
|
|
509
|
-
return exitOK // orchestrator editing its own .devrites bookkeeping
|
|
510
|
-
}
|
|
511
|
-
target = abs
|
|
513
|
+
handled, violation, target := classifyRootBuildWindowOperation(root, data, in)
|
|
514
|
+
if !handled || !violation {
|
|
515
|
+
return exitOK
|
|
512
516
|
}
|
|
513
517
|
|
|
514
518
|
mode := os.Getenv("DEVRITES_A1_HOOK")
|
|
@@ -842,7 +846,11 @@ func uniqueStrings(values []string) []string {
|
|
|
842
846
|
}
|
|
843
847
|
|
|
844
848
|
func wrightShellWritePaths(command string) ([]string, bool) {
|
|
845
|
-
|
|
849
|
+
patches := patchPaths(command)
|
|
850
|
+
var ok bool
|
|
851
|
+
command, ok = stripShellHeredocBodies(command)
|
|
852
|
+
if !ok || strings.TrimSpace(command) == "" || strings.Contains(command, "$(") ||
|
|
853
|
+
strings.ContainsRune(command, '`') || wrightForbiddenShellRe.MatchString(command) {
|
|
846
854
|
return nil, true
|
|
847
855
|
}
|
|
848
856
|
sanitized, paths, opaque := shellOutputRedirections(command)
|
|
@@ -850,7 +858,6 @@ func wrightShellWritePaths(command string) ([]string, bool) {
|
|
|
850
858
|
return nil, true
|
|
851
859
|
}
|
|
852
860
|
|
|
853
|
-
patches := patchPaths(command)
|
|
854
861
|
if strings.Contains(command, "apply_patch") {
|
|
855
862
|
if len(patches) == 0 {
|
|
856
863
|
return nil, true
|
|
@@ -896,6 +903,15 @@ func wrightShellWritePaths(command string) ([]string, bool) {
|
|
|
896
903
|
found = nonFlagArgs(args)
|
|
897
904
|
case "rm", "unlink", "touch", "mkdir", "rmdir", "truncate":
|
|
898
905
|
found = nonFlagArgs(args)
|
|
906
|
+
case "cp":
|
|
907
|
+
if hasAnyArg(args, "-t", "--target-directory") || hasArgPrefix(args, "--target-directory=") {
|
|
908
|
+
return nil, true
|
|
909
|
+
}
|
|
910
|
+
operands := nonFlagArgs(args)
|
|
911
|
+
if len(operands) < 2 {
|
|
912
|
+
return nil, true
|
|
913
|
+
}
|
|
914
|
+
found = operands[len(operands)-1:]
|
|
899
915
|
default:
|
|
900
916
|
return nil, true
|
|
901
917
|
}
|
|
@@ -1014,36 +1030,50 @@ func guardRootBuildWindow(h harness.Harness, data []byte, in harness.GuardInput,
|
|
|
1014
1030
|
return exitOK, false
|
|
1015
1031
|
}
|
|
1016
1032
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1033
|
+
handled, violation, _ := classifyRootBuildWindowOperation(root, data, in)
|
|
1034
|
+
if violation {
|
|
1035
|
+
return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
|
|
1019
1036
|
}
|
|
1037
|
+
return exitOK, handled
|
|
1038
|
+
}
|
|
1020
1039
|
|
|
1021
|
-
|
|
1040
|
+
func classifyRootBuildWindowOperation(root string, data []byte, in harness.GuardInput) (handled, violation bool, target string) {
|
|
1041
|
+
if isAgentDispatchTool(in.ToolName) {
|
|
1042
|
+
return true, false, ""
|
|
1043
|
+
}
|
|
1022
1044
|
if isOpaqueExecutionTool(in.ToolName) {
|
|
1023
|
-
return
|
|
1045
|
+
return true, true, in.ToolName
|
|
1024
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
projectDir := filepath.Dir(root)
|
|
1025
1049
|
targets, suspicious := rootMutationTargets(data, in)
|
|
1026
1050
|
for _, target := range targets {
|
|
1051
|
+
absoluteTarget := filepath.IsAbs(target)
|
|
1027
1052
|
abs := target
|
|
1028
|
-
if !
|
|
1053
|
+
if !absoluteTarget {
|
|
1029
1054
|
abs = filepath.Join(projectDir, abs)
|
|
1030
1055
|
}
|
|
1031
1056
|
abs = filepath.Clean(abs)
|
|
1032
1057
|
resolved, err := resolveRootMutationTarget(abs)
|
|
1033
1058
|
if err != nil {
|
|
1034
|
-
return
|
|
1059
|
+
return true, true, target
|
|
1035
1060
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1061
|
+
insideProject := pathWithin(resolved, projectDir)
|
|
1062
|
+
if (insideProject && !underDevrites(resolved, root)) ||
|
|
1063
|
+
(!insideProject && !absoluteTarget) {
|
|
1064
|
+
return true, true, target
|
|
1038
1065
|
}
|
|
1039
1066
|
}
|
|
1040
1067
|
if suspicious {
|
|
1041
|
-
return
|
|
1068
|
+
return true, true, in.ToolName
|
|
1042
1069
|
}
|
|
1043
1070
|
if len(targets) > 0 {
|
|
1044
|
-
return
|
|
1071
|
+
return true, false, strings.Join(targets, ",")
|
|
1072
|
+
}
|
|
1073
|
+
if isShellTool(in.ToolName) && safeReadonlyShellCommand(in.Command) {
|
|
1074
|
+
return true, false, ""
|
|
1045
1075
|
}
|
|
1046
|
-
return
|
|
1076
|
+
return false, false, ""
|
|
1047
1077
|
}
|
|
1048
1078
|
|
|
1049
1079
|
func resolveRootMutationTarget(filename string) (string, error) {
|
|
@@ -1071,7 +1101,8 @@ func rootMutationTargets(data []byte, in harness.GuardInput) ([]string, bool) {
|
|
|
1071
1101
|
if len(targets) > 0 {
|
|
1072
1102
|
return targets, false
|
|
1073
1103
|
}
|
|
1074
|
-
|
|
1104
|
+
_, completeHeredoc := stripShellHeredocBodies(in.Command)
|
|
1105
|
+
return nil, opaque && (!completeHeredoc || reviewerMutateRe.MatchString(in.Command) || opaqueInterpreterRe.MatchString(in.Command))
|
|
1075
1106
|
default:
|
|
1076
1107
|
return nil, false
|
|
1077
1108
|
}
|
|
@@ -214,6 +214,7 @@ func (h Harness) ParseGuardInput(r io.Reader) GuardInput {
|
|
|
214
214
|
ToolName string `json:"tool_name"`
|
|
215
215
|
ToolInput struct {
|
|
216
216
|
Command string `json:"command"`
|
|
217
|
+
Cmd string `json:"cmd"`
|
|
217
218
|
FilePath string `json:"file_path"`
|
|
218
219
|
Path string `json:"path"`
|
|
219
220
|
} `json:"tool_input"`
|
|
@@ -237,7 +238,7 @@ func (h Harness) ParseGuardInput(r io.Reader) GuardInput {
|
|
|
237
238
|
}
|
|
238
239
|
return GuardInput{
|
|
239
240
|
ToolName: raw.ToolName,
|
|
240
|
-
Command: raw.ToolInput.Command,
|
|
241
|
+
Command: firstNonEmpty(raw.ToolInput.Command, raw.ToolInput.Cmd),
|
|
241
242
|
FilePath: firstNonEmpty(raw.ToolInput.FilePath, raw.ToolInput.Path),
|
|
242
243
|
SessionID: raw.SessionID,
|
|
243
244
|
AgentType: raw.AgentType,
|
|
@@ -113,6 +113,10 @@ func TestHarnessParsersFailOpenAndFallbacks(t *testing.T) {
|
|
|
113
113
|
if guard.ToolName != "Edit" || guard.FilePath != "src/app.go" || guard.AgentID != "a1" || guard.ToolResponse != `{"ok":true}` {
|
|
114
114
|
t.Fatalf("ParseGuardInput = %#v", guard)
|
|
115
115
|
}
|
|
116
|
+
guard = Codex.ParseGuardInput(strings.NewReader(`{"tool_name":"functions.exec_command","tool_input":{"cmd":"sha256sum /tmp/agent-packet.yaml"}}`))
|
|
117
|
+
if guard.ToolName != "functions.exec_command" || guard.Command != "sha256sum /tmp/agent-packet.yaml" {
|
|
118
|
+
t.Fatalf("ParseGuardInput Codex cmd = %#v", guard)
|
|
119
|
+
}
|
|
116
120
|
|
|
117
121
|
if got := Codex.SubagentAgentType(strings.NewReader(`{"agentType":"executor"}`)); got != "executor" {
|
|
118
122
|
t.Fatalf("SubagentAgentType fallback = %q, want executor", got)
|
|
@@ -583,6 +583,13 @@ func TestHookWrightScopeRequiresSpawnedCodexWorkerForSourceWrites(t *testing.T)
|
|
|
583
583
|
runGuard(`{"tool_name":"spawn_agent","tool_input":{"agent_type":"worker"}}`, false)
|
|
584
584
|
runGuard(`{"tool_name":"spawn_agent","tool_input":{"agent_type":"explorer"}}`, false)
|
|
585
585
|
runGuard(`{"tool_name":"Write","tool_input":{"file_path":".devrites/features/auth-tokens/state.md"}}`, false)
|
|
586
|
+
scratch := t.TempDir()
|
|
587
|
+
packet := filepath.Join(scratch, "agent-packet.yaml")
|
|
588
|
+
runGuard(hookPayload(t, map[string]any{
|
|
589
|
+
"tool_name": "Write",
|
|
590
|
+
"tool_input": map[string]any{"file_path": packet},
|
|
591
|
+
}), false)
|
|
592
|
+
runGuard(`{"tool_name":"Write","tool_input":{"file_path":"../outside.txt"}}`, true)
|
|
586
593
|
runGuard(`{"tool_name":"Write","tool_input":{"file_path":"src/app.go"}}`, true)
|
|
587
594
|
runGuard(`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, true)
|
|
588
595
|
runGuard(`{"tool_name":"js","tool_input":{"code":"writeFile()"}}`, true)
|
|
@@ -644,6 +651,59 @@ func TestHookA1GuardIgnoresLegacyInlineMarker(t *testing.T) {
|
|
|
644
651
|
}
|
|
645
652
|
}
|
|
646
653
|
|
|
654
|
+
func TestHookA1GuardAllowsExternalDispatchScratchOnly(t *testing.T) {
|
|
655
|
+
root := newWorkspace(t)
|
|
656
|
+
writeActive(t, root, "auth-tokens")
|
|
657
|
+
workspace := filepath.Join(root, "features", "auth-tokens")
|
|
658
|
+
if err := os.WriteFile(filepath.Join(workspace, ".reconcile-base"), []byte("snapshot\n"), 0o600); err != nil {
|
|
659
|
+
t.Fatal(err)
|
|
660
|
+
}
|
|
661
|
+
scratch := t.TempDir()
|
|
662
|
+
packet := filepath.Join(scratch, "agent-packet.yaml")
|
|
663
|
+
env := []string{"DEVRITES_A1_HOOK=enforce"}
|
|
664
|
+
patch := func(tool, path string, freeform bool) string {
|
|
665
|
+
body := "*** Begin Patch\n*** Add File: " + path + "\n+x\n*** End Patch\n"
|
|
666
|
+
if freeform {
|
|
667
|
+
return hookPayload(t, map[string]any{"tool_name": tool, "tool_input": body})
|
|
668
|
+
}
|
|
669
|
+
return hookPayload(t, map[string]any{"tool_name": tool, "tool_input": map[string]any{"patch": body}})
|
|
670
|
+
}
|
|
671
|
+
shell := func(command string) string {
|
|
672
|
+
return hookPayload(t, map[string]any{
|
|
673
|
+
"tool_name": "functions.exec_command",
|
|
674
|
+
"tool_input": map[string]any{"cmd": command},
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
runGuard := func(input string, deny bool) {
|
|
679
|
+
t.Helper()
|
|
680
|
+
out, errOut, code := runDevritesIO(t, root, input, env,
|
|
681
|
+
"hook", "a1-guard", "--harness=codex")
|
|
682
|
+
if code != 0 {
|
|
683
|
+
t.Fatalf("exit=%d out=%q stderr=%q", code, out, errOut)
|
|
684
|
+
}
|
|
685
|
+
if deny {
|
|
686
|
+
if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
|
|
687
|
+
t.Fatalf("expected deny, got %q", out)
|
|
688
|
+
}
|
|
689
|
+
} else if strings.TrimSpace(out) != "" {
|
|
690
|
+
t.Fatalf("expected allow, got %q", out)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
runGuard(patch("apply_patch", packet, false), false)
|
|
695
|
+
runGuard(patch("functions.apply_patch", packet, true), false)
|
|
696
|
+
runGuard(shell("cat <<'EOF' > "+packet+"\npacket_version: agent-packet/v1\nEOF"), false)
|
|
697
|
+
runGuard(shell("sha256sum "+packet), false)
|
|
698
|
+
runGuard(shell("cp "+packet+" "+filepath.Join(scratch, "agent-packet.copy.yaml")), false)
|
|
699
|
+
runGuard(patch("apply_patch", "src/app.go", false), true)
|
|
700
|
+
runGuard(patch("functions.apply_patch", "src/app.go", true), true)
|
|
701
|
+
runGuard(shell("cat <<'EOF' > src/app.go\npackage src\nEOF"), true)
|
|
702
|
+
runGuard(shell("cat <<'EOF' > "+packet+"\nunterminated"), true)
|
|
703
|
+
runGuard(shell("cat <<EOF > "+packet+"\n$(cp "+packet+" src/app.go)\nEOF"), true)
|
|
704
|
+
runGuard(shell("cp "+packet+" src/app.go"), true)
|
|
705
|
+
}
|
|
706
|
+
|
|
647
707
|
func TestHookWrightScopeFailsClosedWhenWorkspaceResolutionFails(t *testing.T) {
|
|
648
708
|
root := newWorkspace(t)
|
|
649
709
|
if err := os.Remove(filepath.Join(root, "ACTIVE")); err != nil && !os.IsNotExist(err) {
|
|
@@ -1695,7 +1755,7 @@ func TestCodexAgentDispatchRejectsIncompleteDurableV2(t *testing.T) {
|
|
|
1695
1755
|
}
|
|
1696
1756
|
}
|
|
1697
1757
|
|
|
1698
|
-
func
|
|
1758
|
+
func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T) {
|
|
1699
1759
|
root := newWorkspace(t)
|
|
1700
1760
|
writeActive(t, root, "auth-tokens")
|
|
1701
1761
|
workspace := filepath.Join(root, "features", "auth-tokens")
|
|
@@ -1714,14 +1774,26 @@ func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
|
|
|
1714
1774
|
"prompt": "$rite-build",
|
|
1715
1775
|
}), nil, "hook", "agent-dispatch", "--harness=codex")
|
|
1716
1776
|
|
|
1717
|
-
|
|
1777
|
+
check := hookPayload(t, map[string]any{
|
|
1778
|
+
"hook_event_name": "PreToolUse",
|
|
1779
|
+
"session_id": sessionID,
|
|
1780
|
+
"turn_id": turnID,
|
|
1781
|
+
"tool_name": "Bash",
|
|
1782
|
+
"tool_input": map[string]any{"command": "rtk devrites-engine reconcile check"},
|
|
1783
|
+
})
|
|
1784
|
+
if out, stderr, code := runDevritesIO(t, root, check, nil,
|
|
1785
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1786
|
+
t.Fatalf("ordinary reconcile check exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
closeWindow := hookPayload(t, map[string]any{
|
|
1718
1790
|
"hook_event_name": "PreToolUse",
|
|
1719
1791
|
"session_id": sessionID,
|
|
1720
1792
|
"turn_id": turnID,
|
|
1721
1793
|
"tool_name": "Bash",
|
|
1722
1794
|
"tool_input": map[string]any{"command": "rtk devrites-engine reconcile close"},
|
|
1723
1795
|
})
|
|
1724
|
-
out, _, _ := runDevritesIO(t, root,
|
|
1796
|
+
out, _, _ := runDevritesIO(t, root, closeWindow, nil,
|
|
1725
1797
|
"hook", "agent-dispatch", "--harness=codex")
|
|
1726
1798
|
if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
|
|
1727
1799
|
!strings.Contains(reason, "confirmed") {
|
|
@@ -1774,7 +1846,7 @@ func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
|
|
|
1774
1846
|
}
|
|
1775
1847
|
}
|
|
1776
1848
|
|
|
1777
|
-
if out, stderr, code := runDevritesIO(t, root,
|
|
1849
|
+
if out, stderr, code := runDevritesIO(t, root, closeWindow, nil,
|
|
1778
1850
|
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1779
1851
|
t.Fatalf("completed wright reconcile exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1780
1852
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.10",
|
|
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",
|