feat: devops plugin, CLI commands, Codex dispatch fixes, AX sweep
DevOps plugin (5 skills): - install-core-agent, repair-core-agent, merge-workspace, update-deps, clean-workspaces CLI commands: version, check, extract for diagnostics. Codex dispatch: --skip-git-repo-check, removed broken --model-reasoning-effort, --sandbox workspace-write via --full-auto. Workspace template extracts to wsDir not srcDir. AX sweep (Codex-generated): sanitise.go extracted from prep/plan, mirror.go JSON parsing via encoding/json, setup/config.go URL parsing via net/url, strings/fmt imports eliminated from setup. CODEX.md template updated with Env/Path patterns. Review workspace template with audit-only PROMPT.md. Marketplace updated with devops plugin. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
98baa1b08f
commit
6e37bd22f0
24 changed files with 719 additions and 153 deletions
|
|
@ -44,6 +44,12 @@
|
|||
},
|
||||
"description": "CI/CD, deployment, issue tracking, and Coolify integration",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
{
|
||||
"name": "devops",
|
||||
"source": "./claude/devops",
|
||||
"description": "Agent workflow utilities — install binaries, merge workspaces, update deps, clean queues",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
9
claude/devops/.claude-plugin/plugin.json
Normal file
9
claude/devops/.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "devops",
|
||||
"version": "0.1.0",
|
||||
"description": "DevOps utilities for the Core ecosystem — build, install, deploy.",
|
||||
"author": {
|
||||
"name": "Lethean",
|
||||
"email": "virgil@lethean.io"
|
||||
}
|
||||
}
|
||||
44
claude/devops/skills/clean-workspaces/SKILL.md
Normal file
44
claude/devops/skills/clean-workspaces/SKILL.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
name: clean-workspaces
|
||||
description: This skill should be used when the user asks to "clean workspaces", "clean up old agents", "remove stale workspaces", "nuke completed workspaces", or needs to remove finished/failed agent workspaces from the dispatch queue.
|
||||
argument-hint: [--all | --completed | --failed | --blocked]
|
||||
allowed-tools: ["Bash"]
|
||||
---
|
||||
|
||||
# Clean Agent Workspaces
|
||||
|
||||
Remove stale agent workspaces from the dispatch system.
|
||||
|
||||
## Steps
|
||||
|
||||
1. List all workspaces with their status:
|
||||
```bash
|
||||
for ws in /Users/snider/Code/.core/workspace/*/status.json; do
|
||||
dir=$(dirname "$ws")
|
||||
name=$(basename "$dir")
|
||||
status=$(python3 -c "import json; print(json.load(open('$ws'))['status'])" 2>/dev/null || echo "unknown")
|
||||
echo "$status $name"
|
||||
done | sort
|
||||
```
|
||||
|
||||
2. Based on the argument:
|
||||
- `--completed` — remove workspaces with status "completed"
|
||||
- `--failed` — remove workspaces with status "failed"
|
||||
- `--blocked` — remove workspaces with status "blocked"
|
||||
- `--all` — remove completed + failed + blocked (NOT running)
|
||||
- No argument — show the list and ask the user what to remove
|
||||
|
||||
3. Show the user what will be removed and get confirmation BEFORE deleting.
|
||||
|
||||
4. Remove confirmed workspaces:
|
||||
```bash
|
||||
rm -rf /Users/snider/Code/.core/workspace/<name>/
|
||||
```
|
||||
|
||||
5. Report how many were removed.
|
||||
|
||||
## Important
|
||||
|
||||
- NEVER remove workspaces with status "running" — they have active processes
|
||||
- ALWAYS ask for confirmation before removing
|
||||
- Show the count and names before removing
|
||||
35
claude/devops/skills/install-core-agent/SKILL.md
Normal file
35
claude/devops/skills/install-core-agent/SKILL.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
name: install-core-agent
|
||||
description: This skill should be used when the user asks to "install core-agent", "rebuild core-agent", "update the agent binary", or needs to compile and install the core-agent MCP server binary. Runs the correct go install from the right directory with the right path.
|
||||
argument-hint: (no arguments needed)
|
||||
allowed-tools: ["Bash"]
|
||||
---
|
||||
|
||||
# Install core-agent
|
||||
|
||||
Build and install the core-agent binary from source.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Run from the core/agent repo directory:
|
||||
|
||||
```bash
|
||||
cd /Users/snider/Code/core/agent && go install ./cmd/core-agent/
|
||||
```
|
||||
|
||||
2. Verify the binary is installed:
|
||||
|
||||
```bash
|
||||
which core-agent
|
||||
```
|
||||
|
||||
3. Tell the user to restart core-agent (it runs as an MCP server — the process needs restarting to pick up the new binary).
|
||||
|
||||
## Important
|
||||
|
||||
- The entry point is `./cmd/core-agent/main.go` — NOT `./cmd/` or `.`
|
||||
- `go install ./cmd/core-agent/` produces a binary named `core-agent` automatically
|
||||
- Do NOT use `go install .`, `go install ./cmd/`, or `go build` with manual `-o` flags
|
||||
- Do NOT move, copy, or rename binaries
|
||||
- Do NOT touch `~/go/bin/` or `~/.local/bin/` directly
|
||||
- If the install fails, report the error to the user — do not attempt alternatives
|
||||
56
claude/devops/skills/merge-workspace/SKILL.md
Normal file
56
claude/devops/skills/merge-workspace/SKILL.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
name: merge-workspace
|
||||
description: This skill should be used when the user asks to "merge workspace", "bring over the changes", "review and merge agent work", "pull in the agent's diff", or needs to take a completed agent workspace and merge its changes into the dev branch. Checks the diff, verifies the build, and applies changes.
|
||||
argument-hint: <workspace-name>
|
||||
allowed-tools: ["Bash", "Read"]
|
||||
---
|
||||
|
||||
# Merge Agent Workspace
|
||||
|
||||
Take a completed agent workspace and merge its changes into the repo's dev branch.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Find the workspace. The argument is the workspace name (e.g., `agent-1774177216443021000`). The full path is:
|
||||
```
|
||||
/Users/snider/Code/.core/workspace/<name>/
|
||||
```
|
||||
|
||||
2. Check status:
|
||||
```bash
|
||||
cat /Users/snider/Code/.core/workspace/<name>/status.json
|
||||
```
|
||||
Only proceed if status is `completed` or `ready-for-review`.
|
||||
|
||||
3. Show the diff from the workspace's src/ directory:
|
||||
```bash
|
||||
cd /Users/snider/Code/.core/workspace/<name>/src && git diff HEAD
|
||||
```
|
||||
Present a summary of what changed to the user.
|
||||
|
||||
4. Ask the user if the changes look good before proceeding.
|
||||
|
||||
5. Find the original repo path from status.json (`repo` field). The repo lives at:
|
||||
```
|
||||
/Users/snider/Code/core/<repo>/
|
||||
```
|
||||
|
||||
6. Cherry-pick or apply the changes. Use `git diff` to create a patch and apply it:
|
||||
```bash
|
||||
cd /Users/snider/Code/.core/workspace/<name>/src && git diff HEAD > /tmp/agent-patch.diff
|
||||
cd /Users/snider/Code/core/<repo>/ && git apply /tmp/agent-patch.diff
|
||||
```
|
||||
|
||||
7. Verify the build passes:
|
||||
```bash
|
||||
cd /Users/snider/Code/core/<repo>/ && go build ./...
|
||||
```
|
||||
|
||||
8. Report results. Do NOT commit — let the user decide when to commit.
|
||||
|
||||
## Important
|
||||
|
||||
- Always show the diff BEFORE applying
|
||||
- Always verify the build AFTER applying
|
||||
- Never commit — the user commits when ready
|
||||
- If the patch fails to apply, show the conflict and stop
|
||||
68
claude/devops/skills/repair-core-agent/SKILL.md
Normal file
68
claude/devops/skills/repair-core-agent/SKILL.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
name: repair-core-agent
|
||||
description: This skill should be used when core-agent is broken, MCP tools aren't responding, dispatch fails, the agent binary is stale, or the user says "fix core-agent", "repair the agent", "core-agent is broken", "MCP not working", "dispatch broken". Diagnoses and guides repair of the core-agent MCP server.
|
||||
argument-hint: (no arguments needed)
|
||||
allowed-tools: ["Bash", "Read"]
|
||||
---
|
||||
|
||||
# Repair core-agent
|
||||
|
||||
Diagnose and fix core-agent when it's broken.
|
||||
|
||||
## Diagnosis Steps
|
||||
|
||||
Run these in order, stop at the first failure:
|
||||
|
||||
### 1. Is the binary installed?
|
||||
```bash
|
||||
which core-agent
|
||||
```
|
||||
Should be at `~/.local/bin/core-agent` or `~/go/bin/core-agent`. If BOTH exist, the wrong one might take precedence — check PATH order.
|
||||
|
||||
### 2. Does it compile?
|
||||
```bash
|
||||
cd /Users/snider/Code/core/agent && go build ./cmd/core-agent/
|
||||
```
|
||||
If this fails, there's a code error. Report it and stop.
|
||||
|
||||
### 3. Is a stale process running?
|
||||
```bash
|
||||
ps aux | grep core-agent | grep -v grep
|
||||
```
|
||||
If yes, the user needs to restart it to pick up the new binary.
|
||||
|
||||
### 4. Can it start?
|
||||
```bash
|
||||
core-agent mcp 2>&1 | head -5
|
||||
```
|
||||
Should show subsystem loading messages.
|
||||
|
||||
### 5. Are workspaces clean?
|
||||
```bash
|
||||
ls /Users/snider/Code/.core/workspace/ 2>/dev/null
|
||||
```
|
||||
Should only have `events.jsonl`. Stale workspaces with "running" status but dead PIDs cause phantom slot usage.
|
||||
|
||||
### 6. Is agents.yaml readable?
|
||||
```bash
|
||||
cat /Users/snider/Code/.core/agents.yaml
|
||||
```
|
||||
Check concurrency settings, agent definitions.
|
||||
|
||||
## Common Fixes
|
||||
|
||||
| Symptom | Fix |
|
||||
|---------|-----|
|
||||
| Wrong binary running | Remove stale binary, user reinstalls |
|
||||
| MCP tools not found | Restart core-agent process |
|
||||
| Dispatch always queued | Check concurrency limits in agents.yaml |
|
||||
| Workspaces not prepping | Check workspace template: `ls pkg/lib/workspace/default/` |
|
||||
| go.work missing from workspace | Rebuild core-agent — template was updated |
|
||||
| Codex can't find core.Env | Core dep too old — run update-deps skill |
|
||||
|
||||
## Important
|
||||
|
||||
- Do NOT run `go install` — tell the user to do it
|
||||
- Do NOT kill processes without asking
|
||||
- Do NOT delete workspaces without asking
|
||||
- Report what's wrong, suggest the fix, let the user decide
|
||||
55
claude/devops/skills/update-deps/SKILL.md
Normal file
55
claude/devops/skills/update-deps/SKILL.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
name: update-deps
|
||||
description: This skill should be used when the user asks to "update deps", "bump core", "update go.mod", "upgrade dependencies", or needs to update dappco.re/go/core or other Go module dependencies in a core ecosystem repo. Uses go get properly — never manual go.mod editing.
|
||||
argument-hint: [repo-name] [module@version]
|
||||
allowed-tools: ["Bash"]
|
||||
---
|
||||
|
||||
# Update Go Module Dependencies
|
||||
|
||||
Properly update dependencies in a Core ecosystem Go module.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Determine the repo. If an argument is given, use it. Otherwise use the current working directory.
|
||||
```
|
||||
/Users/snider/Code/core/<repo>/
|
||||
```
|
||||
|
||||
2. Check current dependency versions:
|
||||
```bash
|
||||
grep 'dappco.re' go.mod
|
||||
```
|
||||
|
||||
3. Update the dependency using `go get`. Examples:
|
||||
```bash
|
||||
# Update core to latest
|
||||
GONOSUMDB='dappco.re/*' GONOSUMCHECK='dappco.re/*' GOPROXY=direct go get dappco.re/go/core@latest
|
||||
|
||||
# Update to specific version
|
||||
GONOSUMDB='dappco.re/*' GONOSUMCHECK='dappco.re/*' GOPROXY=direct go get dappco.re/go/core@v0.6.0
|
||||
|
||||
# Update all dappco.re deps
|
||||
GONOSUMDB='dappco.re/*' GONOSUMCHECK='dappco.re/*' GOPROXY=direct go get -u dappco.re/...
|
||||
```
|
||||
|
||||
4. Tidy:
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
5. Verify:
|
||||
```bash
|
||||
go build ./...
|
||||
```
|
||||
|
||||
6. Report what changed in go.mod.
|
||||
|
||||
## Important
|
||||
|
||||
- ALWAYS use `go get` — NEVER manually edit go.mod
|
||||
- ALWAYS set `GONOSUMDB` and `GONOSUMCHECK` for dappco.re modules
|
||||
- ALWAYS set `GOPROXY=direct` to bypass proxy cache for private modules
|
||||
- ALWAYS run `go mod tidy` after updating
|
||||
- ALWAYS verify with `go build ./...`
|
||||
- If a version doesn't resolve, check if the tag has been pushed to GitHub (dappco.re vanity imports resolve through GitHub)
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"dappco.re/go/agent/pkg/agentic"
|
||||
"dappco.re/go/agent/pkg/brain"
|
||||
"dappco.re/go/agent/pkg/lib"
|
||||
"dappco.re/go/agent/pkg/monitor"
|
||||
"forge.lthn.ai/core/mcp/pkg/mcp"
|
||||
)
|
||||
|
|
@ -21,6 +22,108 @@ func main() {
|
|||
})
|
||||
c.App().Version = "0.2.0"
|
||||
|
||||
// version — print version and build info
|
||||
c.Command("version", core.Command{
|
||||
Description: "Print version and build info",
|
||||
Action: func(opts core.Options) core.Result {
|
||||
core.Print(nil, "core-agent %s", c.App().Version)
|
||||
core.Print(nil, " go: %s", core.Env("GO"))
|
||||
core.Print(nil, " os: %s/%s", core.Env("OS"), core.Env("ARCH"))
|
||||
core.Print(nil, " home: %s", core.Env("DIR_HOME"))
|
||||
core.Print(nil, " hostname: %s", core.Env("HOSTNAME"))
|
||||
core.Print(nil, " pid: %s", core.Env("PID"))
|
||||
return core.Result{OK: true}
|
||||
},
|
||||
})
|
||||
|
||||
// check — verify workspace, deps, and config are healthy
|
||||
c.Command("check", core.Command{
|
||||
Description: "Verify workspace, deps, and config",
|
||||
Action: func(opts core.Options) core.Result {
|
||||
fs := c.Fs()
|
||||
|
||||
core.Print(nil, "core-agent %s health check", c.App().Version)
|
||||
core.Print(nil, "")
|
||||
|
||||
// Binary location
|
||||
core.Print(nil, " binary: %s", os.Args[0])
|
||||
|
||||
// Agents config
|
||||
agentsPath := core.Path("Code", ".core", "agents.yaml")
|
||||
if fs.IsFile(agentsPath) {
|
||||
core.Print(nil, " agents: %s (ok)", agentsPath)
|
||||
} else {
|
||||
core.Print(nil, " agents: %s (MISSING)", agentsPath)
|
||||
}
|
||||
|
||||
// Workspace dir
|
||||
wsRoot := core.Path("Code", ".core", "workspace")
|
||||
if fs.IsDir(wsRoot) {
|
||||
r := fs.List(wsRoot)
|
||||
count := 0
|
||||
if r.OK {
|
||||
count = len(r.Value.([]os.DirEntry))
|
||||
}
|
||||
core.Print(nil, " workspace: %s (%d entries)", wsRoot, count)
|
||||
} else {
|
||||
core.Print(nil, " workspace: %s (MISSING)", wsRoot)
|
||||
}
|
||||
|
||||
// Core dep version
|
||||
core.Print(nil, " core: dappco.re/go/core@v%s", c.App().Version)
|
||||
|
||||
// Env keys
|
||||
core.Print(nil, " env keys: %d loaded", len(core.EnvKeys()))
|
||||
|
||||
core.Print(nil, "")
|
||||
core.Print(nil, "ok")
|
||||
return core.Result{OK: true}
|
||||
},
|
||||
})
|
||||
|
||||
// extract — test workspace template extraction
|
||||
c.Command("extract", core.Command{
|
||||
Description: "Extract a workspace template to a directory",
|
||||
Action: func(opts core.Options) core.Result {
|
||||
tmpl := opts.String("_arg")
|
||||
if tmpl == "" {
|
||||
tmpl = "default"
|
||||
}
|
||||
target := opts.String("target")
|
||||
if target == "" {
|
||||
target = core.Path("Code", ".core", "workspace", "test-extract")
|
||||
}
|
||||
|
||||
data := &lib.WorkspaceData{
|
||||
Repo: "test-repo",
|
||||
Branch: "dev",
|
||||
Task: "test extraction",
|
||||
Agent: "codex",
|
||||
}
|
||||
|
||||
core.Print(nil, "extracting template %q to %s", tmpl, target)
|
||||
if err := lib.ExtractWorkspace(tmpl, target, data); err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
// List what was created
|
||||
fs := &core.Fs{}
|
||||
r := fs.List(target)
|
||||
if r.OK {
|
||||
for _, e := range r.Value.([]os.DirEntry) {
|
||||
marker := " "
|
||||
if e.IsDir() {
|
||||
marker = "/"
|
||||
}
|
||||
core.Print(nil, " %s%s", e.Name(), marker)
|
||||
}
|
||||
}
|
||||
|
||||
core.Print(nil, "done")
|
||||
return core.Result{OK: true}
|
||||
},
|
||||
})
|
||||
|
||||
// Shared setup — creates MCP service with all subsystems wired
|
||||
initServices := func() (*mcp.Service, *monitor.Subsystem, error) {
|
||||
procFactory := process.NewService(process.Options{})
|
||||
|
|
@ -76,18 +179,17 @@ func main() {
|
|||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
addr := os.Getenv("MCP_HTTP_ADDR")
|
||||
addr := core.Env("MCP_HTTP_ADDR")
|
||||
if addr == "" {
|
||||
addr = "0.0.0.0:9101"
|
||||
}
|
||||
|
||||
healthAddr := os.Getenv("HEALTH_ADDR")
|
||||
healthAddr := core.Env("HEALTH_ADDR")
|
||||
if healthAddr == "" {
|
||||
healthAddr = "0.0.0.0:9102"
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
pidFile := core.Concat(home, "/.core/core-agent.pid")
|
||||
pidFile := core.Path(".core", "core-agent.pid")
|
||||
|
||||
daemon := process.NewDaemon(process.DaemonOptions{
|
||||
PIDFile: pidFile,
|
||||
|
|
|
|||
|
|
@ -68,11 +68,24 @@ func agentCommand(agent, prompt string) (string, []string, error) {
|
|||
case "codex":
|
||||
if model == "review" {
|
||||
// Codex review mode — non-interactive code review
|
||||
// Note: --base and prompt are mutually exclusive in codex CLI
|
||||
return "codex", []string{"review", "--base", "HEAD~1"}, nil
|
||||
return "codex", []string{
|
||||
"review", "--base", "HEAD~1",
|
||||
}, nil
|
||||
}
|
||||
// Codex agent mode — autonomous coding
|
||||
return "codex", []string{"exec", "--full-auto", prompt}, nil
|
||||
// Codex agent mode — workspace root is not a git repo (src/ is),
|
||||
// so --skip-git-repo-check is required. --full-auto gives
|
||||
// workspace-write sandbox with on-request approval.
|
||||
args := []string{
|
||||
"exec",
|
||||
"--full-auto",
|
||||
"--skip-git-repo-check",
|
||||
"-o", "agent-codex.log",
|
||||
prompt,
|
||||
}
|
||||
if model != "" {
|
||||
args = append(args[:3], append([]string{"--model", model}, args[3:]...)...)
|
||||
}
|
||||
return "codex", args, nil
|
||||
case "claude":
|
||||
args := []string{
|
||||
"-p", prompt,
|
||||
|
|
@ -127,7 +140,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
|
|||
proc, err := process.StartWithOptions(context.Background(), process.RunOptions{
|
||||
Command: command,
|
||||
Args: args,
|
||||
Dir: srcDir,
|
||||
Dir: wsDir,
|
||||
Env: []string{"TERM=dumb", "NO_COLOR=1", "CI=true", "GOWORK=off"},
|
||||
Detach: true,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ package agentic
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
|
@ -265,16 +265,24 @@ func (s *PrepSubsystem) listLocalRepos(basePath string) []string {
|
|||
|
||||
// extractJSONField extracts a simple string field from JSON array output.
|
||||
func extractJSONField(jsonStr, field string) string {
|
||||
// Quick and dirty — works for gh CLI output like [{"url":"https://..."}]
|
||||
key := core.Sprintf(`"%s":"`, field)
|
||||
idx := strings.Index(jsonStr, key)
|
||||
if idx < 0 {
|
||||
if jsonStr == "" || field == "" {
|
||||
return ""
|
||||
}
|
||||
start := idx + len(key)
|
||||
end := strings.Index(jsonStr[start:], `"`)
|
||||
if end < 0 {
|
||||
|
||||
var list []map[string]any
|
||||
if err := json.Unmarshal([]byte(jsonStr), &list); err == nil {
|
||||
for _, item := range list {
|
||||
if value, ok := item[field].(string); ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var item map[string]any
|
||||
if err := json.Unmarshal([]byte(jsonStr), &item); err != nil {
|
||||
return ""
|
||||
}
|
||||
return jsonStr[start : start+end]
|
||||
|
||||
value, _ := item[field].(string)
|
||||
return value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,16 @@ func TestExtractJSONField_Good(t *testing.T) {
|
|||
assert.Equal(t, "https://github.com/dAppCore/go-io/pull/1", extractJSONField(json, "url"))
|
||||
}
|
||||
|
||||
func TestExtractJSONField_Good_Object(t *testing.T) {
|
||||
json := `{"url":"https://github.com/dAppCore/go-io/pull/2"}`
|
||||
assert.Equal(t, "https://github.com/dAppCore/go-io/pull/2", extractJSONField(json, "url"))
|
||||
}
|
||||
|
||||
func TestExtractJSONField_Good_PrettyPrinted(t *testing.T) {
|
||||
json := "[\n {\n \"url\": \"https://github.com/dAppCore/go-io/pull/3\"\n }\n]"
|
||||
assert.Equal(t, "https://github.com/dAppCore/go-io/pull/3", extractJSONField(json, "url"))
|
||||
}
|
||||
|
||||
func TestExtractJSONField_Bad_Missing(t *testing.T) {
|
||||
assert.Equal(t, "", extractJSONField(`{"name":"test"}`, "url"))
|
||||
assert.Equal(t, "", extractJSONField("", "url"))
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
|
|
@ -348,28 +347,7 @@ func planPath(dir, id string) string {
|
|||
}
|
||||
|
||||
func generatePlanID(title string) string {
|
||||
slug := strings.Map(func(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' {
|
||||
return r
|
||||
}
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r + 32
|
||||
}
|
||||
if r == ' ' {
|
||||
return '-'
|
||||
}
|
||||
return -1
|
||||
}, title)
|
||||
|
||||
// Trim consecutive dashes and cap length
|
||||
for core.Contains(slug, "--") {
|
||||
slug = core.Replace(slug, "--", "-")
|
||||
}
|
||||
slug = strings.Trim(slug, "-")
|
||||
if len(slug) > 30 {
|
||||
slug = slug[:30]
|
||||
}
|
||||
slug = strings.TrimRight(slug, "-")
|
||||
slug := sanitisePlanSlug(title)
|
||||
|
||||
// Append short random suffix for uniqueness
|
||||
b := make([]byte, 3)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
goio "io"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -204,19 +203,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
|||
}
|
||||
|
||||
// Create feature branch
|
||||
taskSlug := strings.Map(func(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' {
|
||||
return r
|
||||
}
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r + 32 // lowercase
|
||||
}
|
||||
return '-'
|
||||
}, input.Task)
|
||||
if len(taskSlug) > 40 {
|
||||
taskSlug = taskSlug[:40]
|
||||
}
|
||||
taskSlug = strings.Trim(taskSlug, "-")
|
||||
taskSlug := sanitiseBranchSlug(input.Task, 40)
|
||||
if taskSlug == "" {
|
||||
// Fallback for issue-only dispatches with no task text
|
||||
taskSlug = core.Sprintf("issue-%d", input.Issue)
|
||||
|
|
@ -234,17 +221,17 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
|||
out.Branch = branchName
|
||||
|
||||
// Create context dirs inside src/
|
||||
fs.EnsureDir(core.JoinPath(srcDir, "kb"))
|
||||
fs.EnsureDir(core.JoinPath(srcDir, "specs"))
|
||||
fs.EnsureDir(core.JoinPath(wsDir, "kb"))
|
||||
fs.EnsureDir(core.JoinPath(wsDir, "specs"))
|
||||
|
||||
// Remote stays as local clone origin — agent cannot push to forge.
|
||||
// Reviewer pulls changes from workspace and pushes after verification.
|
||||
|
||||
// 2. Extract workspace template
|
||||
wsTmpl := "default"
|
||||
// 2. Extract workspace template — default first, then overlay
|
||||
wsTmpl := ""
|
||||
if input.Template == "security" {
|
||||
wsTmpl = "security"
|
||||
} else if input.Template == "verify" || input.Template == "conventions" {
|
||||
} else if input.Template == "review" || input.Template == "verify" || input.Template == "conventions" {
|
||||
wsTmpl = "review"
|
||||
}
|
||||
|
||||
|
|
@ -276,18 +263,9 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
|||
TestCmd: detectTestCmd(repoPath),
|
||||
}
|
||||
|
||||
lib.ExtractWorkspace(wsTmpl, srcDir, wsData)
|
||||
out.ClaudeMd = true
|
||||
|
||||
// Copy repo's own CLAUDE.md over template if it exists
|
||||
claudeMdPath := core.JoinPath(repoPath, "CLAUDE.md")
|
||||
if r := fs.Read(claudeMdPath); r.OK {
|
||||
fs.Write(core.JoinPath(srcDir, "CLAUDE.md"), r.Value.(string))
|
||||
}
|
||||
// Copy GEMINI.md from core/agent (ethics framework for all agents)
|
||||
agentGeminiMd := core.JoinPath(s.codePath, "core", "agent", "GEMINI.md")
|
||||
if r := fs.Read(agentGeminiMd); r.OK {
|
||||
fs.Write(core.JoinPath(srcDir, "GEMINI.md"), r.Value.(string))
|
||||
lib.ExtractWorkspace("default", wsDir, wsData)
|
||||
if wsTmpl != "" {
|
||||
lib.ExtractWorkspace(wsTmpl, wsDir, wsData)
|
||||
}
|
||||
|
||||
// 3. Generate TODO.md from issue (overrides template)
|
||||
|
|
@ -469,12 +447,7 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i
|
|||
}
|
||||
|
||||
content, _ := base64.StdEncoding.DecodeString(pageData.ContentBase64)
|
||||
filename := strings.Map(func(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
|
||||
return r
|
||||
}
|
||||
return '-'
|
||||
}, page.Title) + ".md"
|
||||
filename := sanitiseFilename(page.Title) + ".md"
|
||||
|
||||
fs.Write(core.JoinPath(wsDir, "src", "kb", filename), string(content))
|
||||
count++
|
||||
|
|
|
|||
|
|
@ -127,6 +127,19 @@ func TestDetectTestCmd_Good_DefaultsToGo(t *testing.T) {
|
|||
assert.Equal(t, "go test ./...", detectTestCmd(dir))
|
||||
}
|
||||
|
||||
func TestSanitiseBranchSlug_Good(t *testing.T) {
|
||||
assert.Equal(t, "fix-login-bug", sanitiseBranchSlug("Fix login bug!", 40))
|
||||
assert.Equal(t, "trim-me", sanitiseBranchSlug("---Trim Me---", 40))
|
||||
}
|
||||
|
||||
func TestSanitiseBranchSlug_Good_Truncates(t *testing.T) {
|
||||
assert.Equal(t, "feature", sanitiseBranchSlug("feature--extra", 7))
|
||||
}
|
||||
|
||||
func TestSanitiseFilename_Good(t *testing.T) {
|
||||
assert.Equal(t, "Core---Agent-Notes", sanitiseFilename("Core / Agent:Notes"))
|
||||
}
|
||||
|
||||
func TestNewPrep_Good_Defaults(t *testing.T) {
|
||||
t.Setenv("FORGE_TOKEN", "")
|
||||
t.Setenv("GITEA_TOKEN", "")
|
||||
|
|
|
|||
96
pkg/agentic/sanitise.go
Normal file
96
pkg/agentic/sanitise.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
func sanitiseBranchSlug(text string, max int) string {
|
||||
out := make([]rune, 0, len(text))
|
||||
for _, r := range text {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-':
|
||||
out = append(out, r)
|
||||
case r >= 'A' && r <= 'Z':
|
||||
out = append(out, r+32)
|
||||
default:
|
||||
out = append(out, '-')
|
||||
}
|
||||
|
||||
if max > 0 && len(out) >= max {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return trimRuneEdges(string(out), '-')
|
||||
}
|
||||
|
||||
func sanitisePlanSlug(text string) string {
|
||||
out := make([]rune, 0, len(text))
|
||||
for _, r := range text {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-':
|
||||
out = append(out, r)
|
||||
case r >= 'A' && r <= 'Z':
|
||||
out = append(out, r+32)
|
||||
case r == ' ':
|
||||
out = append(out, '-')
|
||||
}
|
||||
}
|
||||
|
||||
slug := collapseRepeatedRune(string(out), '-')
|
||||
slug = trimRuneEdges(slug, '-')
|
||||
if len(slug) > 30 {
|
||||
slug = slug[:30]
|
||||
}
|
||||
|
||||
return trimRuneEdges(slug, '-')
|
||||
}
|
||||
|
||||
func sanitiseFilename(text string) string {
|
||||
out := make([]rune, 0, len(text))
|
||||
for _, r := range text {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.':
|
||||
out = append(out, r)
|
||||
default:
|
||||
out = append(out, '-')
|
||||
}
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func collapseRepeatedRune(text string, target rune) string {
|
||||
runes := []rune(text)
|
||||
out := make([]rune, 0, len(runes))
|
||||
lastWasTarget := false
|
||||
|
||||
for _, r := range runes {
|
||||
if r == target {
|
||||
if lastWasTarget {
|
||||
continue
|
||||
}
|
||||
lastWasTarget = true
|
||||
} else {
|
||||
lastWasTarget = false
|
||||
}
|
||||
|
||||
out = append(out, r)
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func trimRuneEdges(text string, target rune) string {
|
||||
runes := []rune(text)
|
||||
start := 0
|
||||
end := len(runes)
|
||||
|
||||
for start < end && runes[start] == target {
|
||||
start++
|
||||
}
|
||||
|
||||
for end > start && runes[end-1] == target {
|
||||
end--
|
||||
}
|
||||
|
||||
return string(runes[start:end])
|
||||
}
|
||||
|
|
@ -206,7 +206,7 @@ func TestExtractWorkspace_CreatesFiles(t *testing.T) {
|
|||
t.Fatalf("ExtractWorkspace failed: %v", err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"CODEX.md", "CLAUDE.md", "PROMPT.md", "TODO.md", "CONTEXT.md"} {
|
||||
for _, name := range []string{"CODEX.md", "CLAUDE.md", "PROMPT.md", "TODO.md", "CONTEXT.md", "go.work"} {
|
||||
path := filepath.Join(dir, name)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
t.Errorf("expected %s to exist", name)
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
.core/workspace
|
||||
|
|
@ -30,6 +30,7 @@ c := core.New(core.Options{
|
|||
| `c.Cli()` | `*Cli` | CLI surface (command tree → terminal) |
|
||||
| `c.IPC()` | `*Ipc` | Message bus (Action/Query/Task) |
|
||||
| `c.I18n()` | `*I18n` | Internationalisation + locale collection |
|
||||
| `c.Env("key")` | `string` | Read-only system/environment info |
|
||||
| `c.Options()` | `*Options` | Input configuration used to create Core |
|
||||
| `c.Context()` | `context.Context` | Application context (cancelled on shutdown) |
|
||||
|
||||
|
|
@ -96,6 +97,64 @@ c.RegisterTask(func(c *core.Core, t core.Task) core.Result {
|
|||
})
|
||||
```
|
||||
|
||||
### Environment — use `core.Env()`, never `os.Getenv` for standard dirs
|
||||
|
||||
Env is environment (read-only system facts). Config is ours (mutable app settings).
|
||||
|
||||
```go
|
||||
// System
|
||||
core.Env("OS") // "darwin", "linux", "windows"
|
||||
core.Env("ARCH") // "arm64", "amd64"
|
||||
core.Env("DS") // "/" or "\" (directory separator)
|
||||
core.Env("HOSTNAME") // machine name
|
||||
core.Env("USER") // current user
|
||||
|
||||
// Directories
|
||||
core.Env("DIR_HOME") // home dir (overridable via CORE_HOME env var)
|
||||
core.Env("DIR_CONFIG") // OS config dir
|
||||
core.Env("DIR_CACHE") // OS cache dir
|
||||
core.Env("DIR_DATA") // OS data dir (platform-specific)
|
||||
core.Env("DIR_TMP") // temp dir
|
||||
core.Env("DIR_CWD") // working directory at startup
|
||||
core.Env("DIR_CODE") // ~/Code
|
||||
core.Env("DIR_DOWNLOADS") // ~/Downloads
|
||||
|
||||
// Timestamps
|
||||
core.Env("CORE_START") // RFC3339 UTC boot timestamp
|
||||
```
|
||||
|
||||
### Paths — use `core.Path()`, never `filepath.Join` or raw concatenation
|
||||
|
||||
Path() is the single point of responsibility for filesystem paths. Every path goes through it — security fixes happen in one place.
|
||||
|
||||
```go
|
||||
// WRONG
|
||||
home, _ := os.UserHomeDir()
|
||||
configPath := filepath.Join(home, ".config", "app.yaml")
|
||||
base := filepath.Base(configPath)
|
||||
|
||||
// CORRECT
|
||||
configPath := core.Path(".config", "app.yaml") // anchored to DIR_HOME
|
||||
base := core.PathBase(configPath)
|
||||
```
|
||||
|
||||
```go
|
||||
// Relative → anchored to DIR_HOME
|
||||
core.Path("Code", ".core") // "/Users/snider/Code/.core"
|
||||
core.Path(".config", "app.yaml") // "/Users/snider/.config/app.yaml"
|
||||
|
||||
// Absolute → pass through (cleaned)
|
||||
core.Path("/tmp", "workspace") // "/tmp/workspace"
|
||||
|
||||
// No args → DIR_HOME
|
||||
core.Path() // "/Users/snider"
|
||||
|
||||
// Component helpers
|
||||
core.PathBase("/a/b/c") // "c"
|
||||
core.PathDir("/a/b/c") // "/a/b"
|
||||
core.PathExt("main.go") // ".go"
|
||||
```
|
||||
|
||||
## Mandatory Patterns
|
||||
|
||||
### Errors — use `core.E()`, never `fmt.Errorf` or `errors.New`
|
||||
|
|
@ -122,16 +181,35 @@ core.Info("starting server", "addr", addr)
|
|||
core.Error("operation failed", "err", err)
|
||||
```
|
||||
|
||||
### Filesystem — use `core.Fs{}`, never `os.ReadFile/WriteFile/MkdirAll`
|
||||
### Filesystem — use `core.Fs{}` + `core.Path()`, never `os.*` or `filepath.*`
|
||||
|
||||
```go
|
||||
var fs = &core.Fs{}
|
||||
r := fs.Read(path) // returns core.Result
|
||||
|
||||
// Build paths with Path() — never raw concatenation
|
||||
configPath := core.Path(".config", "app.yaml")
|
||||
|
||||
// Read/write through Fs — never os.ReadFile/WriteFile
|
||||
r := fs.Read(configPath)
|
||||
if !r.OK { return r }
|
||||
content := r.Value.(string)
|
||||
|
||||
fs.Write(path, content) // returns core.Result
|
||||
fs.EnsureDir(dir) // returns core.Result
|
||||
fs.Write(configPath, content)
|
||||
fs.EnsureDir(core.Path(".config"))
|
||||
|
||||
// File checks — never os.Stat
|
||||
fs.Exists(path) // bool
|
||||
fs.IsFile(path) // bool
|
||||
fs.IsDir(path) // bool
|
||||
|
||||
// Directory listing — never os.ReadDir
|
||||
r := fs.List(dir) // Result{[]os.DirEntry, true}
|
||||
|
||||
// Append — never os.OpenFile
|
||||
r := fs.Append(logPath) // Result{*os.File, true}
|
||||
|
||||
// Delete — never os.Remove
|
||||
fs.Delete(path) // Result
|
||||
```
|
||||
|
||||
### Returns — use `core.Result`, never `(value, error)`
|
||||
|
|
@ -392,10 +470,15 @@ c.I18n().SetLanguage("en-GB")
|
|||
| `log` | `core.Error`, `core.Info` |
|
||||
| `strings` | `core.Contains`, `core.Split` etc |
|
||||
| `errors` | `core.E`, `core.Wrap` |
|
||||
| `path/filepath` | `core.Path`, `core.PathBase`, `core.PathDir`, `core.PathExt` |
|
||||
| `io/ioutil` | `core.Fs{}` |
|
||||
| `os` (file ops) | `core.Fs{}` |
|
||||
| `os.UserHomeDir` | `core.Env("DIR_HOME")` |
|
||||
| `os.Getenv` (standard dirs) | `core.Env("DIR_CONFIG")` etc |
|
||||
| `runtime.GOOS` | `core.Env("OS")` |
|
||||
| `runtime.GOARCH` | `core.Env("ARCH")` |
|
||||
|
||||
Acceptable stdlib: `os.Getenv`, `os.Exit`, `os.Stderr`, `os.UserHomeDir`, `context`, `sync`, `time`, `net/http`, `encoding/json`, `path/filepath`.
|
||||
Acceptable stdlib: `os.Exit`, `os.Stderr`, `os.Getenv` (non-standard keys), `context`, `sync`, `time`, `net/http`, `encoding/json`.
|
||||
|
||||
## Build & Test
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
go 1.26.0
|
||||
|
||||
use .
|
||||
use ./src
|
||||
|
|
|
|||
14
pkg/lib/workspace/review/PROMPT.md.tmpl
Normal file
14
pkg/lib/workspace/review/PROMPT.md.tmpl
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
Read CLAUDE.md for review instructions. Read CODEX.md for the AX conventions to audit against.
|
||||
|
||||
## Mode: AUDIT ONLY
|
||||
|
||||
You are a reviewer. You do NOT fix code. You do NOT commit.
|
||||
|
||||
1. Read every .go file in cmd/ and pkg/
|
||||
2. Check each against the patterns in CODEX.md
|
||||
3. Report findings to stdout with: severity (critical/high/medium/low), file:line, one-sentence description
|
||||
4. Group by package
|
||||
|
||||
If you find merge conflicts, report them as critical findings — do not resolve them.
|
||||
|
||||
Do NOT run the closeout sequence. Do NOT use code-review agents. Do NOT commit. Report and stop.
|
||||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"dappco.re/go/agent/pkg/agentic"
|
||||
|
|
@ -31,12 +32,15 @@ type harvestResult struct {
|
|||
// branches back to the source repos. Returns a summary message.
|
||||
func (m *Subsystem) harvestCompleted() string {
|
||||
wsRoot := agentic.WorkspaceRoot()
|
||||
entries := core.PathGlob(workspaceStatusGlob(wsRoot))
|
||||
entries, err := filepath.Glob(workspaceStatusGlob(wsRoot))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var harvested []harvestResult
|
||||
|
||||
for _, entry := range entries {
|
||||
wsDir := core.PathDir(monitorPath(entry))
|
||||
wsDir := filepath.Dir(entry)
|
||||
result := m.harvestWorkspace(wsDir)
|
||||
if result != nil {
|
||||
harvested = append(harvested, *result)
|
||||
|
|
@ -98,7 +102,7 @@ func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult {
|
|||
}
|
||||
|
||||
srcDir := core.Concat(wsDir, "/src")
|
||||
if !fs.Exists(srcDir) || fs.IsFile(srcDir) {
|
||||
if !fs.IsDir(srcDir) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -200,8 +204,12 @@ func countUnpushed(srcDir, branch string) int {
|
|||
// Checks ALL changed files (added, modified, renamed), not just new.
|
||||
// Fails closed: if git diff fails, rejects the workspace.
|
||||
func checkSafety(srcDir string) string {
|
||||
files, ok := changedFilesSinceDefault(srcDir)
|
||||
if !ok {
|
||||
// Check all changed files — added, modified, renamed
|
||||
base := defaultBranch(srcDir)
|
||||
cmd := exec.Command("git", "diff", "--name-only", core.Concat(base, "...HEAD"))
|
||||
cmd.Dir = srcDir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "safety check failed: git diff error"
|
||||
}
|
||||
|
||||
|
|
@ -215,18 +223,21 @@ func checkSafety(srcDir string) string {
|
|||
".db": true, ".sqlite": true, ".sqlite3": true,
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
ext := core.Lower(core.PathExt(file))
|
||||
for _, file := range core.Split(core.Trim(string(out)), "\n") {
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
ext := core.Lower(filepath.Ext(file))
|
||||
if binaryExts[ext] {
|
||||
return core.Sprintf("binary file added: %s", file)
|
||||
}
|
||||
|
||||
// Check file size (reject > 1MB)
|
||||
fullPath := core.Concat(srcDir, "/", file)
|
||||
r := fs.Stat(fullPath)
|
||||
size, ok := resultFileSize(r)
|
||||
if ok && size > 1024*1024 {
|
||||
return core.Sprintf("large file: %s (%d bytes)", file, size)
|
||||
if stat := fs.Stat(fullPath); stat.OK {
|
||||
if info, ok := stat.Value.(interface{ Size() int64 }); ok && info.Size() > 1024*1024 {
|
||||
return core.Sprintf("large file: %s (%d bytes)", file, info.Size())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,26 +246,18 @@ func checkSafety(srcDir string) string {
|
|||
|
||||
// countChangedFiles returns the number of files changed vs the default branch.
|
||||
func countChangedFiles(srcDir string) int {
|
||||
files, ok := changedFilesSinceDefault(srcDir)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return len(files)
|
||||
}
|
||||
|
||||
func changedFilesSinceDefault(srcDir string) ([]string, bool) {
|
||||
base := defaultBranch(srcDir)
|
||||
cmd := exec.Command("git", "diff", "--name-only", core.Concat(base, "...HEAD"))
|
||||
cmd.Dir = srcDir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
return 0
|
||||
}
|
||||
lines := core.Split(core.Trim(string(out)), "\n")
|
||||
if len(lines) == 1 && lines[0] == "" {
|
||||
return nil, true
|
||||
return 0
|
||||
}
|
||||
return lines, true
|
||||
return len(lines)
|
||||
}
|
||||
|
||||
// pushBranch pushes the agent's branch to origin.
|
||||
|
|
@ -268,19 +271,6 @@ func pushBranch(srcDir, branch string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func resultFileSize(r core.Result) (int64, bool) {
|
||||
type sizable interface {
|
||||
Size() int64
|
||||
}
|
||||
|
||||
switch value := r.Value.(type) {
|
||||
case sizable:
|
||||
return value.Size(), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// updateStatus updates the workspace status.json.
|
||||
func updateStatus(wsDir, status, question string) {
|
||||
r := fs.Read(workspaceStatusPath(wsDir))
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
neturl "net/url"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
|
@ -136,7 +136,7 @@ func GenerateTestConfig(projType ProjectType) (string, error) {
|
|||
}
|
||||
|
||||
func renderConfig(comment string, sections []configSection) (string, error) {
|
||||
var builder strings.Builder
|
||||
builder := core.NewBuilder()
|
||||
|
||||
if comment != "" {
|
||||
builder.WriteString("# ")
|
||||
|
|
@ -157,7 +157,7 @@ func renderConfig(comment string, sections []configSection) (string, error) {
|
|||
builder.WriteString(" ")
|
||||
builder.WriteString(value.Key)
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString(strings.TrimSpace(string(scalar)))
|
||||
builder.WriteString(core.Trim(string(scalar)))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
|
|
@ -177,32 +177,31 @@ func detectGitRemote(path string) string {
|
|||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
url := strings.TrimSpace(string(output))
|
||||
|
||||
// SSH: git@github.com:owner/repo.git or ssh://git@forge.lthn.ai:2223/core/agent.git
|
||||
if strings.Contains(url, ":") {
|
||||
parts := strings.SplitN(url, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
repo := parts[1]
|
||||
repo = strings.TrimSuffix(repo, ".git")
|
||||
// Handle port in SSH URL (ssh://git@host:port/path)
|
||||
if strings.Contains(repo, "/") {
|
||||
segments := strings.SplitN(repo, "/", 2)
|
||||
if len(segments) == 2 && strings.ContainsAny(segments[0], "0123456789") {
|
||||
repo = segments[1]
|
||||
}
|
||||
}
|
||||
return repo
|
||||
}
|
||||
return parseGitRemote(core.Trim(string(output)))
|
||||
}
|
||||
|
||||
// HTTPS: https://github.com/owner/repo.git
|
||||
for _, host := range []string{"github.com/", "forge.lthn.ai/"} {
|
||||
if idx := strings.Index(url, host); idx >= 0 {
|
||||
repo := url[idx+len(host):]
|
||||
return strings.TrimSuffix(repo, ".git")
|
||||
func parseGitRemote(remote string) string {
|
||||
if remote == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if parsed, err := neturl.Parse(remote); err == nil && parsed.Host != "" {
|
||||
return trimRemotePath(parsed.Path)
|
||||
}
|
||||
|
||||
parts := core.SplitN(remote, ":", 2)
|
||||
if len(parts) == 2 && core.Contains(parts[0], "@") {
|
||||
return trimRemotePath(parts[1])
|
||||
}
|
||||
|
||||
if core.Contains(remote, "/") {
|
||||
return trimRemotePath(remote)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func trimRemotePath(remote string) string {
|
||||
trimmed := core.TrimPrefix(remote, "/")
|
||||
return core.TrimSuffix(trimmed, ".git")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/agent/pkg/lib"
|
||||
core "dappco.re/go/core"
|
||||
|
|
@ -106,7 +104,7 @@ func scaffoldTemplate(opts Options, projType ProjectType) error {
|
|||
data := &lib.WorkspaceData{
|
||||
Repo: filepath.Base(opts.Path),
|
||||
Branch: "main",
|
||||
Task: fmt.Sprintf("Initialise %s project tooling.", projType),
|
||||
Task: core.Sprintf("Initialise %s project tooling.", projType),
|
||||
Agent: "setup",
|
||||
Language: string(projType),
|
||||
Prompt: "This workspace was scaffolded by pkg/setup. Review the repository and continue from the generated context files.",
|
||||
|
|
@ -209,7 +207,7 @@ func defaultTestCommand(projType ProjectType) string {
|
|||
}
|
||||
|
||||
func formatFlow(projType ProjectType) string {
|
||||
var builder strings.Builder
|
||||
builder := core.NewBuilder()
|
||||
builder.WriteString("- Build: `")
|
||||
builder.WriteString(defaultBuildCommand(projType))
|
||||
builder.WriteString("`\n")
|
||||
|
|
|
|||
|
|
@ -30,6 +30,26 @@ func TestGenerateBuildConfig_Good(t *testing.T) {
|
|||
assert.Contains(t, cfg, "cgo: false")
|
||||
}
|
||||
|
||||
func TestParseGitRemote_Good(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"https://github.com/dAppCore/go-io.git": "dAppCore/go-io",
|
||||
"git@github.com:dAppCore/go-io.git": "dAppCore/go-io",
|
||||
"ssh://git@forge.lthn.ai:2223/core/agent.git": "core/agent",
|
||||
"ssh://git@forge.lthn.ai:2223/core/agent": "core/agent",
|
||||
"git@forge.lthn.ai:core/agent.git": "core/agent",
|
||||
"/srv/git/core/agent.git": "srv/git/core/agent",
|
||||
}
|
||||
|
||||
for remote, want := range tests {
|
||||
assert.Equal(t, want, parseGitRemote(remote), remote)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGitRemote_Bad(t *testing.T) {
|
||||
assert.Equal(t, "", parseGitRemote(""))
|
||||
assert.Equal(t, "", parseGitRemote("origin"))
|
||||
}
|
||||
|
||||
func TestRun_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.True(t, fs.WriteMode(filepath.Join(dir, "go.mod"), "module example.com/test\n", 0644).OK)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue