devrites 3.2.16 → 3.2.18

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.
Files changed (122) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/engine/Makefile +3 -3
  4. package/engine/hooks.go +20 -4
  5. package/engine/hooks_agent_dispatch.go +297 -48
  6. package/engine/hooks_events_test.go +40 -0
  7. package/engine/hooks_workspace.go +26 -6
  8. package/engine/internal/devritespaths/paths.go +80 -0
  9. package/engine/internal/install/install.go +36 -16
  10. package/engine/internal/install/install_test.go +50 -1
  11. package/engine/internal/iohooks/iohooks.go +5 -3
  12. package/engine/internal/iohooks/iohooks_test.go +11 -0
  13. package/engine/internal/lib/adoption.go +36 -11
  14. package/engine/internal/lib/adoption_test.go +30 -0
  15. package/engine/internal/lib/analyze.go +10 -2
  16. package/engine/internal/lib/archivesearch.go +41 -9
  17. package/engine/internal/lib/archivesearch_test.go +28 -0
  18. package/engine/internal/lib/buildreadiness.go +5 -1
  19. package/engine/internal/lib/closeout.go +34 -11
  20. package/engine/internal/lib/closeout_test.go +99 -0
  21. package/engine/internal/lib/context.go +6 -1
  22. package/engine/internal/lib/dispatchwaive.go +31 -0
  23. package/engine/internal/lib/dispatchwaive_test.go +23 -0
  24. package/engine/internal/lib/extensions.go +5 -1
  25. package/engine/internal/lib/learnings.go +8 -2
  26. package/engine/internal/lib/ledger.go +17 -4
  27. package/engine/internal/lib/ledger_test.go +24 -0
  28. package/engine/internal/lib/mutationgate.go +6 -2
  29. package/engine/internal/lib/reconcile.go +201 -64
  30. package/engine/internal/lib/reconcile_test.go +159 -12
  31. package/engine/internal/lib/resolve.go +8 -3
  32. package/engine/internal/lib/resolve_remediation_test.go +14 -0
  33. package/engine/internal/lib/runbook.go +4 -1
  34. package/engine/internal/lib/runbook_context_test.go +43 -0
  35. package/engine/internal/lib/testintegrity.go +4 -4
  36. package/engine/internal/lib/testintegrity_test.go +15 -0
  37. package/engine/internal/lib/tickafk.go +6 -2
  38. package/engine/internal/lib/util.go +128 -23
  39. package/engine/internal/profile/profile.go +27 -13
  40. package/engine/internal/profile/profile_test.go +13 -0
  41. package/engine/main.go +4 -1
  42. package/engine/testdata/golden/TestParityReconcile/close-window.golden +1 -0
  43. package/engine/testdata/golden/TestParityReconcile/not-git.golden +1 -1
  44. package/engine/testdata/golden/TestParityTestIntegrity/not-git.golden +1 -1
  45. package/engine/tests/hook_test.go +353 -17
  46. package/engine/tests/parity_reconcile_test.go +4 -1
  47. package/engine/tests/parity_testintegrity_test.go +1 -1
  48. package/pack/.claude/agents/devrites-code-reviewer.md +3 -0
  49. package/pack/.claude/agents/devrites-devex-reviewer.md +12 -8
  50. package/pack/.claude/agents/devrites-doubt-reviewer.md +3 -0
  51. package/pack/.claude/agents/devrites-forge-judge.md +4 -1
  52. package/pack/.claude/agents/devrites-frontend-reviewer.md +3 -0
  53. package/pack/.claude/agents/devrites-performance-reviewer.md +10 -9
  54. package/pack/.claude/agents/devrites-plan-reviewer.md +3 -0
  55. package/pack/.claude/agents/devrites-proof-runner.md +14 -14
  56. package/pack/.claude/agents/devrites-retrospector.md +3 -0
  57. package/pack/.claude/agents/devrites-security-auditor.md +3 -0
  58. package/pack/.claude/agents/devrites-simplifier-reviewer.md +3 -0
  59. package/pack/.claude/agents/devrites-slice-wright.md +4 -2
  60. package/pack/.claude/agents/devrites-spec-reviewer.md +3 -0
  61. package/pack/.claude/agents/devrites-strategy-reviewer.md +3 -0
  62. package/pack/.claude/agents/devrites-test-analyst.md +3 -0
  63. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
  64. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +10 -0
  65. package/pack/.claude/skills/devrites-source-driven/SKILL.md +1 -1
  66. package/pack/.claude/skills/rite-build/reference/phase-contract.md +17 -3
  67. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +3 -1
  68. package/pack/.claude/skills/rite-prove/SKILL.md +25 -14
  69. package/pack/.claude/skills/rite-temper/SKILL.md +1 -1
  70. package/pack/.claude/skills/rite-upgrade/SKILL.md +1 -1
  71. package/pack/.claude/skills/rite-vet/SKILL.md +18 -15
  72. package/pack/generated/claude/agents/devrites-code-reviewer.md +3 -0
  73. package/pack/generated/claude/agents/devrites-devex-reviewer.md +12 -8
  74. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +3 -0
  75. package/pack/generated/claude/agents/devrites-forge-judge.md +4 -1
  76. package/pack/generated/claude/agents/devrites-frontend-reviewer.md +3 -0
  77. package/pack/generated/claude/agents/devrites-performance-reviewer.md +10 -9
  78. package/pack/generated/claude/agents/devrites-plan-reviewer.md +3 -0
  79. package/pack/generated/claude/agents/devrites-proof-runner.md +14 -14
  80. package/pack/generated/claude/agents/devrites-retrospector.md +3 -0
  81. package/pack/generated/claude/agents/devrites-security-auditor.md +3 -0
  82. package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +3 -0
  83. package/pack/generated/claude/agents/devrites-slice-wright.md +4 -2
  84. package/pack/generated/claude/agents/devrites-spec-reviewer.md +3 -0
  85. package/pack/generated/claude/agents/devrites-strategy-reviewer.md +3 -0
  86. package/pack/generated/claude/agents/devrites-test-analyst.md +3 -0
  87. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
  88. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +10 -0
  89. package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +1 -1
  90. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +17 -3
  91. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +3 -1
  92. package/pack/generated/claude/skills/rite-prove/SKILL.md +25 -14
  93. package/pack/generated/claude/skills/rite-temper/SKILL.md +1 -1
  94. package/pack/generated/claude/skills/rite-upgrade/SKILL.md +1 -1
  95. package/pack/generated/claude/skills/rite-vet/SKILL.md +18 -15
  96. package/pack/generated/codex/agents/devrites-code-reviewer.toml +3 -0
  97. package/pack/generated/codex/agents/devrites-devex-reviewer.toml +11 -7
  98. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +3 -0
  99. package/pack/generated/codex/agents/devrites-forge-judge.toml +4 -1
  100. package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +3 -0
  101. package/pack/generated/codex/agents/devrites-performance-reviewer.toml +9 -8
  102. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +3 -0
  103. package/pack/generated/codex/agents/devrites-proof-runner.toml +13 -13
  104. package/pack/generated/codex/agents/devrites-retrospector.toml +3 -0
  105. package/pack/generated/codex/agents/devrites-security-auditor.toml +3 -0
  106. package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +3 -0
  107. package/pack/generated/codex/agents/devrites-slice-wright.toml +4 -2
  108. package/pack/generated/codex/agents/devrites-spec-reviewer.toml +3 -0
  109. package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +3 -0
  110. package/pack/generated/codex/agents/devrites-test-analyst.toml +3 -0
  111. package/pack/generated/codex/hooks.json +1 -1
  112. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +8 -0
  113. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +10 -0
  114. package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +1 -1
  115. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +17 -3
  116. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +3 -1
  117. package/pack/generated/codex/skills/rite-prove/SKILL.md +25 -14
  118. package/pack/generated/codex/skills/rite-temper/SKILL.md +1 -1
  119. package/pack/generated/codex/skills/rite-upgrade/SKILL.md +1 -1
  120. package/pack/generated/codex/skills/rite-vet/SKILL.md +18 -15
  121. package/package.json +1 -1
  122. package/scripts/codex-generate.sh +1 -1
