devrites 3.2.16 → 3.2.17
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 +8 -0
- package/README.md +1 -1
- package/engine/hooks.go +20 -4
- package/engine/hooks_agent_dispatch.go +297 -48
- package/engine/hooks_events_test.go +40 -0
- package/engine/hooks_workspace.go +26 -6
- package/engine/internal/devritespaths/paths.go +80 -0
- package/engine/internal/lib/adoption.go +22 -7
- package/engine/internal/lib/adoption_test.go +30 -0
- package/engine/internal/lib/closeout.go +34 -11
- package/engine/internal/lib/closeout_test.go +99 -0
- package/engine/internal/lib/dispatchwaive.go +31 -0
- package/engine/internal/lib/dispatchwaive_test.go +23 -0
- package/engine/internal/lib/mutationgate.go +6 -2
- package/engine/internal/lib/reconcile.go +201 -64
- package/engine/internal/lib/reconcile_test.go +159 -12
- package/engine/internal/lib/testintegrity.go +4 -4
- package/engine/internal/lib/testintegrity_test.go +15 -0
- package/engine/internal/lib/util.go +128 -23
- package/engine/main.go +4 -1
- package/engine/testdata/golden/TestParityReconcile/close-window.golden +1 -0
- package/engine/testdata/golden/TestParityReconcile/not-git.golden +1 -1
- package/engine/testdata/golden/TestParityTestIntegrity/not-git.golden +1 -1
- package/engine/tests/hook_test.go +353 -17
- package/engine/tests/parity_reconcile_test.go +4 -1
- package/engine/tests/parity_testintegrity_test.go +1 -1
- package/pack/.claude/agents/devrites-code-reviewer.md +3 -0
- package/pack/.claude/agents/devrites-devex-reviewer.md +12 -8
- package/pack/.claude/agents/devrites-doubt-reviewer.md +3 -0
- package/pack/.claude/agents/devrites-forge-judge.md +4 -1
- package/pack/.claude/agents/devrites-frontend-reviewer.md +3 -0
- package/pack/.claude/agents/devrites-performance-reviewer.md +10 -9
- package/pack/.claude/agents/devrites-plan-reviewer.md +3 -0
- package/pack/.claude/agents/devrites-proof-runner.md +14 -14
- package/pack/.claude/agents/devrites-retrospector.md +3 -0
- package/pack/.claude/agents/devrites-security-auditor.md +3 -0
- package/pack/.claude/agents/devrites-simplifier-reviewer.md +3 -0
- package/pack/.claude/agents/devrites-slice-wright.md +4 -2
- package/pack/.claude/agents/devrites-spec-reviewer.md +3 -0
- package/pack/.claude/agents/devrites-strategy-reviewer.md +3 -0
- package/pack/.claude/agents/devrites-test-analyst.md +3 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +10 -0
- package/pack/.claude/skills/devrites-source-driven/SKILL.md +1 -1
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +17 -3
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +3 -1
- package/pack/.claude/skills/rite-prove/SKILL.md +25 -14
- package/pack/.claude/skills/rite-temper/SKILL.md +1 -1
- package/pack/.claude/skills/rite-upgrade/SKILL.md +1 -1
- package/pack/.claude/skills/rite-vet/SKILL.md +18 -15
- package/pack/generated/claude/agents/devrites-code-reviewer.md +3 -0
- package/pack/generated/claude/agents/devrites-devex-reviewer.md +12 -8
- package/pack/generated/claude/agents/devrites-doubt-reviewer.md +3 -0
- package/pack/generated/claude/agents/devrites-forge-judge.md +4 -1
- package/pack/generated/claude/agents/devrites-frontend-reviewer.md +3 -0
- package/pack/generated/claude/agents/devrites-performance-reviewer.md +10 -9
- package/pack/generated/claude/agents/devrites-plan-reviewer.md +3 -0
- package/pack/generated/claude/agents/devrites-proof-runner.md +14 -14
- package/pack/generated/claude/agents/devrites-retrospector.md +3 -0
- package/pack/generated/claude/agents/devrites-security-auditor.md +3 -0
- package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +3 -0
- package/pack/generated/claude/agents/devrites-slice-wright.md +4 -2
- package/pack/generated/claude/agents/devrites-spec-reviewer.md +3 -0
- package/pack/generated/claude/agents/devrites-strategy-reviewer.md +3 -0
- package/pack/generated/claude/agents/devrites-test-analyst.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +10 -0
- package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +17 -3
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +3 -1
- package/pack/generated/claude/skills/rite-prove/SKILL.md +25 -14
- package/pack/generated/claude/skills/rite-temper/SKILL.md +1 -1
- package/pack/generated/claude/skills/rite-upgrade/SKILL.md +1 -1
- package/pack/generated/claude/skills/rite-vet/SKILL.md +18 -15
- package/pack/generated/codex/agents/devrites-code-reviewer.toml +3 -0
- package/pack/generated/codex/agents/devrites-devex-reviewer.toml +11 -7
- package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +3 -0
- package/pack/generated/codex/agents/devrites-forge-judge.toml +4 -1
- package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +3 -0
- package/pack/generated/codex/agents/devrites-performance-reviewer.toml +9 -8
- package/pack/generated/codex/agents/devrites-plan-reviewer.toml +3 -0
- package/pack/generated/codex/agents/devrites-proof-runner.toml +13 -13
- package/pack/generated/codex/agents/devrites-retrospector.toml +3 -0
- package/pack/generated/codex/agents/devrites-security-auditor.toml +3 -0
- package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +3 -0
- package/pack/generated/codex/agents/devrites-slice-wright.toml +4 -2
- package/pack/generated/codex/agents/devrites-spec-reviewer.toml +3 -0
- package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +3 -0
- package/pack/generated/codex/agents/devrites-test-analyst.toml +3 -0
- package/pack/generated/codex/hooks.json +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +10 -0
- package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +17 -3
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +3 -1
- package/pack/generated/codex/skills/rite-prove/SKILL.md +25 -14
- package/pack/generated/codex/skills/rite-temper/SKILL.md +1 -1
- package/pack/generated/codex/skills/rite-upgrade/SKILL.md +1 -1
- package/pack/generated/codex/skills/rite-vet/SKILL.md +18 -15
- package/package.json +1 -1
- package/scripts/codex-generate.sh +1 -1
|
@@ -1,42 +1,147 @@
|
|
|
1
1
|
package lib
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
4
9
|
"os/exec"
|
|
5
10
|
"path/filepath"
|
|
6
11
|
"strings"
|
|
12
|
+
"time"
|
|
7
13
|
)
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
var errNotGitRepository = errors.New("not a git repository")
|
|
16
|
+
|
|
17
|
+
const (
|
|
18
|
+
gitCommandTimeout = 30 * time.Second
|
|
19
|
+
gitOutputLimit = 16 << 20
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
type cappedGitOutput struct {
|
|
23
|
+
bytes.Buffer
|
|
24
|
+
truncated bool
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func (w *cappedGitOutput) Write(p []byte) (int, error) {
|
|
28
|
+
remaining := gitOutputLimit - w.Len()
|
|
29
|
+
if remaining > 0 {
|
|
30
|
+
if len(p) > remaining {
|
|
31
|
+
_, _ = w.Buffer.Write(p[:remaining])
|
|
32
|
+
w.truncated = true
|
|
33
|
+
} else {
|
|
34
|
+
_, _ = w.Buffer.Write(p)
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
w.truncated = true
|
|
38
|
+
}
|
|
39
|
+
return len(p), nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type gitCommandError struct {
|
|
43
|
+
args []string
|
|
44
|
+
output string
|
|
45
|
+
exitCode int
|
|
46
|
+
err error
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func (e *gitCommandError) Error() string {
|
|
50
|
+
detail := strings.TrimSpace(e.output)
|
|
51
|
+
if detail == "" {
|
|
52
|
+
return fmt.Sprintf("git %s: %v", strings.Join(e.args, " "), e.err)
|
|
53
|
+
}
|
|
54
|
+
return fmt.Sprintf("git %s: %v: %s", strings.Join(e.args, " "), e.err, detail)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (e *gitCommandError) Unwrap() error { return e.err }
|
|
58
|
+
|
|
59
|
+
func runGitCommand(dir string, env []string, args ...string) ([]byte, error) {
|
|
60
|
+
ctx, cancel := context.WithTimeout(context.Background(), gitCommandTimeout)
|
|
61
|
+
defer cancel()
|
|
62
|
+
commandArgs := append([]string{"-C", dir}, args...)
|
|
63
|
+
cmd := exec.CommandContext(ctx, "git", commandArgs...)
|
|
64
|
+
cmd.WaitDelay = 2 * time.Second
|
|
65
|
+
if env == nil {
|
|
66
|
+
env = os.Environ()
|
|
67
|
+
} else {
|
|
68
|
+
env = append([]string{}, env...)
|
|
69
|
+
}
|
|
70
|
+
cmd.Env = append(
|
|
71
|
+
env,
|
|
72
|
+
"GIT_TERMINAL_PROMPT=0",
|
|
73
|
+
"GCM_INTERACTIVE=never",
|
|
74
|
+
"GIT_PAGER=cat",
|
|
75
|
+
"PAGER=cat",
|
|
76
|
+
"LC_ALL=C",
|
|
77
|
+
)
|
|
78
|
+
var output cappedGitOutput
|
|
79
|
+
cmd.Stdout = &output
|
|
80
|
+
cmd.Stderr = &output
|
|
81
|
+
err := cmd.Run()
|
|
82
|
+
if err == nil {
|
|
83
|
+
return output.Bytes(), nil
|
|
84
|
+
}
|
|
85
|
+
code := -1
|
|
86
|
+
var exitErr *exec.ExitError
|
|
87
|
+
if errors.As(err, &exitErr) {
|
|
88
|
+
code = exitErr.ExitCode()
|
|
89
|
+
}
|
|
90
|
+
if ctx.Err() != nil {
|
|
91
|
+
err = ctx.Err()
|
|
92
|
+
}
|
|
93
|
+
detail := output.String()
|
|
94
|
+
if output.truncated {
|
|
95
|
+
detail += "\n[git output truncated]"
|
|
96
|
+
}
|
|
97
|
+
return nil, &gitCommandError{
|
|
98
|
+
args: args,
|
|
99
|
+
output: detail,
|
|
100
|
+
exitCode: code,
|
|
101
|
+
err: err,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func gitErrorExitCode(err error) (int, bool) {
|
|
106
|
+
var commandErr *gitCommandError
|
|
107
|
+
if !errors.As(err, &commandErr) {
|
|
108
|
+
return 0, false
|
|
109
|
+
}
|
|
110
|
+
return commandErr.exitCode, commandErr.exitCode >= 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// gitToplevel returns the absolute path of the Git working tree containing dir.
|
|
114
|
+
// Repository absence is distinct from Git execution failure so integrity gates
|
|
115
|
+
// can fail closed on a missing binary, corrupt repository, or poisoned runtime.
|
|
116
|
+
func gitToplevel(dir string) (string, error) {
|
|
117
|
+
out, err := runGitCommand(dir, nil, "rev-parse", "--show-toplevel")
|
|
14
118
|
if err != nil {
|
|
15
|
-
|
|
119
|
+
var commandErr *gitCommandError
|
|
120
|
+
if errors.As(err, &commandErr) {
|
|
121
|
+
detail := strings.ToLower(commandErr.output)
|
|
122
|
+
if strings.Contains(detail, "not a git repository") ||
|
|
123
|
+
strings.Contains(detail, "not a git work tree") {
|
|
124
|
+
return "", errNotGitRepository
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return "", err
|
|
128
|
+
}
|
|
129
|
+
root := strings.TrimSpace(string(out))
|
|
130
|
+
if root == "" {
|
|
131
|
+
return "", fmt.Errorf("git rev-parse returned an empty top-level path")
|
|
16
132
|
}
|
|
17
|
-
return filepath.Clean(
|
|
133
|
+
return filepath.Clean(root), nil
|
|
18
134
|
}
|
|
19
135
|
|
|
20
136
|
// gitDiffNames lists the paths that differ, relative to gitRoot. With one ref it
|
|
21
137
|
// diffs the working tree against that ref; with two it diffs the two tree objects.
|
|
22
|
-
func gitDiffNames(gitRoot string, refs ...string) []string {
|
|
23
|
-
args := append([]string{"
|
|
24
|
-
out, err :=
|
|
138
|
+
func gitDiffNames(gitRoot string, refs ...string) ([]string, error) {
|
|
139
|
+
args := append([]string{"diff", "--name-only"}, refs...)
|
|
140
|
+
out, err := runGitCommand(gitRoot, nil, args...)
|
|
25
141
|
if err != nil {
|
|
26
|
-
return nil
|
|
142
|
+
return nil, err
|
|
27
143
|
}
|
|
28
|
-
return splitLinesNoTrailing(out)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// stripASCIISpace removes every whitespace character from s (used to normalise an
|
|
32
|
-
// ACTIVE pointer before comparing it to a slug).
|
|
33
|
-
func stripASCIISpace(s string) string {
|
|
34
|
-
return strings.Map(func(r rune) rune {
|
|
35
|
-
if strings.ContainsRune(spaceChars, r) {
|
|
36
|
-
return -1
|
|
37
|
-
}
|
|
38
|
-
return r
|
|
39
|
-
}, s)
|
|
144
|
+
return splitLinesNoTrailing(out), nil
|
|
40
145
|
}
|
|
41
146
|
|
|
42
147
|
// isAllDigits reports whether s is a non-empty run of ASCII digits.
|
package/engine/main.go
CHANGED
|
@@ -56,7 +56,8 @@ Usage:
|
|
|
56
56
|
devrites-engine mutation-gate [slug] Advisory: detect the mutation runner and scope it
|
|
57
57
|
devrites-engine test-integrity [slug] Gate: no test deleted, skipped, or de-asserted
|
|
58
58
|
devrites-engine review-integrity [slug] Gate: no adversarial review axis is silent (zero findings, no justification)
|
|
59
|
-
devrites-engine
|
|
59
|
+
devrites-engine dispatch-waive <reason> Record a deterministic pre-dispatch STOP reason
|
|
60
|
+
devrites-engine reconcile <sub> [slug] A1 gate: snapshot|check|restore-check|close the wright's source writes
|
|
60
61
|
devrites-engine resolve <qid> "<ans>" Resolve an open question; keep state.md consistent
|
|
61
62
|
devrites-engine close-out <slug> Archive a shipped feature and clear ACTIVE
|
|
62
63
|
devrites-engine archive-search "<nouns>" Find prior shipped specs that overlap the query
|
|
@@ -238,6 +239,8 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
238
239
|
return lib.TestIntegrity(root, args[1:], stdout, stderr)
|
|
239
240
|
case "review-integrity":
|
|
240
241
|
return lib.ReviewIntegrity(root, args[1:], stdout, stderr)
|
|
242
|
+
case "dispatch-waive":
|
|
243
|
+
return lib.DispatchWaive(args[1:], stdout, stderr)
|
|
241
244
|
case "reconcile":
|
|
242
245
|
return lib.Reconcile(root, args[1:], stdout, stderr)
|
|
243
246
|
case "resolve":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
exit
|
|
1
|
+
exit 6
|
|
@@ -1 +1 @@
|
|
|
1
|
-
exit
|
|
1
|
+
exit 2
|
|
@@ -236,6 +236,18 @@ func parsePermissionDecision(t *testing.T, stdout string) (decision, reason stri
|
|
|
236
236
|
return env.HookSpecificOutput.PermissionDecision, env.HookSpecificOutput.PermissionDecisionReason
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
func parseStopDecision(t *testing.T, stdout string) (decision, reason string) {
|
|
240
|
+
t.Helper()
|
|
241
|
+
var env struct {
|
|
242
|
+
Decision string `json:"decision"`
|
|
243
|
+
Reason string `json:"reason"`
|
|
244
|
+
}
|
|
245
|
+
if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &env); err != nil {
|
|
246
|
+
t.Fatalf("stdout is not a valid Stop decision: %v\n%s", err, stdout)
|
|
247
|
+
}
|
|
248
|
+
return env.Decision, env.Reason
|
|
249
|
+
}
|
|
250
|
+
|
|
239
251
|
func TestHookReviewerReadonlyEnforceDeniesMutatingBash(t *testing.T) {
|
|
240
252
|
for _, h := range []string{"claude", "codex"} {
|
|
241
253
|
h := h
|
|
@@ -260,14 +272,22 @@ func TestHookReviewerReadonlyEnforceDeniesMutatingBash(t *testing.T) {
|
|
|
260
272
|
|
|
261
273
|
func TestHookReviewerReadonlyAllowsSafeBash(t *testing.T) {
|
|
262
274
|
root := newWorkspace(t)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
"
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
275
|
+
for _, command := range []string{
|
|
276
|
+
"grep -rn foo .",
|
|
277
|
+
`cd "` + filepath.Dir(root) + `" && git diff --binary --full-index --no-renames HEAD HEAD --`,
|
|
278
|
+
} {
|
|
279
|
+
in := hookPayload(t, map[string]any{
|
|
280
|
+
"tool_name": "Bash",
|
|
281
|
+
"tool_input": map[string]any{"command": command},
|
|
282
|
+
})
|
|
283
|
+
out, _, code := runDevritesIO(t, root, in, []string{"DEVRITES_REVIEWER_RO=enforce"},
|
|
284
|
+
"hook", "reviewer-readonly", "--harness=claude")
|
|
285
|
+
if code != 0 {
|
|
286
|
+
t.Fatalf("%q exit = %d, want 0", command, code)
|
|
287
|
+
}
|
|
288
|
+
if strings.TrimSpace(out) != "" {
|
|
289
|
+
t.Errorf("%q stdout = %q, want silent for a read-only command", command, out)
|
|
290
|
+
}
|
|
271
291
|
}
|
|
272
292
|
}
|
|
273
293
|
|
|
@@ -440,20 +460,87 @@ func TestHookReviewerReadonlyActiveLeafDeniesEveryMutationSurface(t *testing.T)
|
|
|
440
460
|
|
|
441
461
|
func TestHookReviewerReadonlyActiveLeafAllowsBoundedProof(t *testing.T) {
|
|
442
462
|
root := newWorkspace(t)
|
|
443
|
-
|
|
444
|
-
out, _, code := runDevritesIO(t, root, in, []string{
|
|
463
|
+
env := []string{
|
|
445
464
|
"DEVRITES_AGENT_RUN=1",
|
|
446
465
|
"DEVRITES_ACTIVE_AGENT=devrites-proof-runner",
|
|
447
|
-
}
|
|
466
|
+
}
|
|
467
|
+
for _, command := range []string{
|
|
468
|
+
"go test ./... -count=1",
|
|
469
|
+
"command -v devrites-engine",
|
|
470
|
+
"devrites-engine conventions orient",
|
|
471
|
+
"devrites-engine learnings mine",
|
|
472
|
+
} {
|
|
473
|
+
in := fmt.Sprintf(`{"tool_name":"Bash","tool_input":{"command":%q}}`, command)
|
|
474
|
+
out, _, code := runDevritesIO(t, root, in, env,
|
|
475
|
+
"hook", "reviewer-readonly", "--harness=claude")
|
|
476
|
+
if code != 0 || strings.TrimSpace(out) != "" {
|
|
477
|
+
t.Fatalf("%q should be allowed: exit=%d out=%q", command, code, out)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
in := `{"tool_name":"Bash","tool_input":{"command":"go test ./... -count=1"}}`
|
|
481
|
+
out, _, code := runDevritesIO(t, root, in, env,
|
|
482
|
+
"hook", "wright-scope", "--harness=claude")
|
|
448
483
|
if code != 0 || strings.TrimSpace(out) != "" {
|
|
449
|
-
t.Fatalf("
|
|
484
|
+
t.Fatalf("wright hook must not shadow reviewer policy: exit=%d out=%q", code, out)
|
|
450
485
|
}
|
|
451
|
-
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
func TestHookReviewerReadonlyProofRunnerKeepsArtifactProofRootOwned(t *testing.T) {
|
|
489
|
+
root := newWorkspace(t)
|
|
490
|
+
env := []string{
|
|
452
491
|
"DEVRITES_AGENT_RUN=1",
|
|
453
492
|
"DEVRITES_ACTIVE_AGENT=devrites-proof-runner",
|
|
454
|
-
}
|
|
493
|
+
}
|
|
494
|
+
for _, command := range []string{
|
|
495
|
+
"npm run build",
|
|
496
|
+
"npm run test:e2e -- tests/admin-locations.e2e.spec.ts",
|
|
497
|
+
"go build ./...",
|
|
498
|
+
"bash scripts/mutate-repository.sh",
|
|
499
|
+
"npm run deploy",
|
|
500
|
+
"make deploy",
|
|
501
|
+
"docker compose up -d",
|
|
502
|
+
"printf x > src/app.go",
|
|
503
|
+
"npm install left-pad",
|
|
504
|
+
"python -c 'open(\"src/app.go\", \"w\").write(\"x\")'",
|
|
505
|
+
} {
|
|
506
|
+
in := fmt.Sprintf(`{"tool_name":"Bash","tool_input":{"command":%q}}`, command)
|
|
507
|
+
out, errOut, code := runDevritesIO(t, root, in, env,
|
|
508
|
+
"hook", "reviewer-readonly", "--harness=claude")
|
|
509
|
+
if code != 0 {
|
|
510
|
+
t.Fatalf("root-owned proof command guard exit=%d stderr=%q", code, errOut)
|
|
511
|
+
}
|
|
512
|
+
if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
|
|
513
|
+
t.Fatalf("proof leaf command was not denied: command=%q out=%q", command, out)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
func TestHookRedwatchRecognizesCodexShellAliases(t *testing.T) {
|
|
519
|
+
root := newWorkspace(t)
|
|
520
|
+
writeActive(t, root, "auth-tokens")
|
|
521
|
+
red := filepath.Join(root, "features", "auth-tokens", ".red")
|
|
522
|
+
|
|
523
|
+
fail := `{"tool_name":"functions.exec_command","tool_input":{"cmd":"npm test"},"tool_response":"Tests: 1 failed, 3 passed"}`
|
|
524
|
+
out, errOut, code := runDevritesIO(t, root, fail, nil,
|
|
525
|
+
"hook", "redwatch", "--harness=codex")
|
|
526
|
+
if code != 0 {
|
|
527
|
+
t.Fatalf("failing exec_command exit=%d stderr=%q", code, errOut)
|
|
528
|
+
}
|
|
529
|
+
if !strings.Contains(out, "tests/build are RED") {
|
|
530
|
+
t.Fatalf("failing exec_command omitted RED context: %q", out)
|
|
531
|
+
}
|
|
532
|
+
if _, err := os.Stat(red); err != nil {
|
|
533
|
+
t.Fatalf("failing exec_command did not create .red: %v", err)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
pass := `{"tool_name":"exec_command","tool_input":{"cmd":"npm test"},"tool_response":"PASS: all tests passed"}`
|
|
537
|
+
out, errOut, code = runDevritesIO(t, root, pass, nil,
|
|
538
|
+
"hook", "redwatch", "--harness=codex")
|
|
455
539
|
if code != 0 || strings.TrimSpace(out) != "" {
|
|
456
|
-
t.Fatalf("
|
|
540
|
+
t.Fatalf("passing exec_command exit=%d out=%q stderr=%q", code, out, errOut)
|
|
541
|
+
}
|
|
542
|
+
if _, err := os.Stat(red); !os.IsNotExist(err) {
|
|
543
|
+
t.Fatalf("passing exec_command retained .red: %v", err)
|
|
457
544
|
}
|
|
458
545
|
}
|
|
459
546
|
|
|
@@ -502,7 +589,7 @@ func TestHookWrightScopeUsesExactOrchestratorAllowlist(t *testing.T) {
|
|
|
502
589
|
}
|
|
503
590
|
}
|
|
504
591
|
|
|
505
|
-
func
|
|
592
|
+
func TestHookWrightScopeRequiresBoundCodexWorkerDuringReconcileWindow(t *testing.T) {
|
|
506
593
|
root := newWorkspace(t)
|
|
507
594
|
writeActive(t, root, "auth-tokens")
|
|
508
595
|
workspace := filepath.Join(root, "features", "auth-tokens")
|
|
@@ -519,7 +606,7 @@ func TestHookWrightScopeBindsCodexWorkerDuringReconcileWindow(t *testing.T) {
|
|
|
519
606
|
input string
|
|
520
607
|
deny bool
|
|
521
608
|
}{
|
|
522
|
-
{"listed edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"},"agent_type":"worker"}`,
|
|
609
|
+
{"unbound listed edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"},"agent_type":"worker"}`, true},
|
|
523
610
|
{"unlisted edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/other.go"},"agent_type":"worker"}`, true},
|
|
524
611
|
} {
|
|
525
612
|
t.Run(test.name, func(t *testing.T) {
|
|
@@ -549,6 +636,90 @@ func TestHookWrightScopeBindsCodexWorkerDuringReconcileWindow(t *testing.T) {
|
|
|
549
636
|
}
|
|
550
637
|
}
|
|
551
638
|
|
|
639
|
+
func TestHookWrightScopeAllowsOnlyBoundCodexV1WorkerPaths(t *testing.T) {
|
|
640
|
+
root := newWorkspace(t)
|
|
641
|
+
writeActive(t, root, "auth-tokens")
|
|
642
|
+
workspace := filepath.Join(root, "features", "auth-tokens")
|
|
643
|
+
for name, body := range map[string]string{
|
|
644
|
+
".wright-allowlist": "src/app.go\n",
|
|
645
|
+
".reconcile-base": "snapshot\n",
|
|
646
|
+
".reconcile-allowlist": "src/app.go\n",
|
|
647
|
+
".reconcile-devrites": "[]\n",
|
|
648
|
+
} {
|
|
649
|
+
if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
|
|
650
|
+
t.Fatal(err)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
|
|
654
|
+
t.Fatal(err)
|
|
655
|
+
}
|
|
656
|
+
sessionID, turnID := "session-bound-worker", "turn-bound-worker"
|
|
657
|
+
writeCodexAgentContract(t, root, "devrites-slice-wright")
|
|
658
|
+
writeCodexSkillContract(t, root, "rite-build", "devrites-slice-wright")
|
|
659
|
+
runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
660
|
+
"hook_event_name": "UserPromptSubmit",
|
|
661
|
+
"session_id": sessionID,
|
|
662
|
+
"turn_id": turnID,
|
|
663
|
+
"prompt": "$rite-build",
|
|
664
|
+
}), nil, "hook", "agent-dispatch", "--harness=codex")
|
|
665
|
+
if out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
666
|
+
"hook_event_name": "PreToolUse",
|
|
667
|
+
"session_id": sessionID,
|
|
668
|
+
"turn_id": turnID,
|
|
669
|
+
"tool_name": "spawn_agent",
|
|
670
|
+
"tool_use_id": "spawn-bound-worker",
|
|
671
|
+
"tool_input": map[string]any{
|
|
672
|
+
"agent_type": "worker",
|
|
673
|
+
"fork_turns": "none",
|
|
674
|
+
"message": "Read .codex/agents/devrites-slice-wright.toml.",
|
|
675
|
+
},
|
|
676
|
+
}), nil, "hook", "agent-dispatch", "--harness=codex"); code != 0 {
|
|
677
|
+
t.Fatalf("spawn exit=%d out=%s stderr=%s", code, out, stderr)
|
|
678
|
+
} else if decision, reason := parsePermissionDecision(t, out); decision != "allow" {
|
|
679
|
+
t.Fatalf("spawn denied: %s (%s)", decision, reason)
|
|
680
|
+
}
|
|
681
|
+
if out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
682
|
+
"hook_event_name": "SubagentStart",
|
|
683
|
+
"session_id": sessionID,
|
|
684
|
+
"turn_id": "child-turn",
|
|
685
|
+
"agent_id": "bound-worker",
|
|
686
|
+
"agent_type": "worker",
|
|
687
|
+
}), nil, "hook", "subagent-orient", "--harness=codex"); code != 0 ||
|
|
688
|
+
!strings.Contains(out, "Confirmed role") {
|
|
689
|
+
t.Fatalf("start exit=%d out=%s stderr=%s", code, out, stderr)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
env := []string{"DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1", "DEVRITES_HOOK_PROFILE=strict"}
|
|
693
|
+
for _, test := range []struct {
|
|
694
|
+
path string
|
|
695
|
+
deny bool
|
|
696
|
+
}{
|
|
697
|
+
{"src/app.go", false},
|
|
698
|
+
{"src/other.go", true},
|
|
699
|
+
} {
|
|
700
|
+
input := hookPayload(t, map[string]any{
|
|
701
|
+
"hook_event_name": "PreToolUse",
|
|
702
|
+
"session_id": sessionID,
|
|
703
|
+
"agent_id": "bound-worker",
|
|
704
|
+
"agent_type": "worker",
|
|
705
|
+
"tool_name": "Edit",
|
|
706
|
+
"tool_input": map[string]any{"file_path": test.path},
|
|
707
|
+
})
|
|
708
|
+
out, stderr, code := runDevritesIO(t, root, input, env,
|
|
709
|
+
"hook", "wright-scope", "--harness=codex")
|
|
710
|
+
if code != 0 {
|
|
711
|
+
t.Fatalf("%s exit=%d stderr=%s", test.path, code, stderr)
|
|
712
|
+
}
|
|
713
|
+
if test.deny {
|
|
714
|
+
if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
|
|
715
|
+
t.Fatalf("%s should be denied: %s", test.path, out)
|
|
716
|
+
}
|
|
717
|
+
} else if strings.TrimSpace(out) != "" {
|
|
718
|
+
t.Fatalf("%s should be allowed: %s", test.path, out)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
552
723
|
func TestHookWrightScopeAcceptsCodexV2NamedRole(t *testing.T) {
|
|
553
724
|
root := newWorkspace(t)
|
|
554
725
|
writeActive(t, root, "auth-tokens")
|
|
@@ -629,6 +800,8 @@ func TestHookWrightScopeRequiresSpawnedCodexWorkerForSourceWrites(t *testing.T)
|
|
|
629
800
|
runGuard(`{"tool_name":"Write","tool_input":{"file_path":"src/app.go"}}`, true)
|
|
630
801
|
runGuard(`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, true)
|
|
631
802
|
runGuard(`{"tool_name":"js","tool_input":{"code":"writeFile()"}}`, true)
|
|
803
|
+
runGuard(`{"tool_name":"Bash","tool_input":{"command":"devrites-engine reconcile restore-check auth-tokens"}}`, false)
|
|
804
|
+
runGuard(`{"tool_name":"Bash","tool_input":{"command":"devrites-engine reconcile restore-check auth-tokens && printf x > src/app.go"}}`, true)
|
|
632
805
|
out, errOut, code := runDevritesIO(t, root,
|
|
633
806
|
`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`,
|
|
634
807
|
nil, "hook", "wright-scope", "--harness=claude")
|
|
@@ -1544,6 +1717,74 @@ func TestCodexAgentDispatchBlocksFalseWaitAndStop(t *testing.T) {
|
|
|
1544
1717
|
}
|
|
1545
1718
|
}
|
|
1546
1719
|
|
|
1720
|
+
func TestCodexAgentDispatchRequiresSuccessfulReasonBoundWaiver(t *testing.T) {
|
|
1721
|
+
root := newWorkspace(t)
|
|
1722
|
+
role := "devrites-plan-reviewer"
|
|
1723
|
+
writeCodexAgentContract(t, root, role)
|
|
1724
|
+
writeCodexSkillContract(t, root, "rite-vet", role)
|
|
1725
|
+
sessionID, turnID := "session-waiver", "turn-waiver"
|
|
1726
|
+
runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
1727
|
+
"hook_event_name": "UserPromptSubmit",
|
|
1728
|
+
"session_id": sessionID,
|
|
1729
|
+
"turn_id": turnID,
|
|
1730
|
+
"prompt": "$rite-vet",
|
|
1731
|
+
}), nil, "hook", "agent-dispatch", "--harness=codex")
|
|
1732
|
+
|
|
1733
|
+
invalid := hookPayload(t, map[string]any{
|
|
1734
|
+
"hook_event_name": "PreToolUse",
|
|
1735
|
+
"session_id": sessionID,
|
|
1736
|
+
"turn_id": turnID,
|
|
1737
|
+
"tool_name": "functions.exec_command",
|
|
1738
|
+
"tool_input": map[string]any{"cmd": "devrites-engine dispatch-waive wrong-phase && true"},
|
|
1739
|
+
})
|
|
1740
|
+
out, _, _ := runDevritesIO(t, root, invalid, nil,
|
|
1741
|
+
"hook", "agent-dispatch", "--harness=codex")
|
|
1742
|
+
if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
|
|
1743
|
+
t.Fatalf("chained waiver was not denied: %s", out)
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
command := "devrites-engine dispatch-waive wrong-phase"
|
|
1747
|
+
pre := hookPayload(t, map[string]any{
|
|
1748
|
+
"hook_event_name": "PreToolUse",
|
|
1749
|
+
"session_id": sessionID,
|
|
1750
|
+
"turn_id": turnID,
|
|
1751
|
+
"tool_name": "functions.exec_command",
|
|
1752
|
+
"tool_input": map[string]any{"cmd": command},
|
|
1753
|
+
})
|
|
1754
|
+
if out, stderr, code := runDevritesIO(t, root, pre, nil,
|
|
1755
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1756
|
+
t.Fatalf("waiver pre exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
stop := hookPayload(t, map[string]any{
|
|
1760
|
+
"hook_event_name": "Stop",
|
|
1761
|
+
"session_id": sessionID,
|
|
1762
|
+
"turn_id": turnID,
|
|
1763
|
+
})
|
|
1764
|
+
out, _, _ = runDevritesIO(t, root, stop, nil,
|
|
1765
|
+
"hook", "agent-dispatch", "--harness=codex")
|
|
1766
|
+
if decision, _ := parseStopDecision(t, out); decision != "block" {
|
|
1767
|
+
t.Fatalf("PreToolUse waiver falsely satisfied dispatch: %s", out)
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
post := hookPayload(t, map[string]any{
|
|
1771
|
+
"hook_event_name": "PostToolUse",
|
|
1772
|
+
"session_id": sessionID,
|
|
1773
|
+
"turn_id": turnID,
|
|
1774
|
+
"tool_name": "functions.exec_command",
|
|
1775
|
+
"tool_input": map[string]any{"cmd": command},
|
|
1776
|
+
"tool_response": "dispatch-waive: accepted wrong-phase",
|
|
1777
|
+
})
|
|
1778
|
+
if out, stderr, code := runDevritesIO(t, root, post, nil,
|
|
1779
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1780
|
+
t.Fatalf("waiver post exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1781
|
+
}
|
|
1782
|
+
if out, stderr, code := runDevritesIO(t, root, stop, nil,
|
|
1783
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1784
|
+
t.Fatalf("waived stop exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1547
1788
|
func TestCodexAgentDispatchConfirmsGenericRoleAndResult(t *testing.T) {
|
|
1548
1789
|
root := newWorkspace(t)
|
|
1549
1790
|
sessionID, turnID := "session-complete", "turn-complete"
|
|
@@ -1664,12 +1905,99 @@ func TestCodexAgentDispatchConfirmsGenericRoleAndResult(t *testing.T) {
|
|
|
1664
1905
|
t.Fatalf("wait exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1665
1906
|
}
|
|
1666
1907
|
|
|
1908
|
+
if out, stderr, code := runDevritesIO(t, root, stop, nil,
|
|
1909
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 {
|
|
1910
|
+
t.Fatalf("pre-wait stop exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1911
|
+
} else if decision, _ := parseStopDecision(t, out); decision != "block" {
|
|
1912
|
+
t.Fatalf("PreToolUse wait falsely satisfied dispatch: %s", out)
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
waitTimeout := hookPayload(t, map[string]any{
|
|
1916
|
+
"hook_event_name": "PostToolUse",
|
|
1917
|
+
"session_id": sessionID,
|
|
1918
|
+
"turn_id": turnID,
|
|
1919
|
+
"tool_name": "wait",
|
|
1920
|
+
"tool_input": map[string]any{"ids": []string{"agent-1"}},
|
|
1921
|
+
"tool_response": map[string]any{"timed_out": true},
|
|
1922
|
+
})
|
|
1923
|
+
if out, stderr, code := runDevritesIO(t, root, waitTimeout, nil,
|
|
1924
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1925
|
+
t.Fatalf("timed-out wait post exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1926
|
+
}
|
|
1927
|
+
if out, stderr, code := runDevritesIO(t, root, stop, nil,
|
|
1928
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 {
|
|
1929
|
+
t.Fatalf("post-timeout stop exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1930
|
+
} else if decision, _ := parseStopDecision(t, out); decision != "block" {
|
|
1931
|
+
t.Fatalf("timed-out wait falsely satisfied dispatch: %s", out)
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
waitDone := hookPayload(t, map[string]any{
|
|
1935
|
+
"hook_event_name": "PostToolUse",
|
|
1936
|
+
"session_id": sessionID,
|
|
1937
|
+
"turn_id": turnID,
|
|
1938
|
+
"tool_name": "wait",
|
|
1939
|
+
"tool_input": map[string]any{"ids": []string{"agent-1"}},
|
|
1940
|
+
"tool_response": map[string]any{"status": "completed"},
|
|
1941
|
+
})
|
|
1942
|
+
if out, stderr, code := runDevritesIO(t, root, waitDone, nil,
|
|
1943
|
+
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1944
|
+
t.Fatalf("wait post exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1667
1947
|
if out, stderr, code := runDevritesIO(t, root, stop, nil,
|
|
1668
1948
|
"hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
|
|
1669
1949
|
t.Fatalf("resolved stop exit=%d out=%s stderr=%s", code, out, stderr)
|
|
1670
1950
|
}
|
|
1671
1951
|
}
|
|
1672
1952
|
|
|
1953
|
+
func TestCodexAgentDispatchRefusesAmbiguousGenericStart(t *testing.T) {
|
|
1954
|
+
root := newWorkspace(t)
|
|
1955
|
+
sessionID, turnID := "session-ambiguous-start", "turn-ambiguous-start"
|
|
1956
|
+
roles := []string{"devrites-security-auditor", "devrites-plan-reviewer"}
|
|
1957
|
+
for _, role := range roles {
|
|
1958
|
+
writeCodexAgentContract(t, root, role)
|
|
1959
|
+
}
|
|
1960
|
+
writeCodexSkillContract(t, root, "devrites-audit", strings.Join(roles, ","))
|
|
1961
|
+
runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
1962
|
+
"hook_event_name": "UserPromptSubmit",
|
|
1963
|
+
"session_id": sessionID,
|
|
1964
|
+
"turn_id": turnID,
|
|
1965
|
+
"prompt": "$devrites-audit",
|
|
1966
|
+
}), nil, "hook", "agent-dispatch", "--harness=codex")
|
|
1967
|
+
|
|
1968
|
+
for i, role := range roles {
|
|
1969
|
+
out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
1970
|
+
"hook_event_name": "PreToolUse",
|
|
1971
|
+
"session_id": sessionID,
|
|
1972
|
+
"turn_id": turnID,
|
|
1973
|
+
"tool_name": "spawn_agent",
|
|
1974
|
+
"tool_use_id": fmt.Sprintf("spawn-%d", i),
|
|
1975
|
+
"tool_input": map[string]any{
|
|
1976
|
+
"agent_type": "explorer",
|
|
1977
|
+
"fork_turns": "none",
|
|
1978
|
+
"message": "Read .codex/agents/" + role + ".toml.",
|
|
1979
|
+
},
|
|
1980
|
+
}), nil, "hook", "agent-dispatch", "--harness=codex")
|
|
1981
|
+
if code != 0 {
|
|
1982
|
+
t.Fatalf("spawn %s exit=%d out=%s stderr=%s", role, code, out, stderr)
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
|
|
1987
|
+
"hook_event_name": "SubagentStart",
|
|
1988
|
+
"session_id": sessionID,
|
|
1989
|
+
"turn_id": "child-turn",
|
|
1990
|
+
"agent_id": "ambiguous-agent",
|
|
1991
|
+
"agent_type": "explorer",
|
|
1992
|
+
}), nil, "hook", "subagent-orient", "--harness=codex")
|
|
1993
|
+
if code != 0 {
|
|
1994
|
+
t.Fatalf("start exit=%d stderr=%s", code, stderr)
|
|
1995
|
+
}
|
|
1996
|
+
if !strings.Contains(out, "Dispatch binding failed") {
|
|
1997
|
+
t.Fatalf("ambiguous generic start was bound to an arbitrary role: %s", out)
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1673
2001
|
func TestCodexAgentDispatchConfirmsDurableV2NamedRole(t *testing.T) {
|
|
1674
2002
|
root := newWorkspace(t)
|
|
1675
2003
|
codeHome := t.TempDir()
|
|
@@ -1960,6 +2288,14 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
|
|
|
1960
2288
|
"tool_name": "wait",
|
|
1961
2289
|
"tool_input": map[string]any{"ids": []string{"agent-wright"}},
|
|
1962
2290
|
},
|
|
2291
|
+
{
|
|
2292
|
+
"hook_event_name": "PostToolUse",
|
|
2293
|
+
"session_id": sessionID,
|
|
2294
|
+
"turn_id": turnID,
|
|
2295
|
+
"tool_name": "wait",
|
|
2296
|
+
"tool_input": map[string]any{"ids": []string{"agent-wright"}},
|
|
2297
|
+
"tool_response": map[string]any{"status": "completed"},
|
|
2298
|
+
},
|
|
1963
2299
|
} {
|
|
1964
2300
|
hook := "agent-dispatch"
|
|
1965
2301
|
if payload["hook_event_name"] == "SubagentStart" {
|
|
@@ -71,6 +71,9 @@ func TestParityReconcile(t *testing.T) {
|
|
|
71
71
|
t.Run("close-window", func(t *testing.T) {
|
|
72
72
|
work := newRepo(t)
|
|
73
73
|
snapshot(t, work)
|
|
74
|
+
if out, code := runArgv(t, work, libRootEnv(work), "", binPath, "reconcile", "check", "feat"); code != 0 {
|
|
75
|
+
t.Fatalf("pre-close reconcile check = %d, want 0\n%s", code, out)
|
|
76
|
+
}
|
|
74
77
|
out, code := runArgv(t, work, libRootEnv(work), "", binPath, "reconcile", "close", "feat")
|
|
75
78
|
assertGolden(t, out, code)
|
|
76
79
|
})
|
|
@@ -78,7 +81,7 @@ func TestParityReconcile(t *testing.T) {
|
|
|
78
81
|
t.Run("not-git", func(t *testing.T) {
|
|
79
82
|
work := t.TempDir()
|
|
80
83
|
makeFeatureDir(t, work, "feat")
|
|
81
|
-
check(t, work) //
|
|
84
|
+
check(t, work) // fail closed outside Git
|
|
82
85
|
})
|
|
83
86
|
|
|
84
87
|
t.Run("bad-mode", func(t *testing.T) {
|