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.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/engine/Makefile +3 -3
- 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/install/install.go +36 -16
- package/engine/internal/install/install_test.go +50 -1
- package/engine/internal/iohooks/iohooks.go +5 -3
- package/engine/internal/iohooks/iohooks_test.go +11 -0
- package/engine/internal/lib/adoption.go +36 -11
- package/engine/internal/lib/adoption_test.go +30 -0
- package/engine/internal/lib/analyze.go +10 -2
- package/engine/internal/lib/archivesearch.go +41 -9
- package/engine/internal/lib/archivesearch_test.go +28 -0
- package/engine/internal/lib/buildreadiness.go +5 -1
- package/engine/internal/lib/closeout.go +34 -11
- package/engine/internal/lib/closeout_test.go +99 -0
- package/engine/internal/lib/context.go +6 -1
- package/engine/internal/lib/dispatchwaive.go +31 -0
- package/engine/internal/lib/dispatchwaive_test.go +23 -0
- package/engine/internal/lib/extensions.go +5 -1
- package/engine/internal/lib/learnings.go +8 -2
- package/engine/internal/lib/ledger.go +17 -4
- package/engine/internal/lib/ledger_test.go +24 -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/resolve.go +8 -3
- package/engine/internal/lib/resolve_remediation_test.go +14 -0
- package/engine/internal/lib/runbook.go +4 -1
- package/engine/internal/lib/runbook_context_test.go +43 -0
- package/engine/internal/lib/testintegrity.go +4 -4
- package/engine/internal/lib/testintegrity_test.go +15 -0
- package/engine/internal/lib/tickafk.go +6 -2
- package/engine/internal/lib/util.go +128 -23
- package/engine/internal/profile/profile.go +27 -13
- package/engine/internal/profile/profile_test.go +13 -0
- 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
|
@@ -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
|
|
57
|
-
fmt.
|
|
58
|
-
return
|
|
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 == ""
|
|
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,
|
|
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
|
-
|
|
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.
|
|
@@ -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,
|
|
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
|
-
|
|
175
|
+
if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
169
176
|
if err != nil {
|
|
170
|
-
return
|
|
177
|
+
return err
|
|
171
178
|
}
|
|
172
179
|
rel, err := filepath.Rel(root, path)
|
|
173
|
-
if err != nil
|
|
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
|
-
|
|
189
|
-
|
|
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,
|
|
212
|
+
func fileInfo(path string) (FileInfo, error) {
|
|
199
213
|
raw, err := os.ReadFile(path)
|
|
200
214
|
if err != nil {
|
|
201
|
-
return FileInfo{},
|
|
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)},
|
|
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
|
|
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
|