devrites 3.2.9 → 3.2.11
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 +12 -5
- package/engine/hooks.go +14 -1
- package/engine/hooks_agent_dispatch.go +2 -0
- package/engine/hooks_workspace.go +74 -46
- package/engine/internal/harness/harness.go +2 -1
- package/engine/internal/harness/harness_test.go +4 -0
- package/engine/internal/lib/reconcile.go +68 -11
- package/engine/internal/lib/reconcile_test.go +51 -1
- package/engine/tests/hook_test.go +109 -6
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +6 -4
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +6 -4
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +6 -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.11](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.10...v3.2.11) (2026-07-26)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **devrites:** bind canonical state at wright start ([b58a531](https://github.com/ViktorsBaikers/DevRites/commit/b58a531749b94f734b7abf7469656ad543a0cfa0))
|
|
10
|
+
|
|
11
|
+
## [3.2.10](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.9...v3.2.10) (2026-07-26)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **devrites:** allow external dispatch scratch ([fdaac3f](https://github.com/ViktorsBaikers/DevRites/commit/fdaac3f9aaf19af897c6d3d70ce01fc046a60d3e))
|
|
16
|
+
|
|
5
17
|
## [3.2.9](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.8...v3.2.9) (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.11`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.11): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
package/docs/orchestration.md
CHANGED
|
@@ -62,11 +62,16 @@ Each dispatch follows the retained-baseline sequence:
|
|
|
62
62
|
|
|
63
63
|
1. `reconcile snapshot` captures the original dirty-tree baseline, private Git
|
|
64
64
|
objects, exact allowlist, and canonical-state fingerprint.
|
|
65
|
-
2. The
|
|
65
|
+
2. The confirmed wright start captures a second canonical-state fingerprint.
|
|
66
|
+
This preserves the original source baseline while separating root-owned
|
|
67
|
+
recovery records that predate the writer from canonical mutations made while
|
|
68
|
+
the writer is active.
|
|
69
|
+
3. The sole wright returns code/tests; the root validates its typed identity and
|
|
66
70
|
exact changed-file set.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
5.
|
|
71
|
+
4. `reconcile check` rejects anything outside the root allowlist and any
|
|
72
|
+
canonical-state change after the wright start.
|
|
73
|
+
5. `test-integrity` runs against the retained baseline.
|
|
74
|
+
6. After proof and decision checks pass, `reconcile close` retires the private
|
|
70
75
|
window; only then does the root write canonical records.
|
|
71
76
|
|
|
72
77
|
An ordinary `reconcile check` may revalidate an existing retained window
|
|
@@ -75,7 +80,9 @@ window. `reconcile close` still requires the confirmed, awaited wright result
|
|
|
75
80
|
bound to the active snapshot. While the window is armed, the root guard permits
|
|
76
81
|
inspectable writes only under `.devrites` or at an absolute scratch path outside
|
|
77
82
|
the project; source-tree writes, relative escapes, and opaque execution remain
|
|
78
|
-
blocked.
|
|
83
|
+
blocked. The standalone A1 hook and wright-scope hook share this classification;
|
|
84
|
+
Codex `cmd` and Claude `command` payloads may create a literal external packet
|
|
85
|
+
with a patch, heredoc, or copy and may hash it without gaining source authority.
|
|
79
86
|
|
|
80
87
|
The canonical-state fingerprint excludes root-owned operational records:
|
|
81
88
|
`timeline.jsonl`, feature `events.jsonl`, a valid `recovery-attempts.jsonl`,
|
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")
|
|
@@ -546,6 +546,7 @@ func hookSubagentOrient(h harness.Harness, stdin io.Reader, stdout, stderr io.Wr
|
|
|
546
546
|
}
|
|
547
547
|
agentType := h.SubagentAgentType(bytes.NewReader(data))
|
|
548
548
|
context := strings.ReplaceAll(subagentOrientContext, "\r\n", "\n")
|
|
549
|
+
confirmedRole := ""
|
|
549
550
|
if h == harness.Codex {
|
|
550
551
|
in, parseErr := parseAgentDispatchHookInput(bytes.NewReader(data))
|
|
551
552
|
root, ok := agentDispatchRoot()
|
|
@@ -559,6 +560,7 @@ func hookSubagentOrient(h harness.Harness, stdin io.Reader, stdout, stderr io.Wr
|
|
|
559
560
|
if bindErr != nil {
|
|
560
561
|
context += "\n\n- **Dispatch binding failed.** Return blocked without doing specialist work; the parent must repair the agent-start receipt."
|
|
561
562
|
} else if bound {
|
|
563
|
+
confirmedRole = role
|
|
562
564
|
if in.AgentType == role {
|
|
563
565
|
context += fmt.Sprintf(
|
|
564
566
|
"\n\n- **Confirmed role.** Your named Codex profile is `%s`; follow its loaded `developer_instructions` and execute only the unchanged packet from the parent.",
|
|
@@ -578,6 +580,17 @@ func hookSubagentOrient(h harness.Harness, stdin io.Reader, stdout, stderr io.Wr
|
|
|
578
580
|
}
|
|
579
581
|
} else if !strings.HasPrefix(agentType, "devrites-") {
|
|
580
582
|
return exitOK // Stay silent without a DevRites agent identity.
|
|
583
|
+
} else {
|
|
584
|
+
confirmedRole = agentType
|
|
585
|
+
}
|
|
586
|
+
if confirmedRole == "devrites-slice-wright" {
|
|
587
|
+
root, slug, _, ok := resolveWorkspace()
|
|
588
|
+
if !ok {
|
|
589
|
+
context += "\n\n- **Wright boundary unavailable.** Return blocked without writing; the parent must restore the active reconcile window."
|
|
590
|
+
} else if err := lib.CaptureReconcileWrightBoundary(root, slug); err != nil {
|
|
591
|
+
debugf(stderr, "subagent-orient wright boundary: %v", err)
|
|
592
|
+
context += "\n\n- **Wright boundary unavailable.** Return blocked without writing; the parent must repair the retained reconcile window."
|
|
593
|
+
}
|
|
581
594
|
}
|
|
582
595
|
out, err := h.SubagentStartContext(context)
|
|
583
596
|
if err != nil {
|
|
@@ -400,6 +400,8 @@ func currentReconcileWindowID() string {
|
|
|
400
400
|
if !ok {
|
|
401
401
|
return ""
|
|
402
402
|
}
|
|
403
|
+
// The wright-start canonical fingerprint is captured after the pending
|
|
404
|
+
// receipt. It narrows the delta but does not change source authorization.
|
|
403
405
|
names := []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"}
|
|
404
406
|
h := sha256.New()
|
|
405
407
|
for _, name := range names {
|
|
@@ -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,14 +1030,22 @@ 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 {
|
|
1027
1051
|
absoluteTarget := filepath.IsAbs(target)
|
|
@@ -1032,21 +1056,24 @@ func guardRootBuildWindow(h harness.Harness, data []byte, in harness.GuardInput,
|
|
|
1032
1056
|
abs = filepath.Clean(abs)
|
|
1033
1057
|
resolved, err := resolveRootMutationTarget(abs)
|
|
1034
1058
|
if err != nil {
|
|
1035
|
-
return
|
|
1059
|
+
return true, true, target
|
|
1036
1060
|
}
|
|
1037
1061
|
insideProject := pathWithin(resolved, projectDir)
|
|
1038
1062
|
if (insideProject && !underDevrites(resolved, root)) ||
|
|
1039
1063
|
(!insideProject && !absoluteTarget) {
|
|
1040
|
-
return
|
|
1064
|
+
return true, true, target
|
|
1041
1065
|
}
|
|
1042
1066
|
}
|
|
1043
1067
|
if suspicious {
|
|
1044
|
-
return
|
|
1068
|
+
return true, true, in.ToolName
|
|
1045
1069
|
}
|
|
1046
1070
|
if len(targets) > 0 {
|
|
1047
|
-
return
|
|
1071
|
+
return true, false, strings.Join(targets, ",")
|
|
1072
|
+
}
|
|
1073
|
+
if isShellTool(in.ToolName) && safeReadonlyShellCommand(in.Command) {
|
|
1074
|
+
return true, false, ""
|
|
1048
1075
|
}
|
|
1049
|
-
return
|
|
1076
|
+
return false, false, ""
|
|
1050
1077
|
}
|
|
1051
1078
|
|
|
1052
1079
|
func resolveRootMutationTarget(filename string) (string, error) {
|
|
@@ -1074,7 +1101,8 @@ func rootMutationTargets(data []byte, in harness.GuardInput) ([]string, bool) {
|
|
|
1074
1101
|
if len(targets) > 0 {
|
|
1075
1102
|
return targets, false
|
|
1076
1103
|
}
|
|
1077
|
-
|
|
1104
|
+
_, completeHeredoc := stripShellHeredocBodies(in.Command)
|
|
1105
|
+
return nil, opaque && (!completeHeredoc || reviewerMutateRe.MatchString(in.Command) || opaqueInterpreterRe.MatchString(in.Command))
|
|
1078
1106
|
default:
|
|
1079
1107
|
return nil, false
|
|
1080
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)
|
|
@@ -23,6 +23,7 @@ const (
|
|
|
23
23
|
reconcileCheckedName = ".reconcile-checked"
|
|
24
24
|
reconcileDevritesName = ".reconcile-devrites"
|
|
25
25
|
reconcileObjectsName = ".reconcile-objects"
|
|
26
|
+
reconcileWrightStateName = ".reconcile-wright-devrites"
|
|
26
27
|
defaultWrightAllowlistName = ".wright-allowlist"
|
|
27
28
|
wrightAllowlistFileEnv = "DEVRITES_WRIGHT_ALLOWLIST_FILE"
|
|
28
29
|
reconcileDevritesPathPrefix = ".devrites/"
|
|
@@ -30,9 +31,11 @@ const (
|
|
|
30
31
|
|
|
31
32
|
// Reconcile enforces the source-write boundary around one dispatched
|
|
32
33
|
// slice-wright. The first `snapshot` captures the dirty worktree, private object
|
|
33
|
-
// database, exact orchestrator-authored allowlist, and .devrites state.
|
|
34
|
-
//
|
|
35
|
-
//
|
|
34
|
+
// database, exact orchestrator-authored allowlist, and .devrites state. A
|
|
35
|
+
// confirmed wright start captures the current canonical state separately, so
|
|
36
|
+
// retained-window recovery records are not attributed to the writer. After a
|
|
37
|
+
// clean check, another snapshot re-arms only the dispatch state for a retry
|
|
38
|
+
// while retaining the original source baseline.
|
|
36
39
|
// `check` compares the captured state with the current state and retains the
|
|
37
40
|
// immutable baseline for the later test-integrity gate.
|
|
38
41
|
// `close` explicitly ends the window and removes its private artifacts.
|
|
@@ -62,10 +65,11 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
62
65
|
devritesSnapshot := filepath.Join(d, reconcileDevritesName)
|
|
63
66
|
objects := filepath.Join(d, reconcileObjectsName)
|
|
64
67
|
checked := filepath.Join(d, reconcileCheckedName)
|
|
68
|
+
wrightState := filepath.Join(d, reconcileWrightStateName)
|
|
65
69
|
|
|
66
70
|
closeWindow := func() error {
|
|
67
71
|
var failures []string
|
|
68
|
-
for _, privateFile := range []string{base, capturedAllowlist, devritesSnapshot, checked} {
|
|
72
|
+
for _, privateFile := range []string{base, capturedAllowlist, devritesSnapshot, checked, wrightState} {
|
|
69
73
|
if err := os.Remove(privateFile); err != nil && !os.IsNotExist(err) {
|
|
70
74
|
failures = append(failures, fmt.Sprintf("%s: %v", filepath.Base(privateFile), err))
|
|
71
75
|
}
|
|
@@ -131,7 +135,7 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
131
135
|
fmt.Fprintf(stderr, "reconcile: cannot refresh wright allowlist: %v\n", err)
|
|
132
136
|
return 6
|
|
133
137
|
}
|
|
134
|
-
state, err := captureReconcileDevritesState(root, devritesSnapshot, activeObjects, checked)
|
|
138
|
+
state, err := captureReconcileDevritesState(root, devritesSnapshot, activeObjects, checked, wrightState)
|
|
135
139
|
if err != nil {
|
|
136
140
|
fmt.Fprintf(stderr, "reconcile: cannot refresh .devrites snapshot: %v\n", err)
|
|
137
141
|
return 6
|
|
@@ -140,9 +144,11 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
140
144
|
fmt.Fprintf(stderr, "reconcile: cannot write refreshed .devrites snapshot: %v\n", err)
|
|
141
145
|
return 6
|
|
142
146
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
for _, privateFile := range []string{checked, wrightState} {
|
|
148
|
+
if err := os.Remove(privateFile); err != nil && !os.IsNotExist(err) {
|
|
149
|
+
fmt.Fprintf(stderr, "reconcile: cannot arm refreshed slice window: %v\n", err)
|
|
150
|
+
return 6
|
|
151
|
+
}
|
|
146
152
|
}
|
|
147
153
|
fmt.Fprintf(stdout, "reconcile: dispatch snapshot refreshed for %s; original slice baseline retained.\n", slug)
|
|
148
154
|
return 0
|
|
@@ -180,7 +186,7 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
180
186
|
fmt.Fprintf(stderr, "reconcile: cannot capture wright allowlist: %v\n", err)
|
|
181
187
|
return 6
|
|
182
188
|
}
|
|
183
|
-
state, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked)
|
|
189
|
+
state, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked, wrightState)
|
|
184
190
|
if err != nil {
|
|
185
191
|
_ = closeWindow()
|
|
186
192
|
fmt.Fprintf(stderr, "reconcile: cannot snapshot .devrites: %v\n", err)
|
|
@@ -210,13 +216,20 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
210
216
|
fmt.Fprintf(stderr, "reconcile: invalid captured allowlist: %v\n", err)
|
|
211
217
|
return 6
|
|
212
218
|
}
|
|
213
|
-
|
|
219
|
+
beforeDevritesPath := devritesSnapshot
|
|
220
|
+
if _, err := os.Lstat(wrightState); err == nil {
|
|
221
|
+
beforeDevritesPath = wrightState
|
|
222
|
+
} else if !os.IsNotExist(err) {
|
|
223
|
+
fmt.Fprintf(stderr, "reconcile: invalid wright-start .devrites snapshot: %v\n", err)
|
|
224
|
+
return 6
|
|
225
|
+
}
|
|
226
|
+
beforeDevrites, err := readDevritesState(beforeDevritesPath)
|
|
214
227
|
if err != nil {
|
|
215
228
|
fmt.Fprintf(stderr, "reconcile: invalid .devrites snapshot: %v\n", err)
|
|
216
229
|
return 6
|
|
217
230
|
}
|
|
218
231
|
beforeDevrites = withoutRootOwnedOperationalState(beforeDevrites)
|
|
219
|
-
afterDevrites, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked)
|
|
232
|
+
afterDevrites, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked, wrightState)
|
|
220
233
|
if err != nil {
|
|
221
234
|
fmt.Fprintf(stderr, "reconcile: cannot compare .devrites: %v\n", err)
|
|
222
235
|
return 6
|
|
@@ -274,6 +287,50 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
274
287
|
}
|
|
275
288
|
}
|
|
276
289
|
|
|
290
|
+
// CaptureReconcileWrightBoundary records canonical state at the confirmed
|
|
291
|
+
// wright start. The original source baseline remains untouched; reconciliation
|
|
292
|
+
// can therefore distinguish root recovery records that predate the writer from
|
|
293
|
+
// canonical mutations made while the writer is active.
|
|
294
|
+
func CaptureReconcileWrightBoundary(root, slug string) error {
|
|
295
|
+
d := featureDir(root, slug)
|
|
296
|
+
if slug == "" || !isDir(d) {
|
|
297
|
+
return fmt.Errorf("no active workspace")
|
|
298
|
+
}
|
|
299
|
+
base := filepath.Join(d, reconcileBaseName)
|
|
300
|
+
capturedAllowlist := filepath.Join(d, reconcileAllowlistName)
|
|
301
|
+
devritesSnapshot := filepath.Join(d, reconcileDevritesName)
|
|
302
|
+
objects := filepath.Join(d, reconcileObjectsName)
|
|
303
|
+
checked := filepath.Join(d, reconcileCheckedName)
|
|
304
|
+
wrightState := filepath.Join(d, reconcileWrightStateName)
|
|
305
|
+
|
|
306
|
+
for _, filename := range []string{base, capturedAllowlist} {
|
|
307
|
+
info, err := os.Lstat(filename)
|
|
308
|
+
if err != nil || !info.Mode().IsRegular() {
|
|
309
|
+
if err == nil {
|
|
310
|
+
err = fmt.Errorf("not a regular file")
|
|
311
|
+
}
|
|
312
|
+
return fmt.Errorf("invalid reconcile window %s: %w", filepath.Base(filename), err)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if !isDir(objects) {
|
|
316
|
+
return fmt.Errorf("invalid reconcile window %s: not a directory", filepath.Base(objects))
|
|
317
|
+
}
|
|
318
|
+
if _, err := readDevritesState(devritesSnapshot); err != nil {
|
|
319
|
+
return fmt.Errorf("invalid reconcile window %s: %w", filepath.Base(devritesSnapshot), err)
|
|
320
|
+
}
|
|
321
|
+
state, err := captureReconcileDevritesState(
|
|
322
|
+
root,
|
|
323
|
+
devritesSnapshot,
|
|
324
|
+
objects,
|
|
325
|
+
checked,
|
|
326
|
+
wrightState,
|
|
327
|
+
)
|
|
328
|
+
if err != nil {
|
|
329
|
+
return err
|
|
330
|
+
}
|
|
331
|
+
return writeDevritesState(wrightState, state)
|
|
332
|
+
}
|
|
333
|
+
|
|
277
334
|
func wrightAllowlistPath(gitRoot, workspace string) (string, error) {
|
|
278
335
|
configured := os.Getenv(wrightAllowlistFileEnv)
|
|
279
336
|
var filename string
|
|
@@ -361,6 +361,49 @@ func TestReconcileRejectsDevritesMutation(t *testing.T) {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
func TestReconcileUsesWrightStartAsCanonicalStateBoundary(t *testing.T) {
|
|
365
|
+
gitRoot := newGitRepo(t)
|
|
366
|
+
root := workspace(t, "feat")
|
|
367
|
+
dir := featureDir(root, "feat")
|
|
368
|
+
writeWrightAllowlist(t, root, "feat", "seed.go")
|
|
369
|
+
if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
|
|
370
|
+
t.Fatalf("snapshot = %d, want 0\n%s", code, out)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Root-owned canonical records may legitimately change while a retained
|
|
374
|
+
// source baseline survives technical recovery. The next wright start is the
|
|
375
|
+
// boundary that separates those changes from the writer's delta.
|
|
376
|
+
for _, name := range []string{
|
|
377
|
+
"action.log",
|
|
378
|
+
"browser-evidence.md",
|
|
379
|
+
"decisions.md",
|
|
380
|
+
"evidence.md",
|
|
381
|
+
"footprint.log",
|
|
382
|
+
"state.md",
|
|
383
|
+
"touched-files.md",
|
|
384
|
+
} {
|
|
385
|
+
writeFile(t, filepath.Join(dir, name), "Root-owned recovery record\n")
|
|
386
|
+
}
|
|
387
|
+
if err := CaptureReconcileWrightBoundary(root, "feat"); err != nil {
|
|
388
|
+
t.Fatalf("capture wright boundary: %v", err)
|
|
389
|
+
}
|
|
390
|
+
writeFile(t, filepath.Join(gitRoot, "seed.go"), "package main\n\nfunc repaired() {}\n")
|
|
391
|
+
|
|
392
|
+
if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
|
|
393
|
+
t.Fatalf("check = %d, want 0\n%s", code, out)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// A canonical mutation after the wright starts is still a violation.
|
|
397
|
+
writeFile(t, filepath.Join(dir, "state.md"), "Phase: prove\n")
|
|
398
|
+
code, out := runReconcile(t, root, "check", "feat")
|
|
399
|
+
if code != 5 {
|
|
400
|
+
t.Fatalf("check after writer-time canonical mutation = %d, want 5\n%s", code, out)
|
|
401
|
+
}
|
|
402
|
+
if !strings.Contains(out, ".devrites/work/feat/state.md") {
|
|
403
|
+
t.Fatalf("writer-time canonical mutation missing from output:\n%s", out)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
364
407
|
func TestReconcileAllowsRootOwnedOperationalFiles(t *testing.T) {
|
|
365
408
|
newGitRepo(t)
|
|
366
409
|
root := workspace(t, "feat")
|
|
@@ -613,6 +656,7 @@ func TestReconcileRefreshPreservesOriginalBaselineAcrossRetry(t *testing.T) {
|
|
|
613
656
|
// between attempts. Refresh must re-arm those boundaries without replacing
|
|
614
657
|
// the original pre-slice source baseline.
|
|
615
658
|
writeFile(t, filepath.Join(featureDir(root, "feat"), "recovery-attempts.json"), "{}\n")
|
|
659
|
+
writeFile(t, filepath.Join(featureDir(root, "feat"), reconcileWrightStateName), "stale\n")
|
|
616
660
|
writeWrightAllowlist(t, root, "feat", "seed.go", "retry.go")
|
|
617
661
|
if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
|
|
618
662
|
t.Fatalf("refresh snapshot = %d, want 0\n%s", code, out)
|
|
@@ -626,6 +670,9 @@ func TestReconcileRefreshPreservesOriginalBaselineAcrossRetry(t *testing.T) {
|
|
|
626
670
|
if !bytes.Equal(originalBase, refreshedBase) {
|
|
627
671
|
t.Fatalf("refresh replaced original baseline: before=%q after=%q", originalBase, refreshedBase)
|
|
628
672
|
}
|
|
673
|
+
if isFile(filepath.Join(featureDir(root, "feat"), reconcileWrightStateName)) {
|
|
674
|
+
t.Fatal("refresh retained the prior wright-start boundary")
|
|
675
|
+
}
|
|
629
676
|
|
|
630
677
|
writeFile(t, filepath.Join(gitRoot, "retry.go"), "package main\n")
|
|
631
678
|
if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
|
|
@@ -655,6 +702,7 @@ func TestReconcileCloseClearsPrivateWindowState(t *testing.T) {
|
|
|
655
702
|
newGitRepo(t)
|
|
656
703
|
root := workspace(t, "feat")
|
|
657
704
|
writeFile(t, filepath.Join(root, "work", "feat", reconcileBaseName), "stale\n")
|
|
705
|
+
writeFile(t, filepath.Join(root, "work", "feat", reconcileWrightStateName), "stale\n")
|
|
658
706
|
if err := os.MkdirAll(filepath.Join(root, "work", "feat", reconcileObjectsName), 0o755); err != nil {
|
|
659
707
|
t.Fatal(err)
|
|
660
708
|
}
|
|
@@ -666,7 +714,9 @@ func TestReconcileCloseClearsPrivateWindowState(t *testing.T) {
|
|
|
666
714
|
if !strings.Contains(out, "closed slice window") {
|
|
667
715
|
t.Errorf("expected close message:\n%s", out)
|
|
668
716
|
}
|
|
669
|
-
if isFile(filepath.Join(featureDir(root, "feat"), reconcileBaseName)) ||
|
|
717
|
+
if isFile(filepath.Join(featureDir(root, "feat"), reconcileBaseName)) ||
|
|
718
|
+
isFile(filepath.Join(featureDir(root, "feat"), reconcileWrightStateName)) ||
|
|
719
|
+
isDir(filepath.Join(featureDir(root, "feat"), reconcileObjectsName)) {
|
|
670
720
|
t.Fatal("close retained private baseline state")
|
|
671
721
|
}
|
|
672
722
|
}
|
|
@@ -583,9 +583,11 @@ 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")
|
|
586
588
|
runGuard(hookPayload(t, map[string]any{
|
|
587
589
|
"tool_name": "Write",
|
|
588
|
-
"tool_input": map[string]any{"file_path":
|
|
590
|
+
"tool_input": map[string]any{"file_path": packet},
|
|
589
591
|
}), false)
|
|
590
592
|
runGuard(`{"tool_name":"Write","tool_input":{"file_path":"../outside.txt"}}`, true)
|
|
591
593
|
runGuard(`{"tool_name":"Write","tool_input":{"file_path":"src/app.go"}}`, true)
|
|
@@ -649,6 +651,59 @@ func TestHookA1GuardIgnoresLegacyInlineMarker(t *testing.T) {
|
|
|
649
651
|
}
|
|
650
652
|
}
|
|
651
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
|
+
|
|
652
707
|
func TestHookWrightScopeFailsClosedWhenWorkspaceResolutionFails(t *testing.T) {
|
|
653
708
|
root := newWorkspace(t)
|
|
654
709
|
if err := os.Remove(filepath.Join(root, "ACTIVE")); err != nil && !os.IsNotExist(err) {
|
|
@@ -871,6 +926,44 @@ func TestHookSubagentOrientSilentForNonDevritesAgent(t *testing.T) {
|
|
|
871
926
|
}
|
|
872
927
|
}
|
|
873
928
|
|
|
929
|
+
func TestHookSubagentOrientCapturesWrightCanonicalBoundary(t *testing.T) {
|
|
930
|
+
root := newWorkspace(t)
|
|
931
|
+
writeActive(t, root, "auth-tokens")
|
|
932
|
+
workspace := filepath.Join(root, "features", "auth-tokens")
|
|
933
|
+
if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
|
|
934
|
+
t.Fatal(err)
|
|
935
|
+
}
|
|
936
|
+
for name, body := range map[string]string{
|
|
937
|
+
".reconcile-base": "base\n",
|
|
938
|
+
".reconcile-allowlist": "",
|
|
939
|
+
".reconcile-devrites": "[]\n",
|
|
940
|
+
"state.md": "Phase: build\n",
|
|
941
|
+
"browser-evidence.md": "Prior root evidence\n",
|
|
942
|
+
} {
|
|
943
|
+
if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
|
|
944
|
+
t.Fatal(err)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
in := `{"agent_type":"devrites-slice-wright"}`
|
|
949
|
+
out, errOut, code := runDevritesIO(t, root, in, nil, "hook", "subagent-orient", "--harness=claude")
|
|
950
|
+
if code != 0 {
|
|
951
|
+
t.Fatalf("exit = %d, want 0 (stderr: %s)", code, errOut)
|
|
952
|
+
}
|
|
953
|
+
if strings.Contains(out, "Wright boundary unavailable") {
|
|
954
|
+
t.Fatalf("wright start was blocked: %s", out)
|
|
955
|
+
}
|
|
956
|
+
raw, err := os.ReadFile(filepath.Join(workspace, ".reconcile-wright-devrites"))
|
|
957
|
+
if err != nil {
|
|
958
|
+
t.Fatalf("read wright boundary: %v", err)
|
|
959
|
+
}
|
|
960
|
+
for _, path := range []string{"features/auth-tokens/state.md", "features/auth-tokens/browser-evidence.md"} {
|
|
961
|
+
if !strings.Contains(string(raw), path) {
|
|
962
|
+
t.Errorf("wright boundary omitted %s: %s", path, raw)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
874
967
|
func hookPayload(t *testing.T, value map[string]any) string {
|
|
875
968
|
t.Helper()
|
|
876
969
|
raw, err := json.Marshal(value)
|
|
@@ -1704,11 +1797,18 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1704
1797
|
root := newWorkspace(t)
|
|
1705
1798
|
writeActive(t, root, "auth-tokens")
|
|
1706
1799
|
workspace := filepath.Join(root, "features", "auth-tokens")
|
|
1707
|
-
for
|
|
1708
|
-
|
|
1800
|
+
for name, body := range map[string]string{
|
|
1801
|
+
".reconcile-base": ".reconcile-base\n",
|
|
1802
|
+
".reconcile-allowlist": ".reconcile-allowlist\n",
|
|
1803
|
+
".reconcile-devrites": "[]\n",
|
|
1804
|
+
} {
|
|
1805
|
+
if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
|
|
1709
1806
|
t.Fatal(err)
|
|
1710
1807
|
}
|
|
1711
1808
|
}
|
|
1809
|
+
if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
|
|
1810
|
+
t.Fatal(err)
|
|
1811
|
+
}
|
|
1712
1812
|
sessionID, turnID := "session-wright", "turn-wright"
|
|
1713
1813
|
writeCodexAgentContract(t, root, "devrites-slice-wright")
|
|
1714
1814
|
writeCodexSkillContract(t, root, "rite-build", "devrites-slice-wright")
|
|
@@ -1753,7 +1853,7 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1753
1853
|
"tool_name": "spawn_agent",
|
|
1754
1854
|
"tool_use_id": "spawn-wright",
|
|
1755
1855
|
"tool_input": map[string]any{
|
|
1756
|
-
"agent_type": "
|
|
1856
|
+
"agent_type": "devrites-slice-wright",
|
|
1757
1857
|
"fork_turns": "none",
|
|
1758
1858
|
"message": "Read .codex/agents/devrites-slice-wright.toml.",
|
|
1759
1859
|
},
|
|
@@ -1763,14 +1863,14 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1763
1863
|
"session_id": sessionID,
|
|
1764
1864
|
"turn_id": "child-wright",
|
|
1765
1865
|
"agent_id": "agent-wright",
|
|
1766
|
-
"agent_type": "
|
|
1866
|
+
"agent_type": "devrites-slice-wright",
|
|
1767
1867
|
},
|
|
1768
1868
|
{
|
|
1769
1869
|
"hook_event_name": "SubagentStop",
|
|
1770
1870
|
"session_id": sessionID,
|
|
1771
1871
|
"turn_id": "child-wright",
|
|
1772
1872
|
"agent_id": "agent-wright",
|
|
1773
|
-
"agent_type": "
|
|
1873
|
+
"agent_type": "devrites-slice-wright",
|
|
1774
1874
|
"last_assistant_message": "agent-result/v1 complete",
|
|
1775
1875
|
},
|
|
1776
1876
|
{
|
|
@@ -1790,6 +1890,9 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1790
1890
|
t.Fatalf("%s exit=%d stderr=%s", hook, code, stderr)
|
|
1791
1891
|
}
|
|
1792
1892
|
}
|
|
1893
|
+
if _, err := os.Stat(filepath.Join(workspace, ".reconcile-wright-devrites")); err != nil {
|
|
1894
|
+
t.Fatalf("wright start did not capture canonical boundary: %v", err)
|
|
1895
|
+
}
|
|
1793
1896
|
|
|
1794
1897
|
if out, stderr, code := runDevritesIO(t, root, closeWindow, nil,
|
|
1795
1898
|
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
@@ -120,10 +120,12 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
|
|
|
120
120
|
|
|
121
121
|
## Host dispatch
|
|
122
122
|
|
|
123
|
-
Use named wright → enforced V1 generic worker from `standards/agents.md`.
|
|
124
|
-
|
|
125
|
-
`
|
|
126
|
-
|
|
123
|
+
Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
|
|
124
|
+
dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
|
|
125
|
+
`fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
|
|
126
|
+
result in the current window. The confirmed start fingerprints canonical state apart
|
|
127
|
+
from the retained source baseline: pre-start root recovery records are excluded, while
|
|
128
|
+
post-start canonical changes fail.
|
|
127
129
|
Any generic worker still needs the exact allowlist or an isolated checkout;
|
|
128
130
|
reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
|
|
129
131
|
for HITL. On Codex, `reconcile check` and `reconcile close` require the completed
|
|
@@ -120,10 +120,12 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
|
|
|
120
120
|
|
|
121
121
|
## Host dispatch
|
|
122
122
|
|
|
123
|
-
Use named wright → enforced V1 generic worker from `standards/agents.md`.
|
|
124
|
-
|
|
125
|
-
`
|
|
126
|
-
|
|
123
|
+
Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
|
|
124
|
+
dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
|
|
125
|
+
`fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
|
|
126
|
+
result in the current window. The confirmed start fingerprints canonical state apart
|
|
127
|
+
from the retained source baseline: pre-start root recovery records are excluded, while
|
|
128
|
+
post-start canonical changes fail.
|
|
127
129
|
Any generic worker still needs the exact allowlist or an isolated checkout;
|
|
128
130
|
reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
|
|
129
131
|
for HITL. On Codex, `reconcile check` and `reconcile close` require the completed
|
|
@@ -120,10 +120,12 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
|
|
|
120
120
|
|
|
121
121
|
## Host dispatch
|
|
122
122
|
|
|
123
|
-
Use named wright → enforced V1 generic worker from `standards/agents.md`.
|
|
124
|
-
|
|
125
|
-
`
|
|
126
|
-
|
|
123
|
+
Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
|
|
124
|
+
dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
|
|
125
|
+
`fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
|
|
126
|
+
result in the current window. The confirmed start fingerprints canonical state apart
|
|
127
|
+
from the retained source baseline: pre-start root recovery records are excluded, while
|
|
128
|
+
post-start canonical changes fail.
|
|
127
129
|
Any generic worker still needs the exact allowlist or an isolated checkout;
|
|
128
130
|
reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
|
|
129
131
|
for HITL. On Codex, `reconcile check` and `reconcile close` require the completed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.11",
|
|
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",
|