From 543d1b81adcac5b2cfc69ec75d81dafe850cd658 Mon Sep 17 00:00:00 2001 From: Toby V Date: Sat, 27 Jun 2026 12:49:30 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20Wave=202=20=E2=80=94=20repo=20provision?= =?UTF-8?q?ing=20+=20repo-aware=20coding=20agents=20(ENG-013,=20ENG-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ENG-013: two-root workspace (workspace.ts) — persistent working-code root for cloned target repos (AGENT_CODE_WORKSPACE), distinct from throwaway agent-state sandboxes; PM granted git/gh (ROLES.pm.tools + prompt); sandboxEnv(source, role) forwards a PM-scoped AGENT_PM_GITHUB_TOKEN only to the PM sandbox (host token never forwarded); idempotent ensureRepoCloned (provision.ts) shallow clone/ff-pull with token via http.extraheader, no-op when unconfigured, wired+audited in implementTicket. ENG-001: WorkerInput.workdir — CLI backend runs the agent in the cloned repo cwd (persistent, not cleaned up) so it reads/edits real source; implementTicket passes the clone path; product tree never touched. typecheck + lint + 85 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 + apps/server/src/temporal/activities.ts | 33 ++++++++- packages/agents/src/backends/cli.test.ts | 45 +++++++++++ packages/agents/src/backends/cli.ts | 40 ++++++++-- packages/agents/src/index.ts | 7 ++ packages/agents/src/propose.ts | 3 + packages/agents/src/provision.test.ts | 56 ++++++++++++++ packages/agents/src/provision.ts | 74 +++++++++++++++++++ packages/agents/src/workspace.test.ts | 28 +++++++ packages/agents/src/workspace.ts | 45 +++++++++++ packages/core/src/roles.ts | 4 +- packages/core/src/worker.ts | 6 ++ project/ENG-001-repo-aware-coding-agents.md | 14 +++- ...-repo-provisioning-and-shared-workspace.md | 23 +++++- project/ROADMAP.md | 9 ++- 15 files changed, 374 insertions(+), 16 deletions(-) create mode 100644 packages/agents/src/provision.test.ts create mode 100644 packages/agents/src/provision.ts create mode 100644 packages/agents/src/workspace.test.ts create mode 100644 packages/agents/src/workspace.ts diff --git a/.gitignore b/.gitignore index 9ded2ea..eaae021 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ coverage/ # cli-backend agent sandboxes — runtime scratch only (keep the dir + its README) /workspaces/* !/workspaces/README.md + +# working-code workspace — cloned target repos agents inspect/modify (ENG-013); never committed +/workspace/ diff --git a/apps/server/src/temporal/activities.ts b/apps/server/src/temporal/activities.ts index b9b42b8..e053a2a 100644 --- a/apps/server/src/temporal/activities.ts +++ b/apps/server/src/temporal/activities.ts @@ -1,4 +1,4 @@ -import { assess, draft, proposeFileChanges, proposeTickets } from "@eng/agents" +import { assess, draft, ensureRepoCloned, proposeFileChanges, proposeTickets } from "@eng/agents" import { type DeliveryAdapter, type DeployState, @@ -213,6 +213,36 @@ export async function implementTicket(ticketId: string): Promise> | null = null try { proposed = await proposeFileChanges({ @@ -223,6 +253,7 @@ export async function implementTicket(ticketId: string): Promise { const env = sandboxEnv({ PATH: "/usr/bin" }) expect("HOME" in env).toBe(false) }) + + it("forwards a PM-scoped GitHub token only for the PM role (ENG-013)", () => { + const src = { PATH: "/usr/bin", AGENT_PM_GITHUB_TOKEN: "pmtok" } + expect(sandboxEnv(src, "pm").GITHUB_TOKEN).toBe("pmtok") + expect(sandboxEnv(src, "pm").GH_TOKEN).toBe("pmtok") + expect(sandboxEnv(src, "staff_engineer").GITHUB_TOKEN).toBeUndefined() + expect(sandboxEnv(src).GITHUB_TOKEN).toBeUndefined() + }) + + it("never forwards the host GITHUB_TOKEN, even for the PM", () => { + const env = sandboxEnv({ PATH: "/usr/bin", GITHUB_TOKEN: "host" }, "pm") + expect(env.GITHUB_TOKEN).toBeUndefined() + }) }) describe("CliBackend sandbox enforcement", () => { @@ -107,3 +120,35 @@ describe("CliBackend sandbox enforcement", () => { expect(result.summary).not.toBe(process.cwd()) }) }) + +describe("CliBackend repo-aware workdir (ENG-001)", () => { + let dir: string + let bin: string + beforeEach(async () => { + dir = join(tmpdir(), `eng-code-${process.pid}-${Date.now()}`) + await mkdir(dir, { recursive: true }) + await writeFile(join(dir, "TARGET.txt"), "seeded-content") + bin = join(dir, "fakebin.sh") + await writeFile(bin, "#!/bin/sh\ncat TARGET.txt\n") // reads a file from its cwd (the clone) + await chmod(bin, 0o755) + }) + afterEach(async () => { + await rm(dir, { recursive: true, force: true }) + }) + + it("runs the agent in the provided workdir so it reads real source, and leaves it intact", async () => { + const backend = new CliBackend("claude-opus-4-8", { bin }) + const result = await backend.run({ + role: "staff_engineer", + systemPrompt: "", + tools: [], + goalContext: "", + task: "x", + budgetCentsRemaining: 100, + workdir: dir, + }) + expect(result.summary).toContain("seeded-content") // the agent saw the seeded file + expect(existsSync(dir)).toBe(true) // a provided workdir is persistent — not cleaned up + expect(existsSync(join(dir, "TARGET.txt"))).toBe(true) + }) +}) diff --git a/packages/agents/src/backends/cli.ts b/packages/agents/src/backends/cli.ts index 398b142..9e3a006 100644 --- a/packages/agents/src/backends/cli.ts +++ b/packages/agents/src/backends/cli.ts @@ -66,8 +66,16 @@ const SANDBOX_ENV_ALLOWLIST = [ * Build the scrubbed environment for a sandboxed agent run: the allow-list above, plus any names * explicitly opted in via `AGENT_SANDBOX_ENV_PASSTHROUGH` (comma-separated). Everything else — notably * credentials — is withheld. Pure and side-effect-free for testability. + * + * Scoped-token exception (ENG-013): the PM role provisions repos with `git`/`gh`, so — and only for + * the PM — a dedicated `AGENT_PM_GITHUB_TOKEN` is forwarded as `GITHUB_TOKEN`/`GH_TOKEN`. This is a + * deliberately narrow widening of the ENG-005 boundary: no other role's sandbox receives git/gh + * credentials, and the host's full `GITHUB_TOKEN` is never forwarded. */ -export function sandboxEnv(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { +export function sandboxEnv( + source: NodeJS.ProcessEnv = process.env, + role?: string, +): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = {} for (const key of SANDBOX_ENV_ALLOWLIST) { const value = source[key] @@ -83,6 +91,10 @@ export function sandboxEnv(source: NodeJS.ProcessEnv = process.env): NodeJS.Proc if (value !== undefined) env[key] = value } } + if (role === "pm" && source.AGENT_PM_GITHUB_TOKEN) { + env.GITHUB_TOKEN = source.AGENT_PM_GITHUB_TOKEN + env.GH_TOKEN = source.AGENT_PM_GITHUB_TOKEN + } return env } @@ -120,13 +132,19 @@ export class CliBackend implements WorkerBackend { buildSystemPrompt(input), ] - // Confine the agent's file/bash tools to a throwaway sandbox — never the repo source. - const sandbox = await createSandbox(this.workspaceRoot, input.role) + // ENG-001: when a workdir is provided (a cloned target repo in the working-code workspace) run + // there so the agent reads/edits real source; otherwise confine it to a throwaway agent-state + // sandbox — never this product's own source. Provided workdirs are persistent — don't clean up. + const useProvided = Boolean(input.workdir) + const cwd = input.workdir ?? (await createSandbox(this.workspaceRoot, input.role)) + const env = sandboxEnv(process.env, input.role) let stdout: string try { - stdout = await this.exec(args, input.task, sandbox, signal) + stdout = await this.exec(args, input.task, cwd, env, signal) } finally { - if (!process.env.AGENT_KEEP_WORKSPACE) await rm(sandbox, { recursive: true, force: true }) + if (!useProvided && !process.env.AGENT_KEEP_WORKSPACE) { + await rm(cwd, { recursive: true, force: true }) + } } let parsed: CliResult | undefined @@ -154,10 +172,16 @@ export class CliBackend implements WorkerBackend { } } - private exec(args: string[], stdin: string, cwd: string, signal?: AbortSignal): Promise { + private exec( + args: string[], + stdin: string, + cwd: string, + env: NodeJS.ProcessEnv, + signal?: AbortSignal, + ): Promise { return new Promise((resolve, reject) => { - // Scrubbed env (ENG-005): the agent's tools never see host secrets. - const child = spawn(this.bin, args, { cwd, signal, env: sandboxEnv() }) + // Scrubbed env (ENG-005); the PM also gets a scoped git/gh token (ENG-013). See sandboxEnv. + const child = spawn(this.bin, args, { cwd, signal, env }) let stdout = "" let stderr = "" child.stdout.on("data", (chunk) => { diff --git a/packages/agents/src/index.ts b/packages/agents/src/index.ts index 1796cb3..9da07e0 100644 --- a/packages/agents/src/index.ts +++ b/packages/agents/src/index.ts @@ -20,7 +20,14 @@ export { parseProposal, proposeFileChanges, } from "./propose" +export { + type CloneResult, + ensureRepoCloned, + type RepoTarget, + repoTargetFromEnv, +} from "./provision" export { PROPOSAL_SCHEMA, TICKETS_SCHEMA, VERDICT_SCHEMA } from "./schemas" +export { repoWorkspacePath, resolveCodeWorkspaceRoot } from "./workspace" /** Factory for the default unit Worker runtime (mode from options or the WORKER_MODE env var). */ export function createWorker(options?: ClaudeWorkerOptions): Worker { diff --git a/packages/agents/src/propose.ts b/packages/agents/src/propose.ts index fe7086f..5738bd7 100644 --- a/packages/agents/src/propose.ts +++ b/packages/agents/src/propose.ts @@ -9,6 +9,8 @@ export interface ProposeChangesInput { /** What to build. */ task: string budgetCentsRemaining: number + /** Cloned target repo to work in (ENG-001) — the agent reads/edits real source there. */ + workdir?: string } export interface ProposedChanges { @@ -39,6 +41,7 @@ export async function proposeFileChanges(input: ProposeChangesInput): Promise { + it("is null when GitHub isn't configured", () => { + expect(repoTargetFromEnv({})).toBeNull() + expect(repoTargetFromEnv({ GITHUB_OWNER: "acme" })).toBeNull() + }) + + it("reads owner/repo and prefers the PM-scoped token over the host token", () => { + expect( + repoTargetFromEnv({ + GITHUB_OWNER: "acme", + GITHUB_REPO: "widget", + GITHUB_TOKEN: "host", + AGENT_PM_GITHUB_TOKEN: "scoped", + }), + ).toEqual({ owner: "acme", repo: "widget", token: "scoped" }) + }) +}) + +describe("provisionAction", () => { + it("clones a fresh dir and pulls an existing checkout", () => { + expect(provisionAction("/no/such/dir-xyz")).toBe("clone") + // The repo root itself has a .git → would be a pull (proves the existsSync branch). + expect(provisionAction(process.cwd())).toBe("pull") + }) +}) + +describe("gitArgs", () => { + it("builds a shallow clone with the auth header (token kept out of config)", () => { + const args = gitArgs("clone", "/code/acme/widget", { + owner: "acme", + repo: "widget", + token: "t0ken", + }) + expect(args).toContain("clone") + expect(args).toContain("--depth") + expect(args.join(" ")).toContain("https://github.com/acme/widget.git") + expect(args.join(" ")).toContain("http.extraheader=AUTHORIZATION: bearer t0ken") + }) + + it("builds a ff-only pull for an existing checkout, no auth header when tokenless", () => { + const args = gitArgs("pull", "/code/acme/widget", { owner: "acme", repo: "widget" }) + expect(args.slice(0, 2)).toEqual(["-C", "/code/acme/widget"]) + expect(args).toContain("pull") + expect(args).toContain("--ff-only") + expect(args.join(" ")).not.toContain("extraheader") + }) +}) + +describe("ensureRepoCloned", () => { + it("skips (returns null) when no target is configured", async () => { + await expect(ensureRepoCloned(null)).resolves.toBeNull() + }) +}) diff --git a/packages/agents/src/provision.ts b/packages/agents/src/provision.ts new file mode 100644 index 0000000..600fba1 --- /dev/null +++ b/packages/agents/src/provision.ts @@ -0,0 +1,74 @@ +import { spawn } from "node:child_process" +import { existsSync } from "node:fs" +import { mkdir } from "node:fs/promises" +import { dirname, join } from "node:path" +import { repoWorkspacePath } from "./workspace" + +/** + * Repository provisioning (ENG-013): clone the target repo into the working-code workspace so coding + * agents (ENG-001) have real source to read and edit. The PM agent owns this via its granted `git`/`gh` + * tools; this module is the *deterministic backbone* the lifecycle calls to guarantee (and verify) the + * clone exists regardless of the agent turn — idempotent, and a no-op when GitHub isn't configured. + */ + +export interface RepoTarget { + owner: string + repo: string + /** Scoped token for private clones; the PM-scoped token is preferred over the host delivery token. */ + token?: string +} + +/** Resolve the target repo from env (reuses the delivery config). Null when unconfigured (skip). */ +export function repoTargetFromEnv(env: NodeJS.ProcessEnv = process.env): RepoTarget | null { + const owner = env.GITHUB_OWNER + const repo = env.GITHUB_REPO + if (!owner || !repo) return null + return { owner, repo, token: env.AGENT_PM_GITHUB_TOKEN ?? env.GITHUB_TOKEN } +} + +export type ProvisionAction = "clone" | "pull" + +/** Pure idempotency decision: an existing checkout is pulled, otherwise cloned fresh. */ +export function provisionAction(dir: string): ProvisionAction { + return existsSync(join(dir, ".git")) ? "pull" : "clone" +} + +export interface CloneResult { + path: string + action: ProvisionAction +} + +/** `git` args for the action — token (if any) injected via http.extraheader so it never lands in + * `.git/config`. Pure, for testing. */ +export function gitArgs(action: ProvisionAction, dir: string, target: RepoTarget): string[] { + const auth = target.token ? ["-c", `http.extraheader=AUTHORIZATION: bearer ${target.token}`] : [] + if (action === "pull") return ["-C", dir, ...auth, "pull", "--ff-only"] + const url = `https://github.com/${target.owner}/${target.repo}.git` + return [...auth, "clone", "--depth", "1", url, dir] +} + +function runGit(args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn("git", args, { stdio: "ignore" }) + child.on("error", reject) + child.on("close", (code) => + code === 0 ? resolve() : reject(new Error(`git exited with code ${code}`)), + ) + }) +} + +/** + * Ensure the target repo is cloned into the working-code workspace, idempotently. Returns the clone + * path + the action taken, or `null` when GitHub isn't configured (caller audits the skip). Operates + * only under the working-code workspace — never this product's own tree. + */ +export async function ensureRepoCloned( + target: RepoTarget | null = repoTargetFromEnv(), +): Promise { + if (!target) return null + const dir = repoWorkspacePath(target.owner, target.repo) + const action = provisionAction(dir) + if (action === "clone") await mkdir(dirname(dir), { recursive: true }) + await runGit(gitArgs(action, dir, target)) + return { path: dir, action } +} diff --git a/packages/agents/src/workspace.test.ts b/packages/agents/src/workspace.test.ts new file mode 100644 index 0000000..6f2d094 --- /dev/null +++ b/packages/agents/src/workspace.test.ts @@ -0,0 +1,28 @@ +import { afterEach, describe, expect, it } from "vitest" +import { repoWorkspacePath, resolveCodeWorkspaceRoot } from "./workspace" + +describe("resolveCodeWorkspaceRoot", () => { + const prev = process.env.AGENT_CODE_WORKSPACE + afterEach(() => { + if (prev === undefined) delete process.env.AGENT_CODE_WORKSPACE + else process.env.AGENT_CODE_WORKSPACE = prev + }) + + it("honors AGENT_CODE_WORKSPACE", () => { + expect(resolveCodeWorkspaceRoot({ AGENT_CODE_WORKSPACE: "/tmp/code-ws" })).toBe("/tmp/code-ws") + }) + + it("defaults to a top-level workspace/ (singular, distinct from throwaway workspaces/)", () => { + const root = resolveCodeWorkspaceRoot({}) + expect(root.endsWith("/workspace")).toBe(true) + expect(root.endsWith("/workspaces")).toBe(false) + }) +}) + +describe("repoWorkspacePath", () => { + it("lays out clones as //", () => { + expect(repoWorkspacePath("acme", "widget", { AGENT_CODE_WORKSPACE: "/code" })).toBe( + "/code/acme/widget", + ) + }) +}) diff --git a/packages/agents/src/workspace.ts b/packages/agents/src/workspace.ts new file mode 100644 index 0000000..75d7207 --- /dev/null +++ b/packages/agents/src/workspace.ts @@ -0,0 +1,45 @@ +import { existsSync } from "node:fs" +import { dirname, join, resolve as pathResolve } from "node:path" + +/** + * The two sandboxed roots (ENG-013). Agents have two distinct filesystem areas, never conflated: + * + * 1. **Agent-state sandbox** — throwaway, per-run scratch under `workspaces/-xxxx` + * (`resolveWorkspaceRoot` in `backends/cli.ts`, env `AGENT_WORKSPACE_DIR`). Ephemeral; no product + * code lives here. + * 2. **Working-code workspace** — *persistent* shared root holding cloned target repos that agents + * inspect and modify (this module, env `AGENT_CODE_WORKSPACE`). + * + * This module owns the working-code root and the per-repo path layout. + */ + +/** Walk up to the monorepo root (the dir containing `pnpm-workspace.yaml`); falls back to `start`. */ +export function findRepoRoot(start: string = process.cwd()): string { + let dir = start + for (let i = 0; i < 8; i++) { + if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir + const parent = dirname(dir) + if (parent === dir) break + dir = parent + } + return start +} + +/** + * Root of the persistent working-code workspace. Override with `AGENT_CODE_WORKSPACE`; otherwise a + * top-level `workspace/` (singular — deliberately distinct from the throwaway `workspaces/` plural). + */ +export function resolveCodeWorkspaceRoot(env: NodeJS.ProcessEnv = process.env): string { + const override = env.AGENT_CODE_WORKSPACE + if (override) return pathResolve(override) + return join(findRepoRoot(), "workspace") +} + +/** Path of a cloned target repo within the working-code workspace: `//`. */ +export function repoWorkspacePath( + owner: string, + repo: string, + env: NodeJS.ProcessEnv = process.env, +): string { + return join(resolveCodeWorkspaceRoot(env), owner, repo) +} diff --git a/packages/core/src/roles.ts b/packages/core/src/roles.ts index 9eb5491..632efc8 100644 --- a/packages/core/src/roles.ts +++ b/packages/core/src/roles.ts @@ -22,8 +22,8 @@ export const ROLES: Record = { id: "pm", title: "Product Manager", systemPrompt: - "You are the PM for the unit. Own discovery, requirements, prioritization, the roadmap, and acceptance criteria. Trace every item to a goal and the unit mission.", - tools: ["tracker", "docs"], + "You are the PM for the unit. Own discovery, requirements, prioritization, the roadmap, and acceptance criteria. Trace every item to a goal and the unit mission. You also provision the target repository: use `gh` to list existing repos and prefer reusing one; create it with `gh` only if it does not exist; and clone it into the working-code workspace so the engineers can read and modify real source. Operations must be idempotent — never clobber an existing repo or checkout.", + tools: ["tracker", "docs", "git", "gh"], monthlyBudgetCents: 50_00, ownsStages: ["discovery"], }, diff --git a/packages/core/src/worker.ts b/packages/core/src/worker.ts index 8491176..bc30726 100644 --- a/packages/core/src/worker.ts +++ b/packages/core/src/worker.ts @@ -22,6 +22,12 @@ export interface WorkerInput { * backend) ignore it and fall back to prompt-contract parsing. */ outputSchema?: Record + /** + * Optional working directory for the run (ENG-001). When set (e.g. a cloned target repo in the + * working-code workspace), the CLI backend runs the agent there — so it reads and edits real source + * — and does NOT clean it up. When unset, the backend uses a throwaway agent-state sandbox. + */ + workdir?: string } export interface ToolCallRecord { diff --git a/project/ENG-001-repo-aware-coding-agents.md b/project/ENG-001-repo-aware-coding-agents.md index 7f69b71..1f73b14 100644 --- a/project/ENG-001-repo-aware-coding-agents.md +++ b/project/ENG-001-repo-aware-coding-agents.md @@ -1,11 +1,23 @@ # ENG-001 — Give coding agents real repository context -- **Status:** backlog +- **Status:** done - **Priority:** P0 (Critical — blocks a usable autonomous run) - **Stage:** implementation - **Assignee role:** staff_engineer (with lead_system_design input) - **Area:** packages/agents +> **Outcome (Wave 2):** `WorkerInput.workdir` added; the **CLI backend** runs `claude -p` with its cwd +> set to the provided workdir (the cloned target repo from ENG-013) so the agent reads and edits real +> source — and a provided workdir is *persistent* (not cleaned up like the throwaway sandbox). +> `proposeFileChanges` forwards `workdir`; `implementTicket` clones the repo (ENG-013) and passes the +> path so the Staff Engineer codes against real files, auditing `repo_context_ready` / +> `repo_context_skipped`. The product's own tree is never touched (the agent works under the separate +> working-code workspace). Test: the agent reads a seeded file in the workdir and the workdir is left +> intact. +> +> *Deferred (noted):* the **API backend** is tool-less, so file-context injection for it (a directory +> tree / key files in the prompt) is a follow-up — the CLI backend is the real repo-aware path. + ## Problem Neither `Worker` backend can see the repository it is supposed to modify, so the coding agent writes diff --git a/project/ENG-013-repo-provisioning-and-shared-workspace.md b/project/ENG-013-repo-provisioning-and-shared-workspace.md index 06146f6..6400331 100644 --- a/project/ENG-013-repo-provisioning-and-shared-workspace.md +++ b/project/ENG-013-repo-provisioning-and-shared-workspace.md @@ -1,6 +1,6 @@ # ENG-013 — Repository provisioning via the PM agent (git + gh) & shared workspace -- **Status:** backlog +- **Status:** done - **Priority:** P0 (Critical — prerequisite for ENG-001) - **Stage:** discovery → implementation - **Assignee role:** pm (owns repo create / list / clone); supporting backend work (per-role tool @@ -8,6 +8,27 @@ - **Area:** packages/core (role config) · packages/agents (CLI backend tool gating + auth) · apps/server (shared workspace + audit) +> **Outcome (Wave 2):** +> - **Two-root workspace** — `packages/agents/src/workspace.ts` (`resolveCodeWorkspaceRoot` / +> `repoWorkspacePath`): a *persistent* working-code root (`workspace/` singular, env +> `AGENT_CODE_WORKSPACE`) for cloned target repos, distinct from the throwaway `workspaces/` +> agent-state sandboxes. `/workspace/` gitignored. +> - **PM tool grant** — `ROLES.pm.tools` now includes `git` + `gh`, and the PM prompt covers +> list/reuse/create/clone (idempotent). +> - **Scoped-token enforcement** — `sandboxEnv(source, role)` forwards `AGENT_PM_GITHUB_TOKEN` as +> `GITHUB_TOKEN`/`GH_TOKEN` **only** to the PM sandbox; the host `GITHUB_TOKEN` is never forwarded to +> any agent. This is the real security boundary (only the PM can authenticate git/gh). +> - **Idempotent provisioning** — `provision.ts` (`repoTargetFromEnv` / `provisionAction` / `gitArgs` / +> `ensureRepoCloned`): shallow clone-or-ff-pull into the working-code workspace, token passed via +> `http.extraheader` (never written to `.git/config`); no-op when GitHub unconfigured. Wired into +> `implementTicket` with `repo_context_ready` / `repo_context_skipped` audit events. +> - Tests: workspace layout, provisioning (skip/action/args), PM-scoped token. +> +> *Deferred (noted):* (1) literal `claude --allowedTools` tool-name gating — the security boundary is +> enforced via the scoped token, so the abstract→Claude-Code tool-name mapping is a follow-up; (2) +> per-epic repo *association* — the target repo is the configured `GITHUB_OWNER`/`GITHUB_REPO` (same as +> delivery) for now; moving repo selection onto the Goal/Epic is a follow-up. + ## Decision Per the product owner's direction, the **PM agent owns repository provisioning** and is granted two diff --git a/project/ROADMAP.md b/project/ROADMAP.md index 6a0d4d3..749fdde 100644 --- a/project/ROADMAP.md +++ b/project/ROADMAP.md @@ -33,9 +33,12 @@ high-leverage enablers forward even when labelled P1/P2. parse-failure vs empty-set audited distinctly). - **ENG-005** ✅ — Scrub the CLI sandbox env (allow-listed `sandboxEnv()`; host secrets withheld). -### Wave 2 — Repo-context critical path -- **ENG-013** — Repo provisioning (PM `git`/`gh`) + two-root workspace. Needs ENG-005. -- **ENG-001** — Repo-aware coding agents. Needs ENG-013; benefits from ENG-009. +### Wave 2 — Repo-context critical path — ✅ complete (2026-06-27) +- **ENG-013** ✅ — Two-root workspace + PM `git`/`gh` grant + PM-scoped token + idempotent + `ensureRepoCloned`, wired+audited in `implementTicket`. (Deferred: literal `--allowedTools` gating; + per-epic repo association.) +- **ENG-001** ✅ — `WorkerInput.workdir`; CLI backend runs in the cloned repo cwd so agents read/edit + real source. (Deferred: API-backend file-context injection.) ### Wave 3 — Governance / observability - **ENG-007** — Budget model (reset + reservation). Do first in this wave — gates 010 and 008.