Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
85e500c
test(made): capture remediation continuation contracts
douglasjarquin Aug 17, 2026
54f4cab
fix(made): enforce external tool contracts
douglasjarquin Aug 17, 2026
b384472
fix(made): complete lifecycle remediation contracts
douglasjarquin Aug 17, 2026
afea024
test(made): check decision errors
douglasjarquin Aug 17, 2026
5998bac
docs(made): record final validation ledger
douglasjarquin Aug 17, 2026
cd37a3f
fix(made): close lifecycle review findings
douglasjarquin Aug 17, 2026
4617d62
test(made): tighten awaiting-merge contract
douglasjarquin Aug 17, 2026
1c393c1
fix(made): close final durability review findings
douglasjarquin Aug 17, 2026
6a5f994
test(made): keep durable queue tests lint clean
douglasjarquin Aug 17, 2026
d0ad3d5
test(made): remove race suite timing flake
douglasjarquin Aug 17, 2026
2a77990
test(made): clear changed-file diagnostics
douglasjarquin Aug 17, 2026
c359423
test(made): clear final diagnostics
douglasjarquin Aug 17, 2026
3f0c774
fix(made): reject unsupported run arguments
douglasjarquin Aug 17, 2026
8c27c83
fix(made): preserve reviewer and recovery custody
douglasjarquin Aug 17, 2026
ad4b7b9
fix(made): contain managed gate paths
douglasjarquin Aug 17, 2026
752d6a8
fix(made): keep gate path validation lint clean
douglasjarquin Aug 17, 2026
6dbe361
fix(made): enforce pending and durable review contracts
douglasjarquin Aug 17, 2026
03f515b
fix(made): serialize durable snapshot publication
douglasjarquin Aug 17, 2026
d1dab7c
fix(made): close trust and decision boundary gaps
douglasjarquin Aug 17, 2026
fdd8a78
fix(made): bind gate notifications to received refs
douglasjarquin Aug 17, 2026
6042090
fix(made): close lifecycle durability boundary gaps
douglasjarquin Aug 17, 2026
51063e8
docs(made): record continuation validation evidence
douglasjarquin Aug 17, 2026
910fc54
fix(made): restrict review agent environment
douglasjarquin Aug 17, 2026
3ee7f91
docs(made): record final source validation
douglasjarquin Aug 17, 2026
453071a
docs(made): record final review and cleanup
douglasjarquin Aug 17, 2026
c661a43
docs(made): close direct PR ledger
douglasjarquin Aug 17, 2026
25df711
docs(made): record direct PR receipt
douglasjarquin Aug 17, 2026
0a7c21d
merge(main): repair remediation continuation conflicts
douglasjarquin Aug 17, 2026
bac8ed2
fix(made): remove obsolete review helpers
douglasjarquin Aug 17, 2026
918da27
fix(daemon): preserve compaction-triggering state
douglasjarquin Aug 17, 2026
12b83a6
docs(made): record conflict repair validation
douglasjarquin Aug 17, 2026
e7cb50a
docs(made): record final review receipt
douglasjarquin Aug 17, 2026
11a1bd1
docs(made): close review receipt
douglasjarquin Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions cmd/made/contracts_red_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"bytes"
"encoding/json"
"os"
"strings"
"testing"

"github.com/douglasjarquin/made/internal/daemon"
)

func TestCapabilitiesJSONExposesStructuredRunContract(t *testing.T) {
var stdout, stderr bytes.Buffer
stdoutFile := tempOutputFile(t)
stderrFile := tempOutputFile(t)
code := run([]string{"capabilities", "--json"}, stdoutFile, stderrFile)
if code != 0 {
t.Fatalf("capabilities exit code = %d; stderr=%s", code, readOutputFile(t, stderrFile))
}
var payload struct {
SchemaVersion int `json:"schema_version"`
ProtocolVersion int `json:"protocol_version"`
Commands []string `json:"commands"`
}
if err := json.Unmarshal(readOutputFile(t, stdoutFile), &payload); err != nil {
t.Fatalf("capabilities output is not JSON: %v", err)
}
if payload.SchemaVersion == 0 || payload.ProtocolVersion == 0 {
t.Fatalf("capabilities versions missing: %+v", payload)
}
for _, want := range []string{"run.submit", "run.status", "run.list", "run.cancel", "review.decide", "doctor"} {
found := false
for _, got := range payload.Commands {
if got == want {
found = true
}
}
if !found {
t.Fatalf("capabilities missing command %q: %+v", want, payload.Commands)
}
}
_ = stdout
_ = stderr
}

