devrites 3.2.10 → 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 +6 -0
- package/README.md +1 -1
- package/docs/orchestration.md +9 -4
- package/engine/hooks.go +13 -0
- package/engine/hooks_agent_dispatch.go +2 -0
- package/engine/internal/lib/reconcile.go +68 -11
- package/engine/internal/lib/reconcile_test.go +51 -1
- package/engine/tests/hook_test.go +53 -5
- 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,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [3.2.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
|
+
|
|
5
11
|
## [3.2.10](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.9...v3.2.10) (2026-07-26)
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ final commit, push, and tag, and it requires a typed `GO` confirmation.
|
|
|
25
25
|
Unattended runs may create local WIP checkpoint commits along the way, but only
|
|
26
26
|
Ship collapses and pushes them.
|
|
27
27
|
|
|
28
|
-
**Status:** [`v3.2.
|
|
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
|
package/engine/hooks.go
CHANGED
|
@@ -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 {
|
|
@@ -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
|
}
|
|
@@ -926,6 +926,44 @@ func TestHookSubagentOrientSilentForNonDevritesAgent(t *testing.T) {
|
|
|
926
926
|
}
|
|
927
927
|
}
|
|
928
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
|
+
|
|
929
967
|
func hookPayload(t *testing.T, value map[string]any) string {
|
|
930
968
|
t.Helper()
|
|
931
969
|
raw, err := json.Marshal(value)
|
|
@@ -1759,11 +1797,18 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1759
1797
|
root := newWorkspace(t)
|
|
1760
1798
|
writeActive(t, root, "auth-tokens")
|
|
1761
1799
|
workspace := filepath.Join(root, "features", "auth-tokens")
|
|
1762
|
-
for
|
|
1763
|
-
|
|
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 {
|
|
1764
1806
|
t.Fatal(err)
|
|
1765
1807
|
}
|
|
1766
1808
|
}
|
|
1809
|
+
if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
|
|
1810
|
+
t.Fatal(err)
|
|
1811
|
+
}
|
|
1767
1812
|
sessionID, turnID := "session-wright", "turn-wright"
|
|
1768
1813
|
writeCodexAgentContract(t, root, "devrites-slice-wright")
|
|
1769
1814
|
writeCodexSkillContract(t, root, "rite-build", "devrites-slice-wright")
|
|
@@ -1808,7 +1853,7 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1808
1853
|
"tool_name": "spawn_agent",
|
|
1809
1854
|
"tool_use_id": "spawn-wright",
|
|
1810
1855
|
"tool_input": map[string]any{
|
|
1811
|
-
"agent_type": "
|
|
1856
|
+
"agent_type": "devrites-slice-wright",
|
|
1812
1857
|
"fork_turns": "none",
|
|
1813
1858
|
"message": "Read .codex/agents/devrites-slice-wright.toml.",
|
|
1814
1859
|
},
|
|
@@ -1818,14 +1863,14 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1818
1863
|
"session_id": sessionID,
|
|
1819
1864
|
"turn_id": "child-wright",
|
|
1820
1865
|
"agent_id": "agent-wright",
|
|
1821
|
-
"agent_type": "
|
|
1866
|
+
"agent_type": "devrites-slice-wright",
|
|
1822
1867
|
},
|
|
1823
1868
|
{
|
|
1824
1869
|
"hook_event_name": "SubagentStop",
|
|
1825
1870
|
"session_id": sessionID,
|
|
1826
1871
|
"turn_id": "child-wright",
|
|
1827
1872
|
"agent_id": "agent-wright",
|
|
1828
|
-
"agent_type": "
|
|
1873
|
+
"agent_type": "devrites-slice-wright",
|
|
1829
1874
|
"last_assistant_message": "agent-result/v1 complete",
|
|
1830
1875
|
},
|
|
1831
1876
|
{
|
|
@@ -1845,6 +1890,9 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1845
1890
|
t.Fatalf("%s exit=%d stderr=%s", hook, code, stderr)
|
|
1846
1891
|
}
|
|
1847
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
|
+
}
|
|
1848
1896
|
|
|
1849
1897
|
if out, stderr, code := runDevritesIO(t, root, closeWindow, nil,
|
|
1850
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",
|