Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ packages/junior-evals/.vitest-evals/recordings/**/*.json
packages/junior-evals/vitest-results*.json
packages/junior-evals/eval-results/
.env*

# Swamp local runtime (Garfield MVP and other dev automation)
.swamp/
.swamp-sources.yaml
5 changes: 5 additions & 0 deletions .swamp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
swampVersion: 20260801.231848.0
initializedAt: "2026-08-02T02:53:03.465Z"
repoId: 30afd46c-7497-4c4c-b0a6-1571ff3f9917
tools: []
gitignoreManaged: false
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,18 @@ Use **pnpm**: `pnpm install`, `pnpm dev`, `pnpm test`, `pnpm typecheck`, `pnpm s
| Testing and evals | `policies/testing.md`, `policies/evals.md`, `packages/junior/tests/README.md`, `packages/junior-evals/README.md` |
| Local agent validation | `packages/docs/src/content/docs/contribute/local-agent-validation.md` |
| Temporary plans | `openspec/changes/<slug>/` |
| Garfield dev loop | `skills/garfield/SKILL.md`, `swamp/README.md`, `scripts/garfield/run.mjs` |

Feature architecture and non-obvious invariants belong in the owning package or module `README.md`. Code, schemas, exported types, and tests are authoritative. Plans cannot override policy; update the policy for an exception.

## Garfield Dev Loop (optional)

When the user asks to run Garfield on a Junior worktree, load `skills/garfield/SKILL.md` and own the full loop. Do not hand back a manual findings checklist.

```bash
node scripts/garfield/run.mjs --goal '...'
# review lanes, write findings, fix, then:
node scripts/garfield/run.mjs --finalize
```

Docs: `swamp/README.md`. This is development tooling, not product runtime.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
"evals:record": "pnpm --filter @sentry/junior-evals evals:record",
"typecheck": "pnpm -r run typecheck",
"skills:check": "pnpm --filter @sentry/junior skills:check",
"test:ci": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-memory build && pnpm --filter @sentry/junior-github build && pnpm --filter @sentry/junior-linear build && pnpm --filter @sentry/junior-vercel build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior test:coverage && pnpm --filter @sentry/junior-memory test && pnpm --filter @sentry/junior-github test && pnpm --filter @sentry/junior-vercel test && pnpm --filter @sentry/junior-dashboard test:coverage"
"test:ci": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-memory build && pnpm --filter @sentry/junior-github build && pnpm --filter @sentry/junior-linear build && pnpm --filter @sentry/junior-vercel build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior test:coverage && pnpm --filter @sentry/junior-memory test && pnpm --filter @sentry/junior-github test && pnpm --filter @sentry/junior-vercel test && pnpm --filter @sentry/junior-dashboard test:coverage",
"garfield:test": "node --test scripts/garfield/lib.test.mjs",
"garfield:bundle": "node scripts/garfield/build-bundle.mjs",
"garfield": "node scripts/garfield/run.mjs",
"garfield:finalize": "node scripts/garfield/run.mjs --finalize"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
Expand Down
93 changes: 93 additions & 0 deletions scripts/garfield/build-bundle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env node
import path from "node:path";
import {
buildBundle,
createRunId,
defaultRunDir,
ensureDir,
parseArgs,
resolveRepoRoot,
writeJson,
} from "./lib.mjs";

function usage(exitCode = 1) {
console.error(`Usage: node scripts/garfield/build-bundle.mjs --goal <text> [options]