func TestObsoleteStatusCommandIsRejected(t *testing.T) {
stdoutFile := tempOutputFile(t)
stderrFile := tempOutputFile(t)
code := run([]string{"status", "--json"}, stdoutFile, stderrFile)
if code != 2 {
t.Fatalf("obsolete status exit code = %d, want 2; stderr=%s", code, readOutputFile(t, stderrFile))
}
}

func TestStatusJSONReportsCurrentStageFromOrderedState(t *testing.T) {
report := newStatusReport(daemon.RunSnapshot{
ID: "run-current-stage",
Stages: []daemon.StageResult{{Name: "intent", Result: "pass"}, {Name: "review", Result: "pending"}},
})
data, err := json.Marshal(report)
if err != nil {
t.Fatalf("marshal status: %v", err)
}
if !strings.Contains(string(data), `"current_stage":"review"`) {
t.Fatalf("status omitted current stage: %s", data)
}
}

func tempOutputFile(t *testing.T) *os.File {
t.Helper()
file, err := os.CreateTemp(t.TempDir(), "output")
if err != nil {
t.Fatalf("CreateTemp: %v", err)
}
return file
}

func readOutputFile(t *testing.T, file *os.File) []byte {
t.Helper()
if _, err := file.Seek(0, 0); err != nil {
t.Fatalf("seek output: %v", err)
}
data, err := os.ReadFile(file.Name())
if err != nil {
t.Fatalf("read output: %v", err)
}
return data
}
63 changes: 50 additions & 13 deletions cmd/made/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,11 @@ func startDaemon(ctx context.Context, home, lockPath string, idle time.Duration,
done <- err
return rm, done
}
reviewStore := daemon.NewReviewDecisions()
reviewStore := daemon.NewReviewDecisionsForManager(rm)
admission := &sync.Mutex{}
runCtx, cancelRun := context.WithCancel(ctx)
srv := api.NewServer(socketPath)
registerDaemonHandlers(srv, rm, reviewStore, spool, cancelRun, admission)
registerDaemonHandlers(srv, rm, reviewStore, spool, cancelRun, home, admission)

done := make(chan error, 1)

