devrites 3.2.7 → 3.2.9
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 +18 -0
- package/engine/hooks_agent_dispatch.go +3 -3
- package/engine/hooks_workspace.go +5 -2
- package/engine/internal/lib/reconcile.go +63 -0
- package/engine/internal/lib/reconcile_test.go +152 -0
- package/engine/tests/hook_test.go +21 -4
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +1 -1
- 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.9](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.8...v3.2.9) (2026-07-26)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **devrites:** unblock retained reconcile checks ([c1093b0](https://github.com/ViktorsBaikers/DevRites/commit/c1093b00c3e32ba3cc22e0b9a2da548618ead621))
|
|
10
|
+
|
|
11
|
+
## [3.2.8](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.7...v3.2.8) (2026-07-26)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **devrites:** prevent reconcile self-invalidation ([161ed18](https://github.com/ViktorsBaikers/DevRites/commit/161ed187e600dfc92623b22db5cd7f0824408d1a))
|
|
16
|
+
|
|
5
17
|
## [3.2.7](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.6...v3.2.7) (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.9`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.9): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
package/docs/orchestration.md
CHANGED
|
@@ -69,6 +69,24 @@ 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.
|
|
79
|
+
|
|
80
|
+
The canonical-state fingerprint excludes root-owned operational records:
|
|
81
|
+
`timeline.jsonl`, feature `events.jsonl`, a valid `recovery-attempts.jsonl`,
|
|
82
|
+
the redwatch sentinel, automatic handoff snapshots, and hidden hook observation
|
|
83
|
+
logs. Those records cannot grant source authority or invalidate a writer result
|
|
84
|
+
merely because the engine or a hook wrote them. Unknown or canonical
|
|
85
|
+
`.devrites` paths remain fail-closed, and recovery ledgers must still parse
|
|
86
|
+
before reconciliation can pass. Gate-authoritative operational state such as
|
|
87
|
+
recovery attempts and `.red` remains enforced by its owning gate. Checks also
|
|
88
|
+
normalize these paths out of retained snapshots created by older engines.
|
|
89
|
+
|
|
72
90
|
On retry, the root may add only an accepted still-in-slice path, then runs
|
|
73
91
|
`reconcile snapshot` again. This refreshes the dispatch boundary while
|
|
74
92
|
preserving the original slice baseline. The same objective root cause has a
|
|
@@ -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 ||
|
|
@@ -1024,8 +1024,9 @@ func guardRootBuildWindow(h harness.Harness, data []byte, in harness.GuardInput,
|
|
|
1024
1024
|
}
|
|
1025
1025
|
targets, suspicious := rootMutationTargets(data, in)
|
|
1026
1026
|
for _, target := range targets {
|
|
1027
|
+
absoluteTarget := filepath.IsAbs(target)
|
|
1027
1028
|
abs := target
|
|
1028
|
-
if !
|
|
1029
|
+
if !absoluteTarget {
|
|
1029
1030
|
abs = filepath.Join(projectDir, abs)
|
|
1030
1031
|
}
|
|
1031
1032
|
abs = filepath.Clean(abs)
|
|
@@ -1033,7 +1034,9 @@ func guardRootBuildWindow(h harness.Harness, data []byte, in harness.GuardInput,
|
|
|
1033
1034
|
if err != nil {
|
|
1034
1035
|
return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
|
|
1035
1036
|
}
|
|
1036
|
-
|
|
1037
|
+
insideProject := pathWithin(resolved, projectDir)
|
|
1038
|
+
if (insideProject && !underDevrites(resolved, root)) ||
|
|
1039
|
+
(!insideProject && !absoluteTarget) {
|
|
1037
1040
|
return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
|
|
1038
1041
|
}
|
|
1039
1042
|
}
|
|
@@ -215,6 +215,7 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
215
215
|
fmt.Fprintf(stderr, "reconcile: invalid .devrites snapshot: %v\n", err)
|
|
216
216
|
return 6
|
|
217
217
|
}
|
|
218
|
+
beforeDevrites = withoutRootOwnedOperationalState(beforeDevrites)
|
|
218
219
|
afterDevrites, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked)
|
|
219
220
|
if err != nil {
|
|
220
221
|
fmt.Fprintf(stderr, "reconcile: cannot compare .devrites: %v\n", err)
|
|
@@ -465,6 +466,13 @@ func captureDevritesState(root string, ignoredPaths ...string) ([]devritesStateE
|
|
|
465
466
|
}
|
|
466
467
|
return nil
|
|
467
468
|
}
|
|
469
|
+
operational, err := reconcileRootOwnedOperationalFile(filename, rel, entry)
|
|
470
|
+
if err != nil {
|
|
471
|
+
return err
|
|
472
|
+
}
|
|
473
|
+
if operational {
|
|
474
|
+
return nil
|
|
475
|
+
}
|
|
468
476
|
|
|
469
477
|
fingerprint, err := devritesFingerprint(filename, entry)
|
|
470
478
|
if err != nil {
|
|
@@ -483,6 +491,61 @@ func captureDevritesState(root string, ignoredPaths ...string) ([]devritesStateE
|
|
|
483
491
|
return state, nil
|
|
484
492
|
}
|
|
485
493
|
|
|
494
|
+
// reconcileRootOwnedOperationalFile excludes engine/root-owned operational
|
|
495
|
+
// records from the writer delta. Their owning hooks/gates validate them where
|
|
496
|
+
// needed; root activity must not be attributed to the slice writer.
|
|
497
|
+
func reconcileRootOwnedOperationalFile(filename, rel string, entry os.DirEntry) (bool, error) {
|
|
498
|
+
if !entry.Type().IsRegular() || !reconcileRootOwnedOperationalPath(rel) {
|
|
499
|
+
return false, nil
|
|
500
|
+
}
|
|
501
|
+
rel = filepath.ToSlash(filepath.Clean(rel))
|
|
502
|
+
parts := strings.Split(rel, "/")
|
|
503
|
+
if len(parts) == 3 && parts[2] == recoveryAttemptsFile {
|
|
504
|
+
if _, err := readRecoveryAttempts(filename); err != nil {
|
|
505
|
+
return false, fmt.Errorf("validate root-owned recovery ledger %s: %w", rel, err)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return true, nil
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
func reconcileRootOwnedOperationalPath(rel string) bool {
|
|
512
|
+
rel = filepath.ToSlash(filepath.Clean(rel))
|
|
513
|
+
if rel == "timeline.jsonl" {
|
|
514
|
+
return true
|
|
515
|
+
}
|
|
516
|
+
parts := strings.Split(rel, "/")
|
|
517
|
+
if len(parts) != 3 ||
|
|
518
|
+
(parts[0] != "work" && parts[0] != "features") ||
|
|
519
|
+
!slugToken.MatchString(parts[1]) {
|
|
520
|
+
return false
|
|
521
|
+
}
|
|
522
|
+
switch parts[2] {
|
|
523
|
+
case "events.jsonl",
|
|
524
|
+
recoveryAttemptsFile,
|
|
525
|
+
".red",
|
|
526
|
+
"handoff.md",
|
|
527
|
+
".a1-guard.log",
|
|
528
|
+
".reviewer-ro.log",
|
|
529
|
+
".stop-gate.log",
|
|
530
|
+
".wright-scope.log":
|
|
531
|
+
return true
|
|
532
|
+
default:
|
|
533
|
+
return false
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
func withoutRootOwnedOperationalState(state []devritesStateEntry) []devritesStateEntry {
|
|
538
|
+
filtered := state[:0]
|
|
539
|
+
for _, item := range state {
|
|
540
|
+
if strings.HasPrefix(item.Fingerprint, "file:") &&
|
|
541
|
+
reconcileRootOwnedOperationalPath(item.Path) {
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
filtered = append(filtered, item)
|
|
545
|
+
}
|
|
546
|
+
return filtered
|
|
547
|
+
}
|
|
548
|
+
|
|
486
549
|
func devritesFingerprint(filename string, entry os.DirEntry) (string, error) {
|
|
487
550
|
info, err := entry.Info()
|
|
488
551
|
if err != nil {
|
|
@@ -361,6 +361,158 @@ func TestReconcileRejectsDevritesMutation(t *testing.T) {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
func TestReconcileAllowsRootOwnedOperationalFiles(t *testing.T) {
|
|
365
|
+
newGitRepo(t)
|
|
366
|
+
root := workspace(t, "feat")
|
|
367
|
+
dir := featureDir(root, "feat")
|
|
368
|
+
writeWrightAllowlist(t, root, "feat", "seed.go")
|
|
369
|
+
if code := Stuck(root, []string{"log", "feat", "dispatch", "SLICE-010"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
|
|
370
|
+
t.Fatalf("stuck log = %d, want 0", code)
|
|
371
|
+
}
|
|
372
|
+
if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
|
|
373
|
+
t.Fatalf("snapshot = %d, want 0\n%s", code, out)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
writeFile(t, filepath.Join(root, "timeline.jsonl"), "telemetry\n")
|
|
377
|
+
writeFile(t, filepath.Join(dir, "events.jsonl"), "telemetry\n")
|
|
378
|
+
writeFile(t, filepath.Join(dir, ".a1-guard.log"), "WOULD-BLOCK\n")
|
|
379
|
+
writeFile(t, filepath.Join(dir, ".reviewer-ro.log"), "WOULD-BLOCK\n")
|
|
380
|
+
writeFile(t, filepath.Join(dir, ".stop-gate.log"), "WOULD-BLOCK\n")
|
|
381
|
+
writeFile(t, filepath.Join(dir, ".wright-scope.log"), "WOULD-BLOCK\n")
|
|
382
|
+
writeFile(t, filepath.Join(dir, ".red"), "npm test\n")
|
|
383
|
+
writeFile(t, filepath.Join(dir, "handoff.md"), "## Handoff snapshot\n")
|
|
384
|
+
if code := RecoveryAttempts(
|
|
385
|
+
root,
|
|
386
|
+
[]string{"record", "--class", "proof_tool_defect", "reconcile telemetry", "changed logs", "feat"},
|
|
387
|
+
&bytes.Buffer{},
|
|
388
|
+
&bytes.Buffer{},
|
|
389
|
+
); code != 0 {
|
|
390
|
+
t.Fatalf("recovery record = %d, want 0", code)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
|
|
394
|
+
t.Fatalf("check = %d, want 0\n%s", code, out)
|
|
395
|
+
}
|
|
396
|
+
if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
|
|
397
|
+
t.Fatalf("refresh = %d, want 0\n%s", code, out)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
writeFile(t, filepath.Join(dir, ".unknown.log"), "not engine-owned\n")
|
|
401
|
+
code, out := runReconcile(t, root, "check", "feat")
|
|
402
|
+
if code != 5 {
|
|
403
|
+
t.Fatalf("check after canonical mutation = %d, want 5\n%s", code, out)
|
|
404
|
+
}
|
|
405
|
+
if !strings.Contains(out, ".devrites/work/feat/.unknown.log") {
|
|
406
|
+
t.Fatalf("canonical mutation missing from output:\n%s", out)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
func TestReconcileAcceptsLegacyOperationalFingerprints(t *testing.T) {
|
|
411
|
+
newGitRepo(t)
|
|
412
|
+
root := workspace(t, "feat")
|
|
413
|
+
dir := featureDir(root, "feat")
|
|
414
|
+
writeWrightAllowlist(t, root, "feat", "seed.go")
|
|
415
|
+
writeFile(t, filepath.Join(root, "timeline.jsonl"), "before\n")
|
|
416
|
+
writeFile(t, filepath.Join(dir, "events.jsonl"), "before\n")
|
|
417
|
+
writeFile(t, filepath.Join(dir, ".a1-guard.log"), "before\n")
|
|
418
|
+
writeFile(t, filepath.Join(dir, ".red"), "before\n")
|
|
419
|
+
writeFile(t, filepath.Join(dir, "handoff.md"), "before\n")
|
|
420
|
+
if code := RecoveryAttempts(
|
|
421
|
+
root,
|
|
422
|
+
[]string{"record", "--class", "proof_tool_defect", "reconcile telemetry", "first failure", "feat"},
|
|
423
|
+
&bytes.Buffer{},
|
|
424
|
+
&bytes.Buffer{},
|
|
425
|
+
); code != 0 {
|
|
426
|
+
t.Fatalf("initial recovery record = %d, want 0", code)
|
|
427
|
+
}
|
|
428
|
+
if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
|
|
429
|
+
t.Fatalf("snapshot = %d, want 0\n%s", code, out)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
snapshotPath := filepath.Join(dir, reconcileDevritesName)
|
|
433
|
+
state, err := readDevritesState(snapshotPath)
|
|
434
|
+
if err != nil {
|
|
435
|
+
t.Fatal(err)
|
|
436
|
+
}
|
|
437
|
+
have := make(map[string]bool, len(state))
|
|
438
|
+
for _, item := range state {
|
|
439
|
+
have[item.Path] = true
|
|
440
|
+
}
|
|
441
|
+
for _, path := range []string{
|
|
442
|
+
"timeline.jsonl",
|
|
443
|
+
"work/feat/events.jsonl",
|
|
444
|
+
"work/feat/.a1-guard.log",
|
|
445
|
+
"work/feat/.red",
|
|
446
|
+
"work/feat/handoff.md",
|
|
447
|
+
"work/feat/recovery-attempts.jsonl",
|
|
448
|
+
} {
|
|
449
|
+
if !have[path] {
|
|
450
|
+
state = append(state, devritesStateEntry{Path: path, Fingerprint: "file:0600:legacy"})
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if err := writeDevritesState(snapshotPath, state); err != nil {
|
|
454
|
+
t.Fatal(err)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
writeFile(t, filepath.Join(root, "timeline.jsonl"), "after\n")
|
|
458
|
+
writeFile(t, filepath.Join(dir, "events.jsonl"), "after\n")
|
|
459
|
+
writeFile(t, filepath.Join(dir, ".a1-guard.log"), "after\n")
|
|
460
|
+
writeFile(t, filepath.Join(dir, ".red"), "after\n")
|
|
461
|
+
writeFile(t, filepath.Join(dir, "handoff.md"), "after\n")
|
|
462
|
+
if code := RecoveryAttempts(
|
|
463
|
+
root,
|
|
464
|
+
[]string{"record", "--class", "proof_tool_defect", "reconcile telemetry", "second failure", "feat"},
|
|
465
|
+
&bytes.Buffer{},
|
|
466
|
+
&bytes.Buffer{},
|
|
467
|
+
); code != 0 {
|
|
468
|
+
t.Fatalf("second recovery record = %d, want 0", code)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
|
|
472
|
+
t.Fatalf("check = %d, want 0\n%s", code, out)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func TestReconcileRejectsCorruptRootOwnedRecoveryLedger(t *testing.T) {
|
|
477
|
+
newGitRepo(t)
|
|
478
|
+
root := workspace(t, "feat")
|
|
479
|
+
writeWrightAllowlist(t, root, "feat", "seed.go")
|
|
480
|
+
if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
|
|
481
|
+
t.Fatalf("snapshot = %d, want 0\n%s", code, out)
|
|
482
|
+
}
|
|
483
|
+
writeFile(t, filepath.Join(featureDir(root, "feat"), recoveryAttemptsFile), "{not-json}\n")
|
|
484
|
+
|
|
485
|
+
code, out := runReconcile(t, root, "check", "feat")
|
|
486
|
+
if code != 6 {
|
|
487
|
+
t.Fatalf("check = %d, want 6\n%s", code, out)
|
|
488
|
+
}
|
|
489
|
+
if !strings.Contains(out, "validate root-owned recovery ledger") {
|
|
490
|
+
t.Fatalf("corrupt recovery ledger missing from output:\n%s", out)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
func TestReconcileRootOwnedOperationalPathsAreExact(t *testing.T) {
|
|
495
|
+
for _, path := range []string{
|
|
496
|
+
"timeline.jsonl",
|
|
497
|
+
"work/feat/.red",
|
|
498
|
+
"features/feat/events.jsonl",
|
|
499
|
+
} {
|
|
500
|
+
if !reconcileRootOwnedOperationalPath(path) {
|
|
501
|
+
t.Errorf("%q should be root-owned operational state", path)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for _, path := range []string{
|
|
505
|
+
"work/feat/.unknown.log",
|
|
506
|
+
"work/feat/action.log",
|
|
507
|
+
"work/feat/footprint.log",
|
|
508
|
+
"work/feat/state.md",
|
|
509
|
+
} {
|
|
510
|
+
if reconcileRootOwnedOperationalPath(path) {
|
|
511
|
+
t.Errorf("%q must remain fingerprinted", path)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
364
516
|
func TestReconcileCheckFailsClosedWhenObjectDatabaseIsMissing(t *testing.T) {
|
|
365
517
|
newGitRepo(t)
|
|
366
518
|
root := workspace(t, "feat")
|
|
@@ -583,6 +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
|
+
runGuard(hookPayload(t, map[string]any{
|
|
587
|
+
"tool_name": "Write",
|
|
588
|
+
"tool_input": map[string]any{"file_path": filepath.Join(t.TempDir(), "agent-packet.yaml")},
|
|
589
|
+
}), false)
|
|
590
|
+
runGuard(`{"tool_name":"Write","tool_input":{"file_path":"../outside.txt"}}`, true)
|
|
586
591
|
runGuard(`{"tool_name":"Write","tool_input":{"file_path":"src/app.go"}}`, true)
|
|
587
592
|
runGuard(`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, true)
|
|
588
593
|
runGuard(`{"tool_name":"js","tool_input":{"code":"writeFile()"}}`, true)
|
|
@@ -1695,7 +1700,7 @@ func TestCodexAgentDispatchRejectsIncompleteDurableV2(t *testing.T) {
|
|
|
1695
1700
|
}
|
|
1696
1701
|
}
|
|
1697
1702
|
|
|
1698
|
-
func
|
|
1703
|
+
func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T) {
|
|
1699
1704
|
root := newWorkspace(t)
|
|
1700
1705
|
writeActive(t, root, "auth-tokens")
|
|
1701
1706
|
workspace := filepath.Join(root, "features", "auth-tokens")
|
|
@@ -1714,14 +1719,26 @@ func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
|
|
|
1714
1719
|
"prompt": "$rite-build",
|
|
1715
1720
|
}), nil, "hook", "agent-dispatch", "--harness=codex")
|
|
1716
1721
|
|
|
1717
|
-
|
|
1722
|
+
check := hookPayload(t, map[string]any{
|
|
1723
|
+
"hook_event_name": "PreToolUse",
|
|
1724
|
+
"session_id": sessionID,
|
|
1725
|
+
"turn_id": turnID,
|
|
1726
|
+
"tool_name": "Bash",
|
|
1727
|
+
"tool_input": map[string]any{"command": "rtk devrites-engine reconcile check"},
|
|
1728
|
+
})
|
|
1729
|
+
if out, stderr, code := runDevritesIO(t, root, check, nil,
|
|
1730
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1731
|
+
t.Fatalf("ordinary reconcile check exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
closeWindow := hookPayload(t, map[string]any{
|
|
1718
1735
|
"hook_event_name": "PreToolUse",
|
|
1719
1736
|
"session_id": sessionID,
|
|
1720
1737
|
"turn_id": turnID,
|
|
1721
1738
|
"tool_name": "Bash",
|
|
1722
1739
|
"tool_input": map[string]any{"command": "rtk devrites-engine reconcile close"},
|
|
1723
1740
|
})
|
|
1724
|
-
out, _, _ := runDevritesIO(t, root,
|
|
1741
|
+
out, _, _ := runDevritesIO(t, root, closeWindow, nil,
|
|
1725
1742
|
"hook", "agent-dispatch", "--harness=codex")
|
|
1726
1743
|
if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
|
|
1727
1744
|
!strings.Contains(reason, "confirmed") {
|
|
@@ -1774,7 +1791,7 @@ func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
|
|
|
1774
1791
|
}
|
|
1775
1792
|
}
|
|
1776
1793
|
|
|
1777
|
-
if out, stderr, code := runDevritesIO(t, root,
|
|
1794
|
+
if out, stderr, code := runDevritesIO(t, root, closeWindow, nil,
|
|
1778
1795
|
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1779
1796
|
t.Fatalf("completed wright reconcile exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1780
1797
|
}
|
|
@@ -26,8 +26,8 @@ only the source-write lifecycle.
|
|
|
26
26
|
4. Immediately before a serial dispatch:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
devrites-engine reconcile snapshot
|
|
30
29
|
devrites-engine stuck log "$(cat .devrites/ACTIVE 2>/dev/null)" dispatch "<slice id>"
|
|
30
|
+
devrites-engine reconcile snapshot
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
A Forge slice runs `forge plan` first, then takes this same snapshot; see
|
|
@@ -26,8 +26,8 @@ only the source-write lifecycle.
|
|
|
26
26
|
4. Immediately before a serial dispatch:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
devrites-engine reconcile snapshot
|
|
30
29
|
devrites-engine stuck log "$(cat .devrites/ACTIVE 2>/dev/null)" dispatch "<slice id>"
|
|
30
|
+
devrites-engine reconcile snapshot
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
A Forge slice runs `forge plan` first, then takes this same snapshot; see
|
|
@@ -26,8 +26,8 @@ only the source-write lifecycle.
|
|
|
26
26
|
4. Immediately before a serial dispatch:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
devrites-engine reconcile snapshot
|
|
30
29
|
devrites-engine stuck log "$(cat .devrites/ACTIVE 2>/dev/null)" dispatch "<slice id>"
|
|
30
|
+
devrites-engine reconcile snapshot
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
A Forge slice runs `forge plan` first, then takes this same snapshot; see
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.9",
|
|
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",
|