@@ -60,6 +60,28 @@ func TestContextSyncRejectsUnsafePath(t *testing.T) {
60
60
  }
61
61
  }
62
62
 
63
+ func TestContextSyncDoesNotOverwriteUnreadableTarget(t *testing.T) {
64
+ project := t.TempDir()
65
+ root := filepath.Join(project, ".devrites")
66
+ if err := os.MkdirAll(root, 0o755); err != nil {
67
+ t.Fatal(err)
68
+ }
69
+ target := filepath.Join(project, "AGENTS.md")
70
+ if err := os.Mkdir(target, 0o755); err != nil {
71
+ t.Fatal(err)
72
+ }
73
+ stderr := &bytes.Buffer{}
74
+ if code := Context(root, []string{"sync", "AGENTS.md"}, &bytes.Buffer{}, stderr); code != 1 {
75
+ t.Fatalf("unreadable target code = %d, want 1; stderr=%q", code, stderr.String())
76
+ }
77
+ if !strings.Contains(stderr.String(), "read existing context file") {
78
+ t.Fatalf("stderr=%q, want target read error", stderr.String())
79
+ }
80
+ if info, err := os.Stat(target); err != nil || !info.IsDir() {
81
+ t.Fatalf("target was replaced: info=%v err=%v", info, err)
82
+ }
83
+ }
84
+
63
85
  func TestContextSyncRefusesExternalWorkspaceOverride(t *testing.T) {
64
86
  project := t.TempDir()
65
87
  root := filepath.Join(project, ".devrites")
@@ -137,3 +159,24 @@ func TestRunbookDryRunDoesNotExecuteShell(t *testing.T) {
137
159
  t.Fatal("dry run executed shell step")
138
160
  }
139
161
  }
