Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-08
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Why

A fresh agent worktree is a clean checkout: gitignored files (`.env`,
`node_modules`, `.venv`) don't exist, so agents start broken until someone
copies/links them. gitguardex already auto-provisions this — but only for the
hardcoded `apps/*` monorepo shape (symlink `apps/<pkg>/.env`, assign dev ports).
Any other repo layout gets nothing. workmux solves this declaratively with a
per-repo config (`files.copy`/`files.symlink` + `post_create` hooks); this ports
that idea so any repo can describe its own provisioning.

## What Changes

- Add a per-repo **`.guardex.json`** config (parsed with the existing
`jsonc-parser`, so comments are allowed — no new dependency) with a
`provision` block:

```jsonc
{ "provision": {
"files": { "copy": [".env", "apps/*/.env"], "symlink": ["node_modules", ".venv"] },
"postCreate": ["pnpm install --offline"] } }
```

- Add `src/scaffold/provision-config.js`: loader + a minimal dependency-free glob
(literal segments + a single-segment `*`, e.g. `apps/*/.env`) + appliers for
copy, symlink, and postCreate hooks. All best-effort: a missing config, a
no-match pattern, or a failing hook never throws fatally.
- Wire it into `prepareAgentWorktree` (already auto-invoked on worktree
creation) so declarative provisioning runs for **any** repo, ahead of the
existing `apps/*` convenience which stays as the zero-config default.

## Impact

- Additive only; the existing `apps/*` env-symlink + dev-port behavior is
unchanged (no longer gated behind an early-return, so non-monorepo repos now
also get declarative provisioning).
- `copy`/`symlink` are pure filesystem ops. **Trust model:** `postCreate`
executes shell commands from the repo owner's committed `.guardex.json` (same
trust as `package.json` scripts or workmux `post_create`); it reads only the
trusted repo-root config, logs each command, is non-fatal, and is disabled
with `GUARDEX_PROVISION_HOOKS=0`.
- Glob is intentionally minimal (one `*` per segment, no `**`/braces); patterns
with absolute paths or `..` are rejected to keep provisioning inside the repo.
- Affected: `src/scaffold/provision-config.js` (new),
`src/scaffold/agent-worktree-prep.js`, `test/provision-config.test.js` (new).
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
## ADDED Requirements

### Requirement: Declarative worktree provisioning config
The system SHALL read an optional `.guardex.json` at the repo root and, when it
contains a `provision` object, apply it to a freshly created agent worktree. The
config SHALL be parsed permitting comments and trailing commas. A missing file,
unparseable JSON, or a missing `provision` block SHALL be treated as "no
declarative provisioning" without error. `provision.files.copy`,
`provision.files.symlink`, and `provision.postCreate` SHALL each normalize to a
list of non-empty strings, dropping any other value.

#### Scenario: Config is loaded and normalized
- **WHEN** a repo's `.guardex.json` has `provision.files.copy` of `[".env", 5]`
- **THEN** the loaded copy list is `[".env"]` (the non-string is dropped).

#### Scenario: Absent or malformed config is inert
- **WHEN** there is no `.guardex.json`, or it is malformed, or it has no
`provision` block
- **THEN** provisioning loads nothing and the worktree is created normally.

### Requirement: Copy and symlink provisioning into the worktree
For each `files.copy` pattern the system SHALL copy each matching repo-root file
into the worktree at the same relative path; for each `files.symlink` pattern it
SHALL create a symlink in the worktree pointing at the repo-root path. Existing
worktree paths SHALL NOT be overwritten. Copy SHALL apply to files only
(directories are skipped with a note; use symlink for directories). Patterns
SHALL support literal segments and a single-segment `*` wildcard (e.g.
`apps/*/.env`); patterns that are absolute or contain `..` SHALL be rejected.

#### Scenario: Files copied, directories symlinked
- **WHEN** `copy` is `[".env"]` and `symlink` is `["node_modules"]` for a repo
that has both
- **THEN** the worktree gets a real copy of `.env` and a symlink `node_modules`
pointing at the repo root
- **AND** re-running leaves the already-present entries unchanged.

#### Scenario: Unsafe pattern rejected
- **WHEN** a pattern is `../secrets` or `/etc/passwd`
- **THEN** it matches nothing and no file outside the repo is touched.