Options:
--goal <text> Required slice goal / user intent
--non-goal <text> Repeatable non-goal
--base <ref> Diff base (default: merge-base with origin/main)
--run-id <id> Stable run id (default: generated)
--run-dir <path> Output directory (default: .swamp/garfield/<run-id>)
--repo-root <path> Repo root (default: discover from cwd)
`);
process.exit(exitCode);
}

const args = parseArgs(process.argv.slice(2));
if (args.help || args.h) usage(0);
if (!args.goal || args.goal === true) usage(1);

const repoRoot = resolveRepoRoot(
typeof args["repo-root"] === "string" && args["repo-root"]
? args["repo-root"]
: process.cwd(),
);
const nonGoals = []
.concat(args["non-goal"] ?? [])
.concat(args["non-goals"] ?? [])
.flatMap((value) => {
if (Array.isArray(value)) return value;
if (typeof value === "string") {
return value.includes(",")
? value.split(",").map((part) => part.trim())
: [value];
}
return [];
})
.filter((value) => typeof value === "string" && value.length > 0);

const runId =
typeof args["run-id"] === "string" && args["run-id"]
? args["run-id"]
: createRunId();
const runDir =
typeof args["run-dir"] === "string" && args["run-dir"]
? path.resolve(args["run-dir"])
: defaultRunDir(repoRoot, runId);

ensureDir(runDir);

const bundle = buildBundle({
repoRoot,
goal: String(args.goal),
nonGoals,
baseRef:
typeof args.base === "string" && args.base ? args.base : undefined,
runId,
});

const bundlePath = path.join(runDir, "bundle.json");
writeJson(bundlePath, bundle);
writeJson(path.join(runDir, "run.json"), {
runId,
runDir,
bundlePath,
createdAt: bundle.createdAt,
});

console.log(
JSON.stringify(
{
ok: true,
runId,
runDir,
bundlePath,
changedFiles: bundle.changedFiles.length,
packages: bundle.packages,
contentHash: bundle.contentHash,
},
null,
2,
),
);
47 changes: 47 additions & 0 deletions scripts/garfield/capture-run-dir.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { ensureDir, parseArgs, resolveRepoRoot, writeJson } from "./lib.mjs";

function usage(exitCode = 1) {
console.error(
"Usage: node scripts/garfield/capture-run-dir.mjs --from <build-json-path>",
);
process.exit(exitCode);
}

const args = parseArgs(process.argv.slice(2));
if (args.help || args.h) usage(0);
if (typeof args.from !== "string") usage(1);

const repoRoot = resolveRepoRoot(process.cwd());
const fromPath = path.resolve(args.from);
const built = JSON.parse(fs.readFileSync(fromPath, "utf8"));
if (!built.runDir || !built.runId) {
console.error("build output missing runDir/runId", built);
process.exit(1);
}

const pointerDir = path.join(repoRoot, ".swamp", "garfield");
ensureDir(pointerDir);
const pointerPath = path.join(pointerDir, "last-run.json");
writeJson(pointerPath, {
ok: true,
runId: built.runId,
runDir: built.runDir,
bundlePath: built.bundlePath,
capturedAt: new Date().toISOString(),
});

console.log(
JSON.stringify(
{
ok: true,
runId: built.runId,
runDir: built.runDir,
pointerPath,
},
null,
2,
),
);
54 changes: 54 additions & 0 deletions scripts/garfield/classify-lanes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node
import path from "node:path";
import {
classifyLanes,
parseArgs,
readJson,
writeJson,
} from "./lib.mjs";

function usage(exitCode = 1) {
console.error(
"Usage: node scripts/garfield/classify-lanes.mjs --run-dir <path> [--profile core|full]",
);
process.exit(exitCode);
}

const args = parseArgs(process.argv.slice(2));
if (args.help || args.h) usage(0);
if (typeof args["run-dir"] !== "string") usage(1);

const runDir = path.resolve(args["run-dir"]);
const profile = args.profile === "full" ? "full" : "core";
const bundle = readJson(path.join(runDir, "bundle.json"));
const lanes = classifyLanes(bundle.changedFiles, bundle.policies, { profile });
const applicable = lanes.filter((lane) => lane.status === "applicable");
const skipped = lanes.filter((lane) => lane.status === "skipped");

const plan = {
version: 1,
runId: bundle.runId,
contentHash: bundle.contentHash,
profile,
lanes,
applicableLaneIds: applicable.map((lane) => lane.id),
skippedLaneIds: skipped.map((lane) => lane.id),
};

writeJson(path.join(runDir, "lane-plan.json"), plan);

console.log(
JSON.stringify(
{
ok: true,
runId: bundle.runId,
profile,
applicable: applicable.length,
skipped: skipped.length,
applicableLaneIds: plan.applicableLaneIds,
planPath: path.join(runDir, "lane-plan.json"),
},
null,
2,
),
);
Loading
Loading