162
+
163
+ func TestRunbookReportsCompletionStateWriteFailure(t *testing.T) {
164
+ root := filepath.Join(t.TempDir(), ".devrites")
165
+ if err := os.MkdirAll(root, 0o755); err != nil {
166
+ t.Fatal(err)
167
+ }
168
+ path := filepath.Join(t.TempDir(), "demo.yaml")
169
+ if err := os.WriteFile(path, []byte("steps:\n - rite: build\n"), 0o644); err != nil {
170
+ t.Fatal(err)
171
+ }
172
+ if err := os.WriteFile(filepath.Join(root, "runs"), []byte("blocks state directory"), 0o644); err != nil {
173
+ t.Fatal(err)
174
+ }
175
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
176
+ if code := executeRunbook(root, path, 0, "test-run", false, stdout, stderr); code != 1 {
177
+ t.Fatalf("executeRunbook code = %d, want 1; stdout=%q stderr=%q", code, stdout.String(), stderr.String())
178
+ }
179
+ if strings.Contains(stdout.String(), "completed") || !strings.Contains(stderr.String(), "runbook:") {
180
+ t.Fatalf("completion write failure was misreported; stdout=%q stderr=%q", stdout.String(), stderr.String())
181
+ }
182
+ }
@@ -52,10 +52,10 @@ func TestIntegrity(root string, args []string, stdout, stderr io.Writer) int {
52
52
  }
53
53
 
54
54
  cwd, _ := os.Getwd()
55
- gitRoot := gitToplevel(cwd)
56
- if gitRoot == "" {
57
- fmt.Fprintln(stderr, "test-integrity: not a git repo: gate skipped; inspect the test diff manually.")
58
- return 0
55
+ gitRoot, err := gitToplevel(cwd)
56
+ if err != nil {
57
+ fmt.Fprintf(stderr, "test-integrity: cannot resolve git worktree: %v\n", err)
58
+ return 2
59
59
  }
60
60
 
61
61
  trees, err := captureSliceTreeRange(gitRoot, root, d)
@@ -77,6 +77,21 @@ func TestIsSourceFileRecognisesCode(t *testing.T) {
77
77
  }
78
78
  }
79
79
 