Expand Down Expand Up @@ -260,14 +260,14 @@ func isTerminalRunStatus(s daemon.RunStatus) bool {

const debugHandlersEnv = "MADE_DEBUG_HANDLERS"

func registerDaemonHandlers(srv *api.Server, rm *daemon.RunManager, store *daemon.ReviewDecisions, spool *daemon.GateSpool, cancel context.CancelFunc, admission ...*sync.Mutex) {
func registerDaemonHandlers(srv *api.Server, rm *daemon.RunManager, store *daemon.ReviewDecisions, spool *daemon.GateSpool, cancel context.CancelFunc, home string, admission ...*sync.Mutex) {
srv.Handle("run.status", runStatusHandler(rm))
srv.Handle("run.submit", runSubmitHandler(rm, store, spool, admission...))
srv.Handle("run.list", runListHandler(rm))
srv.Handle("run.cancel", runCancelHandler(rm))
srv.Handle("review.decide", reviewDecideRunHandler(rm, store))
srv.Handle("daemon.shutdown", daemonShutdownHandler(rm, spool, cancel, admission...))
srv.Handle("gate.admitPush", gateAdmitPushHandler())
srv.Handle("gate.admitPush", gateAdmitPushHandler(home))
srv.Handle("gate.notifyPush", gateNotifyPushHandler(rm, store, spool, admission...))
if os.Getenv(debugHandlersEnv) == "1" {
srv.Handle("debug.submitCancellableRun", debugSubmitCancellableRunHandler(rm))
Expand All @@ -290,7 +290,7 @@ type gateAdmitPushResult struct {
// daemon recognizes" - a real, valid bare repo on disk. It deliberately does
// not touch RunManager; creating a run is the orchestrator's job, not
// admission's.
func gateAdmitPushHandler() api.HandlerFunc {
func gateAdmitPushHandler(home string) api.HandlerFunc {
return func(_ context.Context, params json.RawMessage) (any, error) {
var p gateAdmitPushParams
if err := decodeStrictParams(params, &p); err != nil {
Expand All @@ -299,13 +299,42 @@ func gateAdmitPushHandler() api.HandlerFunc {
if p.GatePath == "" {
return nil, fmt.Errorf("gate.admitPush: gate_path is required")
}
if err := validateManagedGatePath(home, p.GatePath); err != nil {
return nil, fmt.Errorf("gate.admitPush: %w", err)
}
if err := validateBareGateRepo(p.GatePath); err != nil {
return nil, fmt.Errorf("gate.admitPush: %w", err)
}
return gateAdmitPushResult{OK: true}, nil
}
}

func validateManagedGatePath(home, gatePath string) error {
homeResolved, err := filepath.EvalSymlinks(home)
if err != nil {
return fmt.Errorf("resolve Made home: %w", err)
}
gateResolved, err := filepath.EvalSymlinks(gatePath)
if err != nil {
return fmt.Errorf("resolve gate path: %w", err)
}
rel, err := filepath.Rel(homeResolved, gateResolved)
if err != nil {
return fmt.Errorf("relate gate to Made home: %w", err)
}
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) != 3 || parts[0] != "gates" || parts[2] != "gate.git" {
return fmt.Errorf("gate path must be a managed MADE_HOME/gates/<hash>/gate.git path")
}
if len(parts[1]) != 64 || !hexString(parts[1]) {
return fmt.Errorf("gate path hash is not lowercase hexadecimal")
}
if filepath.Clean(gateResolved) != filepath.Clean(filepath.Join(homeResolved, rel)) {
return fmt.Errorf("gate path must not contain symlinks")
}
return nil
}

func validateBareGateRepo(path string) error {
info, err := os.Stat(path)
if err != nil {
Expand Down Expand Up @@ -337,13 +366,14 @@ func validateBareGateRepo(path string) error {
const gateNotifyPushDefaultBranchTimeout = 10 * time.Second

type gateNotifyPushParams struct {
GatePath string `json:"gate_path"`
OldSHA string `json:"old_sha"`
NewSHA string `json:"new_sha"`
Ref string `json:"ref"`
RunID string `json:"run_id,omitempty"`
OutputSHA string `json:"output_sha,omitempty"`
Replay bool `json:"replay,omitempty"`
GatePath string `json:"gate_path"`
OldSHA string `json:"old_sha"`
NewSHA string `json:"new_sha"`
Ref string `json:"ref"`
RunID string `json:"run_id,omitempty"`
OutputSHA string `json:"output_sha,omitempty"`
SubmissionID string `json:"submission_id,omitempty"`
Replay bool `json:"replay,omitempty"`
}

type gateNotifyPushResult struct {
Expand Down Expand Up @@ -453,7 +483,14 @@ func gateNotifyPushHandler(rm *daemon.RunManager, reviewDecisions *daemon.Review
orchestrator.NewWorkFunc(rm, reviewDecisions, emit, runID, defaultBranch, branch, orchestrator.Options{}))
}

snapshot, err := rm.SubmitWithMetadata(runID, repo, branch, p.NewSHA, p.OutputSHA, work)
submissionID := p.SubmissionID
if submissionID == "" {
submissionID = p.Ref + "@" + p.NewSHA
}
snapshot, err := rm.SubmitSubmission(daemon.RunSubmission{
ID: runID, Repo: repo, Branch: branch, Ref: p.Ref, OldSHA: p.OldSHA,
InputSHA: p.NewSHA, OutputSHA: p.OutputSHA, SubmissionID: submissionID, GatePath: p.GatePath,
}, work)
if err != nil {
return nil, fmt.Errorf("gate.notifyPush: submit run: %w", err)
}
Expand Down
5 changes: 4 additions & 1 deletion cmd/made/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ func TestDaemonStop_CancelsInFlightRunBeforeProcessExits(t *testing.T) {
}
for scanner.Scan() {
}
if err := scanner.Err(); err != nil {
return
}
}()

select {
Expand All @@ -70,7 +73,7 @@ func TestDaemonStop_CancelsInFlightRunBeforeProcessExits(t *testing.T) {

socketPath := api.SocketPath(home)
var client *api.Client
for i := 0; i < 200; i++ {
for range 200 {
client, err = api.Dial(socketPath)
if err == nil {
break
Expand Down
18 changes: 16 additions & 2 deletions cmd/made/gate_admit_push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestGateAdmitPushRPC_ValidBareRepoAdmitted(t *testing.T) {
home := shortTempDir(t)
_, client := startTestDaemon(t, home)

barePath := filepath.Join(shortTempDir(t), "gate.git")
barePath := gitgate.GatePath(home, "fixture/repo")
if err := gitgate.InitBare(barePath); err != nil {
t.Fatalf("InitBare: %v", err)
}
Expand All @@ -58,6 +58,20 @@ func TestGateAdmitPushRPC_ValidBareRepoAdmitted(t *testing.T) {
}
}

func TestGateAdmitPushRPC_RejectsBareRepoOutsideMadeHome(t *testing.T) {
home := shortTempDir(t)
_, client := startTestDaemon(t, home)

barePath := filepath.Join(shortTempDir(t), "unmanaged.git")
if err := gitgate.InitBare(barePath); err != nil {
t.Fatalf("InitBare: %v", err)
}

if _, err := client.Call("gate.admitPush", gateAdmitPushParams{GatePath: barePath}); err == nil {
t.Fatal("gate.admitPush accepted a bare repository outside MADE_HOME")
}
}

func TestGateAdmitPushRPC_InvalidPathRejected(t *testing.T) {
home := shortTempDir(t)
_, client := startTestDaemon(t, home)
Expand All @@ -84,7 +98,7 @@ func TestGateAdmitPushCLI_ValidGateExitsZero(t *testing.T) {
t.Setenv("MADE_HOME", home)
_, _ = startTestDaemon(t, home)

barePath := filepath.Join(shortTempDir(t), "gate.git")
barePath := gitgate.GatePath(home, "fixture/repo")
if err := gitgate.InitBare(barePath); err != nil {
t.Fatalf("InitBare: %v", err)
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/made/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ func run(args []string, stdout, stderr *os.File) int {
return runCapabilitiesCommand(args[1:], stdout, stderr)
case "run":
return runRunCommand(args[1:], stdout, stderr)
case "status":
_, _ = fmt.Fprintln(stderr, "made: status is obsolete; use made run status --json <exact-run-id>")
return 2
case "daemon":
return runDaemonCommand(args[1:], stdout, stderr)
case "review":
Expand Down
3 changes: 1 addition & 2 deletions cmd/made/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ func reviewDecideRunHandler(rm *daemon.RunManager, store *daemon.ReviewDecisions
if _, ok := rm.Snapshot(p.RunID); !ok {
return nil, fmt.Errorf("review.decide: exact run_id %q was not found", p.RunID)
}
if err := rm.SetDecision(p.RunID, p.Stage, p.Decision); err != nil {
if err := store.Set(p.RunID, p.Stage, p.Decision); err != nil {
return nil, err
}
store.Set(p.RunID, p.Stage, p.Decision)
return reviewDecisionReport{
SchemaVersion: 1, ProtocolVersion: api.Version,
RunID: p.RunID, Stage: p.Stage, Decision: p.Decision,
Expand Down
38 changes: 38 additions & 0 deletions cmd/made/run_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"os"
"testing"
)

func TestRunStatusRejectsUnsupportedTrailingArgument(t *testing.T) {
stdout, stderr := discardOutput(t)
if code := runExactStatusCommand([]string{"run-1", "unexpected"}, stdout, stderr); code != 2 {
t.Fatalf("run status exit code = %d, want 2", code)
}
}

func TestRunCancelRejectsUnsupportedTrailingArgument(t *testing.T) {
stdout, stderr := discardOutput(t)
if code := runCancelCommand([]string{"run-1", "unexpected"}, stdout, stderr); code != 2 {
t.Fatalf("run cancel exit code = %d, want 2", code)
}
}

func discardOutput(t *testing.T) (stdout, stderr *os.File) {
t.Helper()
stdout, err := os.Open(os.DevNull)
if err != nil {
t.Fatalf("open stdout discard: %v", err)
}
stderr, err = os.Open(os.DevNull)
if err != nil {
_ = stdout.Close()
t.Fatalf("open stderr discard: %v", err)
}
t.Cleanup(func() {
_ = stdout.Close()
_ = stderr.Close()
})
return stdout, stderr
}
Loading
Loading