diff --git a/.core/reference/docs/RFC.md b/.core/reference/docs/RFC.md index 4fe1d73..3954989 100644 --- a/.core/reference/docs/RFC.md +++ b/.core/reference/docs/RFC.md @@ -21,7 +21,7 @@ core/go provides the primitives. core/agent composes them. cmd/core-agent/main.go — entry point: core.New + Run pkg/agentic/ — orchestration (dispatch, prep, verify, scan, commands) pkg/agentic/actions.go — named Action handlers (ctx, Options) → Result -pkg/agentic/proc.go — process helpers via s.Core().Process() +pkg/agentic/pid.go — PID lifecycle helpers pkg/agentic/handlers.go — IPC completion pipeline handlers pkg/agentic/status.go — workspace status (WriteAtomic + JSONMarshalString) pkg/agentic/paths.go — paths, fs (NewUnrestricted), helpers @@ -145,23 +145,13 @@ All handlers use `c.ACTION(messages.X{})` — no ChannelNotifier, no callbacks. All commands via `s.Core().Process()`. Returns `core.Result` — Value is always a string. ```go -func (s *PrepSubsystem) runCmd(ctx context.Context, dir, command string, args ...string) core.Result { - return s.Core().Process().RunIn(ctx, dir, command, args...) +process := s.Core().Process() +r := process.RunIn(ctx, dir, "git", "log", "--oneline", "-20") +if r.OK { + output := core.Trim(r.Value.(string)) } -func (s *PrepSubsystem) runCmdOK(ctx context.Context, dir, command string, args ...string) bool { - return s.runCmd(ctx, dir, command, args...).OK -} - -func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result { - return s.runCmd(ctx, dir, "git", args...) -} - -func (s *PrepSubsystem) gitOutput(ctx context.Context, dir string, args ...string) string { - r := s.gitCmd(ctx, dir, args...) - if !r.OK { return "" } - return core.Trim(r.Value.(string)) -} +r = process.RunWithEnv(ctx, dir, []string{"GOWORK=off"}, "go", "test", "./...") ``` go-process is fully Result-native. `Start`, `Run`, `StartWithOptions`, `RunWithOptions` all return `core.Result`. Value is `*Process` for Start, `string` for Run. OK=true guarantees the type. @@ -409,10 +399,9 @@ c.Drive().New(core.NewOptions( Every exported function MUST have a usage-example comment: ```go -// gitCmd runs a git command in a directory. +// Process runs a git command in a directory. // -// r := s.gitCmd(ctx, "/repo", "log", "--oneline") -func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result { +// r := s.Core().Process().RunIn(ctx, "/repo", "git", "log", "--oneline") ``` --- @@ -436,6 +425,7 @@ func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) - 2026-03-29: cmd/core-agent no longer rewrites `os.Args` before startup. The binary-owned commands now use named handlers, keeping the entrypoint on Core CLI primitives instead of repo-local argument mutation. - 2026-03-30: transport helpers preserve request and read causes, brain direct API calls surface upstream bodies, and review queue retry parsing no longer uses `MustCompile`. +- 2026-03-30: direct Core process calls replaced the `proc.go` wrapper layer; PID helpers now live in `pid.go` and the workspace template documents `c.Process()` directly. - 2026-03-26: net/http consolidated to transport.go (ONE file). net/url + io/fs eliminated. RFC-025 updated with 3 new quality gates (net/http, net/url, io/fs). 1:1 test + example test coverage. Array[T].Deduplicate replaces custom helpers. - 2026-03-25: Quality gates pass. Zero disallowed imports (all 10). encoding/json→Core JSON. path/filepath→Core Path. os→Core Env/Fs. io→Core ReadAll/WriteAll. go-process fully Result-native. ServiceRuntime on all subsystems. 22 named Actions + Task pipeline. ChannelNotifier→IPC. Reference docs synced. - 2026-03-25: Initial spec — written with full core/go v0.8.0 domain context. diff --git a/AGENTS.md b/AGENTS.md index 68ea455..6f5ca53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,7 +59,7 @@ dispatch → prep workspace → spawn agent in Docker container - **UK English**: colour, organisation, centre, initialise - **Errors**: `core.E("pkg.Method", "message", err)` — NEVER `fmt.Errorf` - **File I/O**: Package-level `fs` (go-io Medium) — NEVER `os.ReadFile/WriteFile` -- **Processes**: `proc.go` helpers (go-process) — NEVER `os/exec` directly +- **Processes**: `s.Core().Process()` / go-process — NEVER `os/exec` directly - **Strings**: `core.Contains/Split/Trim/HasPrefix/Sprintf` — NEVER `strings.*` - **Returns**: `core.Result{Value, OK}` — NEVER `(value, error)` pairs - **Comments**: Usage examples showing HOW with real values, not descriptions @@ -86,12 +86,14 @@ One test file per source file. No catch-all files. Names must sort cleanly. ## Process Execution -All external commands go through `pkg/agentic/proc.go` → go-process: +All external commands go through `s.Core().Process()` → go-process: ```go -out, err := runCmd(ctx, dir, "git", "log", "--oneline") -ok := gitCmdOK(ctx, dir, "fetch", "origin", "main") -branch := gitOutput(ctx, dir, "rev-parse", "--abbrev-ref", "HEAD") +process := s.Core().Process() +out := process.RunIn(ctx, dir, "git", "log", "--oneline") +ok := process.RunIn(ctx, dir, "git", "fetch", "origin", "main").OK +branchResult := process.RunIn(ctx, dir, "git", "rev-parse", "--abbrev-ref", "HEAD") +branch := core.Trim(branchResult.Value.(string)) ``` **NEVER import `os/exec`.** Zero source files do. diff --git a/CODEX.md b/CODEX.md index 19cbc1d..a42fe38 100644 --- a/CODEX.md +++ b/CODEX.md @@ -42,7 +42,7 @@ c.Run() - Use UK English in comments and docs. - Use `core.E("pkg.Method", "message", err)` for errors. Never use `fmt.Errorf` or `errors.New`. - Use Core filesystem helpers or package-level `fs`. Never use raw `os.ReadFile`, `os.WriteFile`, or `filepath.*`. -- Route external commands through `pkg/agentic/proc.go` or `s.Core().Process()`. Never import `os/exec`. +- Route external commands through `s.Core().Process()`. Never import `os/exec`. - Use Core string helpers such as `core.Contains`, `core.Trim`, and `core.Split` instead of `strings.*`. - Prefer `core.Result{Value: x, OK: true}` over `(value, error)` return pairs in Core-facing code. - Comments should show real usage examples, not restate the signature. diff --git a/docs/RFC.md b/docs/RFC.md index 29c5ffb..1e9323c 100644 --- a/docs/RFC.md +++ b/docs/RFC.md @@ -21,7 +21,7 @@ core/go provides the primitives. core/agent composes them. cmd/core-agent/main.go — entry point: core.New + Run pkg/agentic/ — orchestration (dispatch, prep, verify, scan, commands) pkg/agentic/actions.go — named Action handlers (ctx, Options) → Result -pkg/agentic/proc.go — process helpers via s.Core().Process() +pkg/agentic/pid.go — PID lifecycle helpers pkg/agentic/handlers.go — IPC completion pipeline handlers pkg/agentic/status.go — workspace status (WriteAtomic + JSONMarshalString) pkg/agentic/paths.go — paths, fs (NewUnrestricted), helpers @@ -145,23 +145,13 @@ All handlers use `c.ACTION(messages.X{})` — no ChannelNotifier, no callbacks. All commands via `s.Core().Process()`. Returns `core.Result` — Value is always a string. ```go -func (s *PrepSubsystem) runCmd(ctx context.Context, dir, command string, args ...string) core.Result { - return s.Core().Process().RunIn(ctx, dir, command, args...) +process := s.Core().Process() +r := process.RunIn(ctx, dir, "git", "log", "--oneline", "-20") +if r.OK { + output := core.Trim(r.Value.(string)) } -func (s *PrepSubsystem) runCmdOK(ctx context.Context, dir, command string, args ...string) bool { - return s.runCmd(ctx, dir, command, args...).OK -} - -func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result { - return s.runCmd(ctx, dir, "git", args...) -} - -func (s *PrepSubsystem) gitOutput(ctx context.Context, dir string, args ...string) string { - r := s.gitCmd(ctx, dir, args...) - if !r.OK { return "" } - return core.Trim(r.Value.(string)) -} +r = process.RunWithEnv(ctx, dir, []string{"GOWORK=off"}, "go", "test", "./...") ``` go-process is fully Result-native. `Start`, `Run`, `StartWithOptions`, `RunWithOptions` all return `core.Result`. Value is `*Process` for Start, `string` for Run. OK=true guarantees the type. @@ -409,10 +399,9 @@ c.Drive().New(core.NewOptions( Every exported function MUST have a usage-example comment: ```go -// gitCmd runs a git command in a directory. +// Process runs a git command in a directory. // -// r := s.gitCmd(ctx, "/repo", "log", "--oneline") -func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result { +// r := s.Core().Process().RunIn(ctx, "/repo", "git", "log", "--oneline") ``` --- @@ -435,6 +424,7 @@ func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) ## Changelog - 2026-03-30: transport helpers preserve request and read causes, brain direct API calls surface upstream bodies, and review queue retry parsing no longer uses `MustCompile`. +- 2026-03-30: direct Core process calls replaced the `proc.go` wrapper layer; PID helpers now live in `pid.go` and the workspace template documents `c.Process()` directly. - 2026-03-30: main now logs startup failures with structured context, and the workspace contract reference restored usage-example comments for the Action lifecycle messages. - 2026-03-30: plan IDs now come from core.ID(), workspace prep validates org/repo names with core.ValidateName, and plan paths use core.SanitisePath. - 2026-03-29: cmd/core-agent no longer rewrites `os.Args` before startup. The binary-owned commands now use named handlers, keeping the entrypoint on Core CLI primitives instead of repo-local argument mutation. diff --git a/docs/RFC.plan.md b/docs/RFC.plan.md index ce99d49..46678fa 100644 --- a/docs/RFC.plan.md +++ b/docs/RFC.plan.md @@ -24,7 +24,7 @@ After loading all three, you have the full domain model: After loading, you should be able to answer without looking at code: - What does `c.Action("agentic.dispatch").Run(ctx, opts)` do? -- Why is `proc.go` being deleted? +- How do direct `s.Core().Process()` calls replace the old process wrapper layer? - What replaces the ACTION cascade in `handlers.go`? - Which imports are disallowed and what replaces each one? - What does `c.Entitled("agentic.concurrency", 1)` check? @@ -43,7 +43,7 @@ Priority order: 5. Replace `core.ID()` (plan.go) 6. Register capabilities as named Actions (OnStartup) 7. Replace ACTION cascade with Task pipeline (handlers.go) -8. Delete `proc.go` → `s.Core().Process()` (after go-process v0.8.0) +8. Use `s.Core().Process()` directly in call sites. The old `proc.go` wrapper layer has been removed. 9. AX-7 test rename + gap fill 10. Example tests per source file diff --git a/docs/plans/2026-03-25-core-go-v0.8.0-migration.md b/docs/plans/2026-03-25-core-go-v0.8.0-migration.md index 40fac06..6d282a2 100644 --- a/docs/plans/2026-03-25-core-go-v0.8.0-migration.md +++ b/docs/plans/2026-03-25-core-go-v0.8.0-migration.md @@ -2,6 +2,8 @@ > Written by Cladius with full core/go + core/agent domain context (2026-03-25). > Read core/go docs/RFC.md for the full spec. This plan covers what core/agent needs to change. +> +> Status note: the proc.go migration described below has shipped. core/agent now uses direct `s.Core().Process()` calls and `pid.go` for PID helpers. Keep this file as the original migration record. ## What Changed in core/go diff --git a/pkg/agentic/auto_pr.go b/pkg/agentic/auto_pr.go index 3188e9a..51f3cf4 100644 --- a/pkg/agentic/auto_pr.go +++ b/pkg/agentic/auto_pr.go @@ -19,11 +19,16 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) { ctx := context.Background() repoDir := WorkspaceRepoDir(wsDir) + process := s.Core().Process() // PRs target dev — agents never merge directly to main base := "dev" - out := s.gitOutput(ctx, repoDir, "log", "--oneline", core.Concat("origin/", base, "..HEAD")) + r := process.RunIn(ctx, repoDir, "git", "log", "--oneline", core.Concat("origin/", base, "..HEAD")) + if !r.OK { + return + } + out := core.Trim(r.Value.(string)) if out == "" { return } @@ -37,7 +42,7 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) { // Push the branch to forge forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo) - if !s.gitCmdOK(ctx, repoDir, "push", forgeRemote, st.Branch) { + if !process.RunIn(ctx, repoDir, "git", "push", forgeRemote, st.Branch).OK { if st2, err := ReadStatus(wsDir); err == nil { st2.Question = "PR push failed" writeStatus(wsDir, st2) diff --git a/pkg/agentic/deps.go b/pkg/agentic/deps.go index 0fbb69e..035c70d 100644 --- a/pkg/agentic/deps.go +++ b/pkg/agentic/deps.go @@ -24,12 +24,15 @@ func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, wsDir, repoDir, if !r.OK { return // no go.mod — not a Go project } - // Parse requires from go.mod deps := parseCoreDeps(r.Value.(string)) if len(deps) == 0 { return } + if s.ServiceRuntime == nil { + return + } + process := s.Core().Process() // Deduplicate (dappco.re and forge.lthn.ai may map to same repo) dedupSeen := make(map[string]bool) @@ -52,7 +55,7 @@ func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, wsDir, repoDir, } repoURL := forgeSSHURL(org, dep.repo) - if result := s.gitCmd(ctx, wsDir, "clone", "--depth=1", repoURL, dep.dir); result.OK { + if result := process.RunIn(ctx, wsDir, "git", "clone", "--depth=1", repoURL, dep.dir); result.OK { cloned = append(cloned, dep.dir) } } diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index 389292e..a1f144b 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -443,6 +443,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, er func (s *PrepSubsystem) runQA(wsDir string) bool { ctx := context.Background() repoDir := WorkspaceRepoDir(wsDir) + process := s.Core().Process() if fs.IsFile(core.JoinPath(repoDir, "go.mod")) { for _, args := range [][]string{ @@ -450,7 +451,7 @@ func (s *PrepSubsystem) runQA(wsDir string) bool { {"go", "vet", "./..."}, {"go", "test", "./...", "-count=1", "-timeout", "120s"}, } { - if !s.runCmdOK(ctx, repoDir, args[0], args[1:]...) { + if !process.RunIn(ctx, repoDir, args[0], args[1:]...).OK { core.Warn("QA failed", "cmd", core.Join(" ", args...)) return false } @@ -459,17 +460,17 @@ func (s *PrepSubsystem) runQA(wsDir string) bool { } if fs.IsFile(core.JoinPath(repoDir, "composer.json")) { - if !s.runCmdOK(ctx, repoDir, "composer", "install", "--no-interaction") { + if !process.RunIn(ctx, repoDir, "composer", "install", "--no-interaction").OK { return false } - return s.runCmdOK(ctx, repoDir, "composer", "test") + return process.RunIn(ctx, repoDir, "composer", "test").OK } if fs.IsFile(core.JoinPath(repoDir, "package.json")) { - if !s.runCmdOK(ctx, repoDir, "npm", "install") { + if !process.RunIn(ctx, repoDir, "npm", "install").OK { return false } - return s.runCmdOK(ctx, repoDir, "npm", "test") + return process.RunIn(ctx, repoDir, "npm", "test").OK } return true diff --git a/pkg/agentic/mirror.go b/pkg/agentic/mirror.go index 2164701..17fccaa 100644 --- a/pkg/agentic/mirror.go +++ b/pkg/agentic/mirror.go @@ -54,6 +54,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu if maxFiles <= 0 { maxFiles = 50 } + process := s.Core().Process() basePath := s.codePath if basePath == "" { @@ -83,7 +84,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu } // Fetch github to get current state - s.gitCmdOK(ctx, repoDir, "fetch", "github") + process.RunIn(ctx, repoDir, "git", "fetch", "github") // Check how far ahead local default branch is vs github localBase := s.DefaultBranch(repoDir) @@ -119,7 +120,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu // Push local main to github dev (explicit main, not HEAD) base := s.DefaultBranch(repoDir) - if r := s.gitCmd(ctx, repoDir, "push", "github", core.Concat(base, ":refs/heads/dev"), "--force"); !r.OK { + if r := process.RunIn(ctx, repoDir, "git", "push", "github", core.Concat(base, ":refs/heads/dev"), "--force"); !r.OK { sync.Skipped = core.Sprintf("push failed: %s", r.Value) synced = append(synced, sync) continue @@ -148,9 +149,10 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu // createGitHubPR creates a PR from dev → main using the gh CLI. func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string, commits, files int) (string, error) { ghRepo := core.Sprintf("%s/%s", GitHubOrg(), repo) + process := s.Core().Process() // Check if there's already an open PR from dev - r := s.runCmd(ctx, repoDir, "gh", "pr", "list", "--repo", ghRepo, "--head", "dev", "--state", "open", "--json", "url", "--limit", "1") + r := process.RunIn(ctx, repoDir, "gh", "pr", "list", "--repo", ghRepo, "--head", "dev", "--state", "open", "--json", "url", "--limit", "1") if r.OK { out := r.Value.(string) if core.Contains(out, "url") { @@ -166,7 +168,7 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string title := core.Sprintf("[sync] %s: %d commits, %d files", repo, commits, files) - r = s.runCmd(ctx, repoDir, "gh", "pr", "create", + r = process.RunIn(ctx, repoDir, "gh", "pr", "create", "--repo", ghRepo, "--head", "dev", "--base", "main", "--title", title, "--body", body) if !r.OK { @@ -183,23 +185,31 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string // ensureDevBranch creates the dev branch on GitHub if it doesn't exist. func (s *PrepSubsystem) ensureDevBranch(repoDir string) { - s.gitCmdOK(context.Background(), repoDir, "push", "github", "HEAD:refs/heads/dev") + s.Core().Process().RunIn(context.Background(), repoDir, "git", "push", "github", "HEAD:refs/heads/dev") } // hasRemote checks if a git remote exists. func (s *PrepSubsystem) hasRemote(repoDir, name string) bool { - return s.gitCmdOK(context.Background(), repoDir, "remote", "get-url", name) + return s.Core().Process().RunIn(context.Background(), repoDir, "git", "remote", "get-url", name).OK } // commitsAhead returns how many commits HEAD is ahead of the ref. func (s *PrepSubsystem) commitsAhead(repoDir, base, head string) int { - out := s.gitOutput(context.Background(), repoDir, "rev-list", core.Concat(base, "..", head), "--count") + r := s.Core().Process().RunIn(context.Background(), repoDir, "git", "rev-list", core.Concat(base, "..", head), "--count") + if !r.OK { + return 0 + } + out := core.Trim(r.Value.(string)) return parseInt(out) } // filesChanged returns the number of files changed between two refs. func (s *PrepSubsystem) filesChanged(repoDir, base, head string) int { - out := s.gitOutput(context.Background(), repoDir, "diff", "--name-only", core.Concat(base, "..", head)) + r := s.Core().Process().RunIn(context.Background(), repoDir, "git", "diff", "--name-only", core.Concat(base, "..", head)) + if !r.OK { + return 0 + } + out := core.Trim(r.Value.(string)) if out == "" { return 0 } diff --git a/pkg/agentic/paths.go b/pkg/agentic/paths.go index fef6505..f463677 100644 --- a/pkg/agentic/paths.go +++ b/pkg/agentic/paths.go @@ -156,14 +156,16 @@ func AgentName() string { // base := s.DefaultBranch("./src") func (s *PrepSubsystem) DefaultBranch(repoDir string) string { ctx := context.Background() - if ref := s.gitOutput(ctx, repoDir, "symbolic-ref", "refs/remotes/origin/HEAD", "--short"); ref != "" { + process := s.Core().Process() + if r := process.RunIn(ctx, repoDir, "git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short"); r.OK { + ref := core.Trim(r.Value.(string)) if core.HasPrefix(ref, "origin/") { return core.TrimPrefix(ref, "origin/") } return ref } for _, branch := range []string{"main", "master"} { - if s.gitCmdOK(ctx, repoDir, "rev-parse", "--verify", branch) { + if process.RunIn(ctx, repoDir, "git", "rev-parse", "--verify", branch).OK { return branch } } diff --git a/pkg/agentic/pid.go b/pkg/agentic/pid.go new file mode 100644 index 0000000..d077165 --- /dev/null +++ b/pkg/agentic/pid.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import "syscall" + +// PIDAlive checks if an OS process is still alive via PID signal check. +// +// if agentic.PIDAlive(pid) { ... } +func PIDAlive(pid int) bool { + if pid > 0 { + return syscall.Kill(pid, 0) == nil + } + return false +} + +// PIDTerminate terminates a process via SIGTERM. +// +// if agentic.PIDTerminate(pid) { ... } +func PIDTerminate(pid int) bool { + if pid > 0 { + return syscall.Kill(pid, syscall.SIGTERM) == nil + } + return false +} diff --git a/pkg/agentic/pid_example_test.go b/pkg/agentic/pid_example_test.go new file mode 100644 index 0000000..19e1764 --- /dev/null +++ b/pkg/agentic/pid_example_test.go @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "os" + + core "dappco.re/go/core" +) + +func ExamplePIDAlive() { + core.Println(PIDAlive(os.Getpid())) + // Output: true +} + +func ExamplePIDTerminate() { + core.Println(PIDTerminate(0)) + // Output: false +} diff --git a/pkg/agentic/pid_test.go b/pkg/agentic/pid_test.go new file mode 100644 index 0000000..b76aad8 --- /dev/null +++ b/pkg/agentic/pid_test.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "context" + "os" + "strconv" + "testing" + "time" + + core "dappco.re/go/core" + "dappco.re/go/core/process" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testPrep is the package-level PrepSubsystem for tests that need process execution. +var testPrep *PrepSubsystem + +// testCore is the package-level Core with go-process registered. +var testCore *core.Core + +// TestMain sets up a PrepSubsystem with go-process registered for all tests in the package. +func TestMain(m *testing.M) { + testCore = core.New( + core.WithService(ProcessRegister), + ) + testCore.ServiceStartup(context.Background(), nil) + + // Enable pipeline feature flags (matches Register defaults) + testCore.Config().Enable("auto-qa") + testCore.Config().Enable("auto-pr") + testCore.Config().Enable("auto-merge") + testCore.Config().Enable("auto-ingest") + + testPrep = &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + os.Exit(m.Run()) +} + +// newPrepWithProcess creates a PrepSubsystem wired to testCore for tests that +// need process execution via s.Core().Process(). +func newPrepWithProcess() *PrepSubsystem { + return &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } +} + +// --- PIDAlive --- + +func TestPid_PIDAlive_Good(t *testing.T) { + pid, _ := strconv.Atoi(core.Env("PID")) + assert.True(t, PIDAlive(pid)) +} + +func TestPid_PIDAlive_Bad(t *testing.T) { + assert.False(t, PIDAlive(999999)) +} + +func TestPid_PIDAlive_Ugly(t *testing.T) { + assert.False(t, PIDAlive(0)) +} + +// --- PIDTerminate --- + +func TestPid_PIDTerminate_Good(t *testing.T) { + r := testCore.Process().Start(context.Background(), core.NewOptions( + core.Option{Key: "command", Value: "sleep"}, + core.Option{Key: "args", Value: []string{"30"}}, + core.Option{Key: "detach", Value: true}, + )) + require.True(t, r.OK) + + proc, ok := r.Value.(*process.Process) + require.True(t, ok) + + pid := proc.Info().PID + require.NotZero(t, pid) + + defer func() { + _ = proc.Kill() + }() + + assert.True(t, PIDTerminate(pid)) + + select { + case <-proc.Done(): + case <-time.After(5 * time.Second): + t.Fatal("PIDTerminate did not stop the process") + } + + assert.False(t, PIDAlive(pid)) +} + +func TestPid_PIDTerminate_Bad(t *testing.T) { + assert.False(t, PIDTerminate(999999)) +} + +func TestPid_PIDTerminate_Ugly(t *testing.T) { + assert.False(t, PIDTerminate(0)) +} diff --git a/pkg/agentic/pr.go b/pkg/agentic/pr.go index d997978..5747094 100644 --- a/pkg/agentic/pr.go +++ b/pkg/agentic/pr.go @@ -65,11 +65,15 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in } if st.Branch == "" { - branch := s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD") - if branch == "" { + process := s.Core().Process() + r := process.RunIn(ctx, repoDir, "git", "rev-parse", "--abbrev-ref", "HEAD") + if !r.OK { + return nil, CreatePROutput{}, core.E("createPR", "failed to detect branch", nil) + } + st.Branch = core.Trim(r.Value.(string)) + if st.Branch == "" { return nil, CreatePROutput{}, core.E("createPR", "failed to detect branch", nil) } - st.Branch = branch } org := st.Org @@ -107,7 +111,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in // Push branch to Forge (origin is the local clone, not Forge) forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo) - r := s.gitCmd(ctx, repoDir, "push", forgeRemote, st.Branch) + r := s.Core().Process().RunIn(ctx, repoDir, "git", "push", forgeRemote, st.Branch) if !r.OK { return nil, CreatePROutput{}, core.E("createPR", core.Concat("git push failed: ", r.Value.(string)), nil) } diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index c535fc0..7f99433 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -431,6 +431,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // Source repo path — org and repo were validated by workspaceDirResult. repoPath := core.JoinPath(s.codePath, input.Org, input.Repo) + process := s.Core().Process() // Ensure meta directory exists if r := fs.EnsureDir(metaDir); !r.OK { @@ -445,17 +446,21 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // Preserve the current branch on resume. Pull it only if it exists on // origin; otherwise refresh the default branch refs without abandoning the // workspace branch. - currentBranch := s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD") + r := process.RunIn(ctx, repoDir, "git", "rev-parse", "--abbrev-ref", "HEAD") + currentBranch := "" + if r.OK { + currentBranch = core.Trim(r.Value.(string)) + } defaultBranch := s.DefaultBranch(repoDir) if currentBranch == "" || currentBranch == "HEAD" { currentBranch = defaultBranch } if currentBranch != "" { - s.gitCmd(ctx, repoDir, "checkout", currentBranch) - if s.gitCmdOK(ctx, repoDir, "ls-remote", "--exit-code", "--heads", "origin", currentBranch) { - s.gitCmd(ctx, repoDir, "pull", "--ff-only", "origin", currentBranch) + process.RunIn(ctx, repoDir, "git", "checkout", currentBranch) + if process.RunIn(ctx, repoDir, "git", "ls-remote", "--exit-code", "--heads", "origin", currentBranch).OK { + process.RunIn(ctx, repoDir, "git", "pull", "--ff-only", "origin", currentBranch) } else if defaultBranch != "" { - s.gitCmd(ctx, repoDir, "fetch", "origin", defaultBranch) + process.RunIn(ctx, repoDir, "git", "fetch", "origin", defaultBranch) } } } @@ -470,7 +475,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques if !resumed { // Clone repo into repo/ - if r := s.gitCmd(ctx, ".", "clone", repoPath, repoDir); !r.OK { + if r := process.RunIn(ctx, ".", "git", "clone", repoPath, repoDir); !r.OK { return nil, PrepOutput{}, core.E("prep", core.Concat("git clone failed for ", input.Repo), nil) } @@ -487,13 +492,16 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques } branchName := core.Sprintf("agent/%s", taskSlug) - if r := s.gitCmd(ctx, repoDir, "checkout", "-b", branchName); !r.OK { + if r := process.RunIn(ctx, repoDir, "git", "checkout", "-b", branchName); !r.OK { return nil, PrepOutput{}, core.E("prep.branch", core.Sprintf("failed to create branch %q", branchName), nil) } out.Branch = branchName } else { // Resume: read branch from existing checkout - out.Branch = s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD") + r := process.RunIn(ctx, repoDir, "git", "rev-parse", "--abbrev-ref", "HEAD") + if r.OK { + out.Branch = core.Trim(r.Value.(string)) + } } // Overwrite CODEX.md with language-specific version if needed. @@ -517,7 +525,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques if !fs.IsDir(docsDir) { docsRepo := core.JoinPath(s.codePath, input.Org, "docs") if fs.IsDir(core.JoinPath(docsRepo, ".git")) { - s.gitCmd(ctx, ".", "clone", "--depth", "1", docsRepo, docsDir) + process.RunIn(ctx, ".", "git", "clone", "--depth", "1", docsRepo, docsDir) } } @@ -789,7 +797,11 @@ func (s *PrepSubsystem) findConsumersList(repo string) (string, int) { } func (s *PrepSubsystem) getGitLog(repoPath string) string { - return s.gitOutput(context.Background(), repoPath, "log", "--oneline", "-20") + r := s.Core().Process().RunIn(context.Background(), repoPath, "git", "log", "--oneline", "-20") + if !r.OK { + return "" + } + return core.Trim(r.Value.(string)) } func (s *PrepSubsystem) pullWikiContent(ctx context.Context, org, repo string) string { diff --git a/pkg/agentic/proc.go b/pkg/agentic/proc.go deleted file mode 100644 index d4c1410..0000000 --- a/pkg/agentic/proc.go +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// Process execution helpers — routes all commands through s.Core().Process(). -// No direct os/exec or go-process imports. -// -// Requires go-process to be registered with Core via: -// -// core.New(core.WithService(agentic.ProcessRegister)) - -package agentic - -import ( - "context" - "syscall" - - core "dappco.re/go/core" -) - -// runCmd executes a command in a directory. Returns Result{Value: string, OK: bool}. -// -// r := s.runCmd(ctx, repoDir, "git", "log", "--oneline", "-20") -// if r.OK { output := r.Value.(string) } -func (s *PrepSubsystem) runCmd(ctx context.Context, dir string, command string, args ...string) core.Result { - return s.Core().Process().RunIn(ctx, dir, command, args...) -} - -// runCmdEnv executes a command with additional environment variables. -// -// r := s.runCmdEnv(ctx, repoDir, []string{"GOWORK=off"}, "go", "test", "./...") -func (s *PrepSubsystem) runCmdEnv(ctx context.Context, dir string, env []string, command string, args ...string) core.Result { - return s.Core().Process().RunWithEnv(ctx, dir, env, command, args...) -} - -// runCmdOK executes a command and returns true if it exits 0. -// -// if s.runCmdOK(ctx, repoDir, "go", "build", "./...") { ... } -func (s *PrepSubsystem) runCmdOK(ctx context.Context, dir string, command string, args ...string) bool { - return s.runCmd(ctx, dir, command, args...).OK -} - -// gitCmd runs a git command in the given directory. -// -// r := s.gitCmd(ctx, repoDir, "log", "--oneline", "-20") -func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result { - return s.runCmd(ctx, dir, "git", args...) -} - -// gitCmdOK runs a git command and returns true if it exits 0. -// -// if s.gitCmdOK(ctx, repoDir, "fetch", "origin", "main") { ... } -func (s *PrepSubsystem) gitCmdOK(ctx context.Context, dir string, args ...string) bool { - return s.gitCmd(ctx, dir, args...).OK -} - -// gitOutput runs a git command and returns trimmed stdout. -// -// branch := s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD") -func (s *PrepSubsystem) gitOutput(ctx context.Context, dir string, args ...string) string { - r := s.gitCmd(ctx, dir, args...) - if !r.OK { - return "" - } - return core.Trim(r.Value.(string)) -} - -// --- Process lifecycle helpers --- - -// PIDAlive checks if an OS process is still alive via PID signal check. -// -// if agentic.PIDAlive(st.PID) { ... } -func PIDAlive(pid int) bool { - if pid > 0 { - return syscall.Kill(pid, 0) == nil - } - return false -} - -// PIDTerminate terminates a process via SIGTERM. -// -// if agentic.PIDTerminate(st.PID) { ... } -func PIDTerminate(pid int) bool { - if pid > 0 { - return syscall.Kill(pid, syscall.SIGTERM) == nil - } - return false -} diff --git a/pkg/agentic/proc_example_test.go b/pkg/agentic/proc_example_test.go deleted file mode 100644 index 55a33e7..0000000 --- a/pkg/agentic/proc_example_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package agentic - -import ( - "context" - - core "dappco.re/go/core" -) - -func ExamplePrepSubsystem_runCmd() { - r := testPrep.runCmd(context.Background(), ".", "echo", "hello") - core.Println(r.OK) - // Output: true -} - -func ExamplePrepSubsystem_gitCmd() { - r := testPrep.gitCmd(context.Background(), ".", "--version") - core.Println(r.OK) - // Output: true -} - -func ExamplePrepSubsystem_gitOutput() { - version := testPrep.gitOutput(context.Background(), ".", "--version") - core.Println(core.HasPrefix(version, "git version")) - // Output: true -} - -func ExamplePrepSubsystem_runCmdOK() { - ok := testPrep.runCmdOK(context.Background(), ".", "echo", "test") - core.Println(ok) - // Output: true -} - -func ExamplePrepSubsystem_gitCmdOK() { - ok := testPrep.gitCmdOK(context.Background(), ".", "--version") - core.Println(ok) - // Output: true -} diff --git a/pkg/agentic/proc_test.go b/pkg/agentic/proc_test.go deleted file mode 100644 index 5186b33..0000000 --- a/pkg/agentic/proc_test.go +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package agentic - -import ( - "context" - "os" - "strconv" - "testing" - "time" - - core "dappco.re/go/core" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// testPrep is the package-level PrepSubsystem for tests that need process execution. -var testPrep *PrepSubsystem - -// testCore is the package-level Core with go-process registered. -var testCore *core.Core - -// TestMain sets up a PrepSubsystem with go-process registered for all tests in the package. -func TestMain(m *testing.M) { - testCore = core.New( - core.WithService(ProcessRegister), - ) - testCore.ServiceStartup(context.Background(), nil) - - // Enable pipeline feature flags (matches Register defaults) - testCore.Config().Enable("auto-qa") - testCore.Config().Enable("auto-pr") - testCore.Config().Enable("auto-merge") - testCore.Config().Enable("auto-ingest") - - testPrep = &PrepSubsystem{ - ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - os.Exit(m.Run()) -} - -// newPrepWithProcess creates a PrepSubsystem wired to testCore for tests that -// need process execution via s.Core().Process(). -func newPrepWithProcess() *PrepSubsystem { - return &PrepSubsystem{ - ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } -} - -// --- runCmd --- - -func TestProc_RunCmd_Good(t *testing.T) { - dir := t.TempDir() - r := testPrep.runCmd(context.Background(), dir, "echo", "hello") - assert.True(t, r.OK) - assert.Contains(t, core.Trim(r.Value.(string)), "hello") -} - -func TestProc_RunCmd_Bad(t *testing.T) { - dir := t.TempDir() - r := testPrep.runCmd(context.Background(), dir, "nonexistent-command-xyz") - assert.False(t, r.OK) -} - -func TestProc_RunCmd_Ugly(t *testing.T) { - dir := t.TempDir() - // Empty command string — should error - r := testPrep.runCmd(context.Background(), dir, "") - assert.False(t, r.OK) -} - -// --- runCmdEnv --- - -func TestProc_RunCmdEnv_Good(t *testing.T) { - dir := t.TempDir() - r := testPrep.runCmdEnv(context.Background(), dir, []string{"MY_CUSTOM_VAR=hello_test"}, "env") - assert.True(t, r.OK) - assert.Contains(t, r.Value.(string), "MY_CUSTOM_VAR=hello_test") -} - -func TestProc_RunCmdEnv_Bad(t *testing.T) { - dir := t.TempDir() - r := testPrep.runCmdEnv(context.Background(), dir, []string{"FOO=bar"}, "nonexistent-command-xyz") - assert.False(t, r.OK) -} - -func TestProc_RunCmdEnv_Ugly(t *testing.T) { - dir := t.TempDir() - // Empty env slice — should work fine, just no extra vars - r := testPrep.runCmdEnv(context.Background(), dir, []string{}, "echo", "works") - assert.True(t, r.OK) - assert.Contains(t, core.Trim(r.Value.(string)), "works") -} - -// --- runCmdOK --- - -func TestProc_RunCmdOK_Good(t *testing.T) { - dir := t.TempDir() - assert.True(t, testPrep.runCmdOK(context.Background(), dir, "echo", "ok")) -} - -func TestProc_RunCmdOK_Bad(t *testing.T) { - dir := t.TempDir() - assert.False(t, testPrep.runCmdOK(context.Background(), dir, "nonexistent-command-xyz")) -} - -func TestProc_RunCmdOK_Ugly(t *testing.T) { - dir := t.TempDir() - // "false" command returns exit 1 - assert.False(t, testPrep.runCmdOK(context.Background(), dir, "false")) -} - -// --- gitCmd --- - -func TestProc_GitCmd_Good(t *testing.T) { - dir := t.TempDir() - r := testPrep.gitCmd(context.Background(), dir, "--version") - assert.True(t, r.OK) -} - -func TestProc_GitCmd_Bad(t *testing.T) { - // git log in a non-git dir should fail - dir := t.TempDir() - r := testPrep.gitCmd(context.Background(), dir, "log") - assert.False(t, r.OK) -} - -func TestProc_GitCmd_Ugly(t *testing.T) { - dir := t.TempDir() - // Empty args — git with no arguments exits 1 - r := testPrep.gitCmd(context.Background(), dir) - assert.False(t, r.OK) -} - -// --- gitCmdOK --- - -func TestProc_GitCmdOK_Good(t *testing.T) { - dir := t.TempDir() - assert.True(t, testPrep.gitCmdOK(context.Background(), dir, "--version")) -} - -func TestProc_GitCmdOK_Bad(t *testing.T) { - // git log in non-git dir returns false - dir := t.TempDir() - assert.False(t, testPrep.gitCmdOK(context.Background(), dir, "log")) -} - -func TestProc_GitCmdOK_Ugly(t *testing.T) { - // Empty dir string — git may use cwd, which may or may not be a repo - // Just ensure no panic - assert.NotPanics(t, func() { - testPrep.gitCmdOK(context.Background(), "", "--version") - }) -} - -// --- gitOutput --- - -func TestProc_GitOutput_Good(t *testing.T) { - dir := initTestRepo(t) - branch := testPrep.gitOutput(context.Background(), dir, "rev-parse", "--abbrev-ref", "HEAD") - assert.Equal(t, "main", branch) -} - -func TestProc_GitOutput_Bad(t *testing.T) { - // Non-git dir returns empty string - dir := t.TempDir() - out := testPrep.gitOutput(context.Background(), dir, "rev-parse", "--abbrev-ref", "HEAD") - assert.Equal(t, "", out) -} - -func TestProc_GitOutput_Ugly(t *testing.T) { - // Failed command returns empty string - dir := t.TempDir() - out := testPrep.gitOutput(context.Background(), dir, "log", "--oneline", "-5") - assert.Equal(t, "", out) -} - -// --- PIDAlive --- - -func TestProc_PIDAlive_Good(t *testing.T) { - // Own PID should be running - pid, _ := strconv.Atoi(core.Env("PID")) - assert.True(t, PIDAlive(pid)) -} - -func TestProc_PIDAlive_Bad(t *testing.T) { - // PID 999999 should not be running (extremely unlikely to exist) - assert.False(t, PIDAlive(999999)) -} - -func TestProc_PIDAlive_Ugly(t *testing.T) { - // PID 0 — should return false (invalid PID guard: pid > 0 is false for 0) - assert.False(t, PIDAlive(0)) -} - -// --- PIDTerminate --- - -func TestProc_PIDTerminate_Good(t *testing.T) { - t.Skip("would need real process to kill") -} - -func TestProc_PIDTerminate_Bad(t *testing.T) { - // PID 999999 should fail to kill - assert.False(t, PIDTerminate(999999)) -} - -func TestProc_PIDTerminate_Ugly(t *testing.T) { - // PID 0 — pid > 0 guard returns false - assert.False(t, PIDTerminate(0)) -} - -// --- initTestRepo creates a git repo with commits for proc tests --- - -func initTestRepo(t *testing.T) string { - t.Helper() - dir := t.TempDir() - gitEnv := []string{ - "GIT_AUTHOR_NAME=Test", - "GIT_AUTHOR_EMAIL=test@test.com", - "GIT_COMMITTER_NAME=Test", - "GIT_COMMITTER_EMAIL=test@test.com", - } - run := func(args ...string) { - t.Helper() - r := testCore.Process().RunWithEnv(context.Background(), dir, gitEnv, args[0], args[1:]...) - require.True(t, r.OK, "cmd %v failed: %s", args, r.Value) - } - run("git", "init", "-b", "main") - run("git", "config", "user.name", "Test") - run("git", "config", "user.email", "test@test.com") - require.True(t, fs.Write(core.JoinPath(dir, "README.md"), "# Test").OK) - run("git", "add", "README.md") - run("git", "commit", "-m", "initial commit") - return dir -} diff --git a/pkg/agentic/review_queue.go b/pkg/agentic/review_queue.go index 5067732..1c54ccd 100644 --- a/pkg/agentic/review_queue.go +++ b/pkg/agentic/review_queue.go @@ -163,6 +163,7 @@ func (s *PrepSubsystem) findReviewCandidates(basePath string) []string { // reviewRepo runs CodeRabbit on a single repo and takes action. func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer string, dryRun, localOnly bool) ReviewResult { result := ReviewResult{Repo: repo} + process := s.Core().Process() // Check saved rate limit if rl := s.loadRateLimitState(); rl != nil && rl.Limited && time.Now().Before(rl.RetryAt) { @@ -176,7 +177,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer reviewer = "coderabbit" } command, args := s.buildReviewCommand(repoDir, reviewer) - r := s.runCmd(ctx, repoDir, command, args...) + r := process.RunIn(ctx, repoDir, command, args...) output, _ := r.Value.(string) // Parse rate limit (both reviewers use similar patterns) @@ -250,14 +251,15 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer // pushAndMerge pushes to GitHub dev and merges the PR. func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string) error { - if r := s.gitCmd(ctx, repoDir, "push", "github", "HEAD:refs/heads/dev", "--force"); !r.OK { + process := s.Core().Process() + if r := process.RunIn(ctx, repoDir, "git", "push", "github", "HEAD:refs/heads/dev", "--force"); !r.OK { return core.E("pushAndMerge", core.Concat("push failed: ", r.Value.(string)), nil) } // Mark PR ready if draft - s.runCmdOK(ctx, repoDir, "gh", "pr", "ready", "--repo", core.Concat(GitHubOrg(), "/", repo)) + process.RunIn(ctx, repoDir, "gh", "pr", "ready", "--repo", core.Concat(GitHubOrg(), "/", repo)) - if r := s.runCmd(ctx, repoDir, "gh", "pr", "merge", "--merge", "--delete-branch"); !r.OK { + if r := process.RunIn(ctx, repoDir, "gh", "pr", "merge", "--merge", "--delete-branch"); !r.OK { return core.E("pushAndMerge", core.Concat("merge failed: ", r.Value.(string)), nil) } diff --git a/pkg/agentic/verify.go b/pkg/agentic/verify.go index 6c1168d..6615bf1 100644 --- a/pkg/agentic/verify.go +++ b/pkg/agentic/verify.go @@ -104,14 +104,15 @@ func (s *PrepSubsystem) attemptVerifyAndMerge(repoDir, org, repo, branch string, // rebaseBranch rebases the current branch onto the default branch and force-pushes. func (s *PrepSubsystem) rebaseBranch(repoDir, branch string) bool { ctx := context.Background() + process := s.Core().Process() base := s.DefaultBranch(repoDir) - if !s.gitCmdOK(ctx, repoDir, "fetch", "origin", base) { + if !process.RunIn(ctx, repoDir, "git", "fetch", "origin", base).OK { return false } - if !s.gitCmdOK(ctx, repoDir, "rebase", core.Concat("origin/", base)) { - s.gitCmdOK(ctx, repoDir, "rebase", "--abort") + if !process.RunIn(ctx, repoDir, "git", "rebase", core.Concat("origin/", base)).OK { + process.RunIn(ctx, repoDir, "git", "rebase", "--abort") return false } @@ -125,7 +126,7 @@ func (s *PrepSubsystem) rebaseBranch(repoDir, branch string) bool { repo = st.Repo } forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, repo) - return s.gitCmdOK(ctx, repoDir, "push", "--force-with-lease", forgeRemote, branch) + return process.RunIn(ctx, repoDir, "git", "push", "--force-with-lease", forgeRemote, branch).OK } // flagForReview adds the "needs-review" label to the PR via Forge API. @@ -217,7 +218,8 @@ func (s *PrepSubsystem) runVerification(repoDir string) verifyResult { func (s *PrepSubsystem) runGoTests(repoDir string) verifyResult { ctx := context.Background() - r := s.runCmdEnv(ctx, repoDir, []string{"GOWORK=off"}, "go", "test", "./...", "-count=1", "-timeout", "120s") + process := s.Core().Process() + r := process.RunWithEnv(ctx, repoDir, []string{"GOWORK=off"}, "go", "test", "./...", "-count=1", "-timeout", "120s") out := resultText(r) exitCode := 0 if !r.OK { @@ -228,10 +230,11 @@ func (s *PrepSubsystem) runGoTests(repoDir string) verifyResult { func (s *PrepSubsystem) runPHPTests(repoDir string) verifyResult { ctx := context.Background() - r := s.runCmd(ctx, repoDir, "composer", "test", "--no-interaction") + process := s.Core().Process() + r := process.RunIn(ctx, repoDir, "composer", "test", "--no-interaction") if !r.OK { // Try pest as fallback - r2 := s.runCmd(ctx, repoDir, "./vendor/bin/pest", "--no-interaction") + r2 := process.RunIn(ctx, repoDir, "./vendor/bin/pest", "--no-interaction") if !r2.OK { return verifyResult{passed: false, testCmd: "none", output: "No PHP test runner found (composer test and vendor/bin/pest both unavailable)", exitCode: 1} } @@ -254,7 +257,8 @@ func (s *PrepSubsystem) runNodeTests(repoDir string) verifyResult { } ctx := context.Background() - r = s.runCmd(ctx, repoDir, "npm", "test") + process := s.Core().Process() + r = process.RunIn(ctx, repoDir, "npm", "test") out := resultText(r) exitCode := 0 if !r.OK { diff --git a/pkg/lib/workspace/default/CODEX.md.tmpl b/pkg/lib/workspace/default/CODEX.md.tmpl index 615df31..9407b6a 100644 --- a/pkg/lib/workspace/default/CODEX.md.tmpl +++ b/pkg/lib/workspace/default/CODEX.md.tmpl @@ -124,7 +124,7 @@ if run.OK { - Use UK English in comments and docs. - Use `core.E("pkg.Method", "message", err)` for errors. Never use `fmt.Errorf` or `errors.New`. - Use `c.Fs()` or a package-level `fs` helper for file I/O. Never use raw `os.ReadFile`, `os.WriteFile`, or `filepath.*`. -- Route external commands through `c.Process()` or the repo's process helper layer. Never import `os/exec`. +- Route external commands through `c.Process()`. Never import `os/exec`. - Use Core string and path helpers such as `core.Contains`, `core.Trim`, `core.Split`, `core.Concat`, and `core.JoinPath` instead of raw `strings.*` or path concatenation. - Prefer `core.Result{Value: x, OK: true}` over `(value, error)` pairs in Core-facing code. - Comments should show HOW with real values, not restate the signature.