80
+ func TestIntegrityFailsClosedWhenGitIsUnavailable(t *testing.T) {
81
+ newGitRepo(t)
82
+ root := workspace(t, "feat")
83
+ t.Setenv("PATH", t.TempDir())
84
+
85
+ var stdout, stderr bytes.Buffer
86
+ code := TestIntegrity(root, []string{"feat"}, &stdout, &stderr)
87
+ if code != 2 {
88
+ t.Fatalf("test-integrity = %d, want 2\nstdout: %s\nstderr: %s", code, stdout.String(), stderr.String())
89
+ }
90
+ if !strings.Contains(stderr.String(), "cannot resolve git worktree") {
91
+ t.Fatalf("missing fail-closed diagnostic: %s", stderr.String())
92
+ }
93
+ }
94
+
80
95
  func TestIntegrityDetectsDeletionFromDirtySliceBaseline(t *testing.T) {
81
96
  gitRoot := newGitRepo(t)
82
97
  root := workspace(t, "feat")
@@ -23,7 +23,7 @@ import (
23
23
  // 5 the file is missing, or the field holds a non-numeric value
24
24
  func TickAfk(args []string, stdout, stderr io.Writer) int {
25
25
  path := argAt(args, 0)
26
- if path == "" || !isFile(path) {
26
+ if path == "" {
27
27
  where := path
28
28
  if where == "" {
29
29
  where = "<unset>"
@@ -32,7 +32,11 @@ func TickAfk(args []string, stdout, stderr io.Writer) int {
32
32
  return 5
33
33
  }
34
34
 
35
- data, _ := os.ReadFile(path)
35
+ data, err := os.ReadFile(path)
36
+ if err != nil {
37
+ fmt.Fprintf(stderr, "tick-afk: read state.md at %s: %v\n", path, err)
38
+ return 5
39
+ }
36
40
  lines := splitLinesNoTrailing(data)
37
41
 
38
42
  remaining, found := readBudget(lines)
@@ -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
- // gitToplevel returns the absolute path of the git working tree containing dir,
10
- // or "" when dir is not inside a repository. The git-backed gates
11
- // (test-integrity and reconcile) skip rather than block when it is empty.
12
- func gitToplevel(dir string) string {
13
- out, err := exec.Command("git", "-C", dir, "rev-parse", "--show-toplevel").Output()
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
- return ""
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(strings.TrimSpace(string(out)))
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{"-C", gitRoot, "diff", "--name-only"}, refs...)
24
- out, err := exec.Command("git", args...).Output()
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.
@@ -68,7 +68,11 @@ func Run(args []string, stdout, stderr io.Writer) int {
68
68
  fmt.Fprintln(stdout, path)
69
69
  return 0
70
70
  case "refresh":
71
- p := derive(root, rootSHA, headSHA)
71
+ p, err := derive(root, rootSHA, headSHA)
72
+ if err != nil {
73
+ fmt.Fprintf(stderr, "devrites: profile cache refresh failed: %v\n", err)
74
+ return 1
75
+ }
72
76
  if err := writeCache(path, p); err != nil {
73
77
  fmt.Fprintf(stderr, "devrites: profile cache write failed: %v\n", err)
74
78
  return 1
@@ -151,9 +155,12 @@ func writeCache(path string, p Profile) error {
151
155
  return os.Rename(name, path)
152
156
  }
153
157
 
154
- func derive(root, rootSHA, headSHA string) Profile {
158
+ func derive(root, rootSHA, headSHA string) (Profile, error) {
155
159
  p := Profile{ProfileSchemaVersion: schemaVersion, RootSHA: rootSHA, HeadSHA: headSHA, Inputs: map[string]FileInfo{}}
156
- entries, _ := os.ReadDir(root)
160
+ entries, err := os.ReadDir(root)
161
+ if err != nil {
162
+ return Profile{}, fmt.Errorf("read repository root: %w", err)
163
+ }
157
164
  for _, e := range entries {
158
165
  name := e.Name()
159
166
  if strings.HasPrefix(name, ".git") || name == "node_modules" {
@@ -165,12 +172,15 @@ func derive(root, rootSHA, headSHA string) Profile {
165
172
  p.TopLevel = append(p.TopLevel, name)
166
173
  }
167
174
  sort.Strings(p.TopLevel)
168
- _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
175
+ if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
169
176
  if err != nil {
170
- return nil
177
+ return err
171
178
  }
172
179
  rel, err := filepath.Rel(root, path)
173
- if err != nil || rel == "." {
180
+ if err != nil {
181
+ return err
182
+ }
183
+ if rel == "." {
174
184
  return nil
175
185
  }
176
186
  rel = filepath.ToSlash(rel)
@@ -185,23 +195,27 @@ func derive(root, rootSHA, headSHA string) Profile {
185
195
  p.Manifests = append(p.Manifests, rel)
186
196
  }
187
197
  if isProfileInput(rel) {
188
- if info, ok := fileInfo(path); ok {
189
- p.Inputs[rel] = info
198
+ info, err := fileInfo(path)
199
+ if err != nil {
200
+ return err
190
201
  }
202
+ p.Inputs[rel] = info
191
203
  }
192
204
  return nil
193
- })
205
+ }); err != nil {
206
+ return Profile{}, fmt.Errorf("walk repository: %w", err)
207
+ }
194
208
  sort.Strings(p.Manifests)
195
- return p
209
+ return p, nil
196
210
  }
197
211
 
198
- func fileInfo(path string) (FileInfo, bool) {
212
+ func fileInfo(path string) (FileInfo, error) {
199
213
  raw, err := os.ReadFile(path)
200
214
  if err != nil {
201
- return FileInfo{}, false
215
+ return FileInfo{}, fmt.Errorf("read profile input %s: %w", path, err)
202
216
  }
203
217
  sum := sha256.Sum256(raw)
204
- return FileInfo{SHA256: hex.EncodeToString(sum[:]), Bytes: len(raw)}, true
218
+ return FileInfo{SHA256: hex.EncodeToString(sum[:]), Bytes: len(raw)}, nil
205
219
  }
206
220
 
207
221
  func cleanProfileInputs(root string) bool {
@@ -0,0 +1,13 @@
1
+ package profile
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+ )
7
+
8
+ func TestDeriveReportsUnreadableRoot(t *testing.T) {
9
+ _, err := derive(filepath.Join(t.TempDir(), "missing"), "root", "head")
10
+ if err == nil {
11
+ t.Fatal("derive succeeded for a missing repository root")
12
+ }
13
+ }
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 reconcile <sub> [slug] A1 gate: snapshot|check|close the wright's source writes
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,2 +1,3 @@
1
1
  exit 0
2
+ reconcile: OK: every slice change matches the orchestrator allowlist; baseline retained for post-slice gates (run "reconcile close" afterward).
2
3
  reconcile: closed slice window for feat.
@@ -1 +1 @@
1
- exit 0
1
+ exit 6
@@ -1 +1 @@
1
- exit 0
1
+ exit 2