diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..76723ef5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install + run: npm ci + + - name: Build + run: npm run build + + - name: Test + run: node --test --experimental-strip-types tests/*.test.ts diff --git a/.gitignore b/.gitignore index 99cce13a..fd95db21 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,12 @@ dist/ *.sqlite3 progress.txt progress*.txt +.env +.env.local +.env.*.local +*.key +*.pem +*.secret +coverage/ +.nyc_output/ +*.log diff --git a/agents/shared/pr/AGENTS.md b/agents/shared/pr/AGENTS.md index 5647e13e..9d8cde64 100644 --- a/agents/shared/pr/AGENTS.md +++ b/agents/shared/pr/AGENTS.md @@ -6,7 +6,11 @@ You create a pull request for completed work. 1. **cd into the repo** and checkout the branch 2. **Push the branch** — `git push -u origin {{branch}}` -3. **Create the PR** — Use `gh pr create` with a well-structured title and body +3. **Create the PR** — Use `git-pr create` with explicit flags: + - `--title "..."` + - `--description "..."` + - `--head {{branch}}` + - `--base main` (unless input explicitly says otherwise) 4. **Report the PR URL** ## PR Creation @@ -17,13 +21,23 @@ The step input will provide: Use that structure exactly. Fill in all sections with the provided context. +After running `git-pr create`: +- If output indicates an existing PR, capture that URL and continue as success. +- On success, always return the PR URL from command output. +- Do not infer PR numbers. Use the actual URL returned by the tool. + ## Output Format ``` STATUS: done PR: https://github.com/org/repo/pull/123 +PR_NUMBER: 123 ``` +Extract the PR number from the `git-pr create` output (e.g., "Created PR #123: ..." or +"Existing PR #123: ...") and include it as `PR_NUMBER`. This lets downstream agents +use the number directly without parsing URLs. + ## What NOT To Do - Don't modify code — just create the PR diff --git a/package-lock.json b/package-lock.json index 3fa58666..a1ad386e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "antfarm", - "version": "0.4.1", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "antfarm", - "version": "0.4.1", + "version": "0.5.1", "dependencies": { "json5": "^2.2.3", "yaml": "^2.4.5" diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 69fd3155..ebf3dc09 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -160,7 +160,7 @@ async function main() { console.log(`Reinstalling ${workflows.length} workflow(s)...`); for (const workflowId of workflows) { try { - await installWorkflow({ workflowId }); + await installWorkflow({ workflowId, overwriteFiles: true }); console.log(` ✓ ${workflowId}`); } catch (err) { console.log(` ✗ ${workflowId}: ${err instanceof Error ? err.message : String(err)}`); diff --git a/src/installer/install.ts b/src/installer/install.ts index 7b5440a8..005ca5bd 100644 --- a/src/installer/install.ts +++ b/src/installer/install.ts @@ -119,7 +119,7 @@ const ROLE_POLICIES: Record { +export async function installWorkflow(params: { workflowId: string; overwriteFiles?: boolean }): Promise { const { workflowDir, bundledSourceDir } = await fetchWorkflow(params.workflowId); const workflow = await loadWorkflowSpec(workflowDir); - const provisioned = await provisionAgents({ workflow, workflowDir, bundledSourceDir }); + const provisioned = await provisionAgents({ workflow, workflowDir, bundledSourceDir, overwriteFiles: params.overwriteFiles }); // Build a role lookup: workflow agent id → role (explicit or inferred) const roleMap = new Map(); diff --git a/src/installer/types.ts b/src/installer/types.ts index 487da90f..872a3e61 100644 --- a/src/installer/types.ts +++ b/src/installer/types.ts @@ -11,7 +11,7 @@ export type WorkflowAgentFiles = { * - coding: Full read/write/exec for implementation (developer, fixer, setup) * - verification: Read + exec but NO write — independent verification integrity (verifier) * - testing: Read + exec + browser/web for E2E testing, NO write (tester) - * - pr: Read + exec only — just runs `gh pr create` (pr) + * - pr: Read + exec only — just runs `git-pr create` (pr) * - scanning: Read + exec + web search for CVE lookups, NO write (scanner) */ export type AgentRole = "analysis" | "coding" | "verification" | "testing" | "pr" | "scanning"; diff --git a/src/medic/checks.ts b/src/medic/checks.ts index 4ddffab5..f947e8c8 100644 --- a/src/medic/checks.ts +++ b/src/medic/checks.ts @@ -1,8 +1,12 @@ /** * Medic health checks — modular functions that inspect DB state and return findings. */ +import fs from "node:fs/promises"; import { getDb } from "../db.js"; import { getMaxRoleTimeoutSeconds } from "../installer/install.js"; +import { loadWorkflowSpec } from "../installer/workflow-spec.js"; +import { resolveWorkflowDir, resolveWorkflowRoot } from "../installer/paths.js"; +import type { WorkflowSpec } from "../installer/types.js"; export type MedicSeverity = "info" | "warning" | "critical"; export type MedicActionType = @@ -196,6 +200,26 @@ export function checkOrphanedCrons( return findings; } +// ── Workflow Spec Loader for Medic ───────────────────────────────── + +/** + * Load a workflow spec by its ID from disk. + * Returns null if the workflow directory doesn't exist. + * Used by medic to determine expected agents for cron gap detection. + */ +export async function loadWorkflowSpecForMedic( + workflowId: string +): Promise { + const workflowDir = resolveWorkflowDir(workflowId); + try { + await fs.access(workflowDir); + } catch { + // Directory doesn't exist + return null; + } + return loadWorkflowSpec(workflowDir); +} + // ── Run All Checks ────────────────────────────────────────────────── /** diff --git a/tests/ci-workflow.test.ts b/tests/ci-workflow.test.ts new file mode 100644 index 00000000..f6e1ea3b --- /dev/null +++ b/tests/ci-workflow.test.ts @@ -0,0 +1,62 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +const workflowPath = path.resolve(import.meta.dirname, '../.github/workflows/ci.yml'); + +describe('CI workflow YAML structure', () => { + it('ci.yml file exists', () => { + assert.ok(fs.existsSync(workflowPath), `Expected ${workflowPath} to exist`); + }); + + it('has on.pull_request trigger (no branch filter)', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok(content.includes('pull_request'), 'Expected pull_request trigger'); + }); + + it('has on.push.branches: [main] trigger', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok(content.includes('push'), 'Expected push trigger'); + assert.ok(content.includes('main'), 'Expected push to main branch'); + }); + + it('uses ubuntu-latest', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok(content.includes('ubuntu-latest'), 'Expected ubuntu-latest'); + }); + + it('uses node-version 22', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok(content.includes("'22'") || content.includes('"22"') || content.includes('22'), 'Expected node-version 22'); + }); + + it('has install step with npm ci', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok(content.includes('npm ci'), 'Expected npm ci install step'); + }); + + it('has build step with npm run build', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok(content.includes('npm run build'), 'Expected npm run build step'); + }); + + it('has test step with node --test', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok( + content.includes('node --test --experimental-strip-types tests/*.test.ts'), + 'Expected test step with node --test', + ); + }); + + it('has exactly three run steps (install, build, test)', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + const runMatches = content.match(/^\s+run:/gm); + assert.equal(runMatches?.length, 3, `Expected 3 run steps, got ${runMatches?.length}`); + }); + + it('has no lint step', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok(!content.toLowerCase().includes('lint'), 'Expected no lint step'); + }); +}); diff --git a/tests/medic-workflow-loader.test.ts b/tests/medic-workflow-loader.test.ts new file mode 100644 index 00000000..5cc64e42 --- /dev/null +++ b/tests/medic-workflow-loader.test.ts @@ -0,0 +1,63 @@ +/** + * Tests for the medic workflow spec loader helper. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const { loadWorkflowSpecForMedic } = await import("../dist/medic/checks.js"); + +// Resolve the actual workflow root path (where antfarm stores workflows) +const workflowRoot = path.join(os.homedir(), ".openclaw", "antfarm", "workflows"); + +describe("loadWorkflowSpecForMedic", () => { + it("returns null when workflow directory doesn't exist", async () => { + const result = await loadWorkflowSpecForMedic("nonexistent-workflow-xyz"); + assert.strictEqual(result, null); + }); + + it("returns workflow spec when directory exists with valid workflow.yml", async () => { + // Create a temp workflow directory in the actual workflow root location + const workflowId = `test-workflow-${Date.now()}`; + const workflowDir = path.join(workflowRoot, workflowId); + await fs.mkdir(workflowDir, { recursive: true }); + + const workflowYaml = ` +id: ${workflowId} +version: "1.0" +agents: + - id: developer + workspace: + baseDir: ~/clawd/workspace/test + files: + - AGENTS.md +steps: + - id: test-step + agent: developer + input: "Do something" + expects: "Done" +`; + await fs.writeFile(path.join(workflowDir, "workflow.yml"), workflowYaml); + + try { + const result = await loadWorkflowSpecForMedic(workflowId); + + assert.ok(result, "should return a workflow spec"); + assert.strictEqual(result?.id, workflowId); + assert.strictEqual(result?.version, "1.0"); + assert.ok(Array.isArray(result?.agents)); + assert.strictEqual(result?.agents.length, 1); + assert.strictEqual(result?.agents[0].id, "developer"); + assert.ok(Array.isArray(result?.steps)); + assert.strictEqual(result?.steps.length, 1); + assert.strictEqual(result?.steps[0].id, "test-step"); + } finally { + // Cleanup + await fs.rm(workflowDir, { recursive: true, force: true }); + } + }); +}); diff --git a/workflows/bug-fix/workflow.yml b/workflows/bug-fix/workflow.yml index 1ec1126a..a84db04b 100644 --- a/workflows/bug-fix/workflow.yml +++ b/workflows/bug-fix/workflow.yml @@ -9,7 +9,7 @@ description: | PR agent creates the pull request. polling: - model: default + model: sonnet timeoutSeconds: 120 agents: @@ -286,7 +286,11 @@ steps: {{verified}} ``` - Use: gh pr create + Use: + git-pr create --title "fix: brief description of what was fixed" --description "" --head {{branch}} --base main + + If PR already exists for the branch, return that existing PR URL and treat as success. + Do not infer PR URL. Use the exact URL returned by git-pr output. Reply with: STATUS: done diff --git a/workflows/feature-dev/agents/developer/AGENTS.md b/workflows/feature-dev/agents/developer/AGENTS.md index bc742ce3..31eaa90f 100644 --- a/workflows/feature-dev/agents/developer/AGENTS.md +++ b/workflows/feature-dev/agents/developer/AGENTS.md @@ -52,10 +52,21 @@ Before EVERY commit, verify: ## Creating PRs -When creating the PR: -- Clear title that summarizes the change -- Description explaining what you did and why -- Note what was tested +**Always use `git-pr`, never `gh pr create` directly.** + +`git-pr` is a provider-agnostic wrapper in `~/.openclaw/workspace/scripts/git-pr`. It auto-detects GitHub vs Gitea from the remote URL and uses the right tool (gh for GitHub, REST API for Gitea). `gh pr create` fails non-interactively against Gitea. + +```bash +git-pr create --title "..." --description "..." --head --base main +``` + +If a PR already exists for the branch, `git-pr` returns the existing URL -- treat that as success. + +When viewing or reviewing PRs: +- `git-pr view ` -- view PR details +- `git-pr diff ` -- view diff +- `git-pr review --approve --body "..."` -- approve +- `git-pr review --request-changes --body "..."` -- request changes ## Output Format diff --git a/workflows/feature-dev/agents/reviewer/AGENTS.md b/workflows/feature-dev/agents/reviewer/AGENTS.md index 709f6583..e51e2ab9 100644 --- a/workflows/feature-dev/agents/reviewer/AGENTS.md +++ b/workflows/feature-dev/agents/reviewer/AGENTS.md @@ -12,10 +12,10 @@ You are a reviewer on a feature development workflow. Your job is to review pull ## How to Review -Use the GitHub CLI: -- `gh pr view ` - See PR details -- `gh pr diff ` - See the actual changes -- `gh pr checks ` - See CI status if available +Use provider-agnostic PR commands: +- `git-pr view ` - See PR details +- `git-pr diff ` - See the actual changes +- `git-pr checks ` - See CI status if available (GitHub only) ## What to Look For @@ -33,8 +33,8 @@ If you request changes: - Be specific: line numbers, what's wrong, how to fix - Be constructive, not just critical -Use: `gh pr comment --body "..."` -Or: `gh pr review --comment --body "..."` +Use: `git-pr comment --body "..."` +Or: `git-pr review --comment --body "..."` ## Output Format diff --git a/workflows/feature-dev/workflow.yml b/workflows/feature-dev/workflow.yml index f1f79fba..bf467252 100644 --- a/workflows/feature-dev/workflow.yml +++ b/workflows/feature-dev/workflow.yml @@ -10,7 +10,7 @@ description: | Then integration/E2E testing, PR creation, and code review. polling: - model: default + model: sonnet timeoutSeconds: 120 agents: @@ -319,7 +319,11 @@ steps: - Description explaining what and why - Reference to what was tested - Use: gh pr create + Use: + git-pr create --title "" --description "" --head {{branch}} --base main + + If PR already exists for the branch, return that existing PR URL and treat as success. + Do not infer PR URL. Use the exact URL returned by git-pr output. Reply with: STATUS: done @@ -334,6 +338,7 @@ steps: Review the pull request. PR: {{pr}} + PR_NUMBER: {{pr_number}} TASK: {{task}} CHANGES: {{changes}} @@ -346,11 +351,11 @@ steps: - Test coverage - Follows project conventions - Use: gh pr view, gh pr diff to read the PR. + Use: git-pr view {{pr_number}}, git-pr diff {{pr_number}} to read the PR. - IMPORTANT: Post your review to the PR on GitHub using: - - If approving: gh pr review --approve --body "your review summary" - - If requesting changes: gh pr review --request-changes --body "your feedback" + IMPORTANT: Post your review to the PR using provider-agnostic commands: + - If approving: git-pr review {{pr_number}} --approve --body "your review summary" + - If requesting changes: git-pr review {{pr_number}} --request-changes --body "your feedback" ## Visual Review (Frontend Changes) Has frontend changes: {{has_frontend_changes}} diff --git a/workflows/security-audit/workflow.yml b/workflows/security-audit/workflow.yml index b6cde8b1..8b1255ca 100644 --- a/workflows/security-audit/workflow.yml +++ b/workflows/security-audit/workflow.yml @@ -9,7 +9,7 @@ description: | Verifier confirms each fix. Tester runs final integration validation. PR agent creates the pull request. polling: - model: default + model: sonnet timeoutSeconds: 120 agents: @@ -386,7 +386,11 @@ steps: Label: security - Use: gh pr create + Use: + git-pr create --title "fix(security): audit and remediation YYYY-MM-DD" --description "" --head {{branch}} --base main + + If PR already exists for the branch, return that existing PR URL and treat as success. + Do not infer PR URL. Use the exact URL returned by git-pr output. Reply with: STATUS: done