### Requirement: post_create hooks with a trust boundary
For each `provision.postCreate` command the system SHALL run it as a shell
command with the worktree as the working directory and with `GUARDEX_WORKTREE`
and `GUARDEX_REPO_ROOT` in the environment. Hooks SHALL run only from the trusted
repo-root config, SHALL be non-fatal (a failing hook is recorded, not thrown),
and SHALL be skippable via `GUARDEX_PROVISION_HOOKS=0`.

#### Scenario: Hook runs in the worktree
- **WHEN** `postCreate` is `["pnpm install"]`
- **THEN** the command runs with cwd = the worktree and `GUARDEX_WORKTREE` set.

#### Scenario: Hooks disabled
- **WHEN** `GUARDEX_PROVISION_HOOKS=0` is set
- **THEN** no postCreate command runs and each is recorded as skipped.

### Requirement: Provisioning runs for any repo layout
Declarative provisioning SHALL run on worktree creation regardless of whether the
repo has an `apps/*` monorepo layout, and SHALL NOT regress the existing `apps/*`
env-symlink and per-app dev-port behavior.

#### Scenario: Non-monorepo repo is provisioned
- **WHEN** a repo with no `apps/` directory has a `.guardex.json` provision block
and a worktree is prepared
- **THEN** the declared copy/symlink operations are applied to the worktree.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Definition of Done

This change is complete only when **all** of the following are true:

- Every checkbox below is checked.
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.

## Handoff

- Handoff: change=`agent-claude-workmux-w2-declarative-guardex-yaml-work-2026-06-08-08-54`; branch=`agent/<your-name>/<branch-slug>`; scope=`.guardex.json declarative worktree provisioning`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`.
- Copy prompt: Continue `agent-claude-workmux-w2-declarative-guardex-yaml-work-2026-06-08-08-54` on branch `agent/<your-name>/<branch-slug>`. Work inside the existing sandbox, review `openspec/changes/agent-claude-workmux-w2-declarative-guardex-yaml-work-2026-06-08-08-54/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/<your-name>/<branch-slug> --base main --via-pr --wait-for-merge --cleanup`.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-claude-workmux-w2-declarative-guardex-yaml-work-2026-06-08-08-54`.
- [x] 1.2 Define normative requirements in `specs/workmux-w2-declarative-guardex-yaml-worktree-provisioning-files-copy-symlink-and-post-create-hooks/spec.md`.

## 2. Implementation

- [x] 2.1 Add `src/scaffold/provision-config.js` (loader + minimal glob + copy/symlink/postCreate appliers).
- [x] 2.2 Wire `provisionFromConfig` into `prepareAgentWorktree` (runs for any repo, ahead of apps/* default).
- [x] 2.3 Add `test/provision-config.test.js` regression coverage (8 cases incl. trust-boundary + unsafe patterns).

## 3. Verification

- [x] 3.1 `node --test test/provision-config.test.js` (8 pass); full-suite failing set byte-identical to base (28=28, zero new).
- [x] 3.2 Run `openspec validate agent-claude-workmux-w2-declarative-guardex-yaml-work-2026-06-08-08-54 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

## 4. Cleanup (mandatory; run before claiming completion)

- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/<your-name>/<branch-slug> --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).
11 changes: 9 additions & 2 deletions src/scaffold/agent-worktree-prep.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { provisionFromConfig } = require('./provision-config');

const ENV_FILE_CANDIDATES = [
'.env',
Expand Down Expand Up @@ -190,10 +191,16 @@ function prepareAgentWorktree(repoRoot, worktreePath) {
if (!repoRoot || !worktreePath) return [];
if (repoRoot === worktreePath) return [];
if (!fs.existsSync(worktreePath)) return [];
const apps = detectAppPackages(repoRoot);
if (apps.length === 0) return [];

const operations = [];

// Declarative `.guardex.json` provisioning runs for ANY repo (monorepo or
// not) — copy/symlink gitignored files and run post_create hooks.
operations.push(...provisionFromConfig(repoRoot, worktreePath));

// Built-in apps/* monorepo convenience: env-file symlinks + a free dev port
// per app. Stays as the zero-config default for monorepos.
const apps = detectAppPackages(repoRoot);
const takenPorts = new Set();
for (const appName of apps) {
operations.push(...symlinkAppEnvFiles(repoRoot, worktreePath, appName));
Expand Down
Loading
Loading