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
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ When writing complex features or significant refactors, use an ExecPlan (as desc

- Never edit, stage, or commit on `dev` / `main`. Open an `agent/*` branch + worktree first.
- Claim files before edits: `gx locks claim --branch "<agent-branch>" <file...>` (or Colony `task_claim_file` on an active task).
- Finish completed work with `gx branch finish --branch "<agent-branch>" --via-pr --wait-for-merge --cleanup`. Never stop at bare `--via-pr`.
- Finish completed work with `gx branch finish --branch "<agent-branch>" --via-pr --wait-for-merge --cleanup`. Never stop at bare `--via-pr`. `gx ship` is the short alias for that exact gated finish.
- When work is complete, always offer to finish and merge it — never leave commits stranded in a worktree. Set `GUARDEX_AUTO_SHIP=1` to make a bare `gx finish` / `gx branch finish` default to that gated ship automatically (see Toggle).
- Commit, push, and open/update a PR for completed work unless the user explicitly says to keep it local.
- Use OpenSpec for change-driven work; create/update `openspec/changes/<slug>/` before editing code (helper agent sub-branches excepted).
- Keep outputs compact: less word, same proof.
Expand Down Expand Up @@ -144,6 +145,8 @@ If a change publishes or bumps a package version, the same change must also upda

Guardex is enabled by default. Disable via repo-root `.env` with `GUARDEX_ON=0|false|no|off`. Re-enable with `GUARDEX_ON=1`.

**Auto-ship** (opt-in, off by default): set `GUARDEX_AUTO_SHIP=1` to make a bare `gx finish` / `gx branch finish` behave like `gx ship` — open a PR from the worktree, wait for the merge into base, clean up the sandbox, and enforce the merge gate (clean AI review + green CI) before merging. Explicit flags still win (e.g. `--no-gate-review` opts back out of the gate, `--direct-only` out of the PR). via-pr / wait-for-merge / cleanup are already the finish defaults, so the toggle's net effect is turning the gate on so unattended merges stay safe.

### Core rules

- Work from an `agent/*` branch + worktree. **Never** edit the protected base directly.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-29
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# agent-claude-add-guardex-auto-ship-opt-in-gated-auto-2026-06-29-08-28 (minimal / T1)

Branch: `agent/claude/add-guardex-auto-ship-opt-in-gated-auto-2026-06-29-08-28`

Add an opt-in `GUARDEX_AUTO_SHIP=1` env toggle so a bare `gx finish` / `gx branch finish`
defaults to the gated ship (open PR from worktree → wait for merge → cleanup, with the
clean-AI-review + green-CI merge gate). Lowers agent friction without weakening the guardrail:
the only default flipped is `gateReview` (via-pr / wait-for-merge / cleanup are already
defaults), and explicit flags (`--no-gate-review`, `--direct-only`) still override.

## Files

- `src/cli/args.js` — `parseFinishArgs` reads `GUARDEX_AUTO_SHIP` (via `envFlagIsTruthy`) and
flips the `gateReview` default to `true` when set. Single insertion point: `gx finish`,
`gx branch finish`, and `gx ship` all resolve options here.
- `test/auto-ship-toggle.test.js` — toggle on → resolves like `gx ship`; explicit `--no-gate-review`
/ `--skip-review-gate` win; toggle unset/falsy → defaults unchanged.
- `AGENTS.md` — documents the toggle + `gx ship` short form, and adds the "always offer to
finish/merge" contract line.

## Verification

- `node --test test/auto-ship-toggle.test.js` → 4/4 pass.
- `npm test` → 718 pass / 27 fail / 1 skip; failing set byte-identical to base `main` (27 = 27,
zero new failures — repo's `test` job is baseline-red).

## Handoff

- Handoff: change=`agent-claude-add-guardex-auto-ship-opt-in-gated-auto-2026-06-29-08-28`; branch=`agent/claude/add-guardex-auto-ship-opt-in-gated-auto-2026-06-29-08-28`; scope=`opt-in GUARDEX_AUTO_SHIP gated-finish toggle + docs`; action=`finish via gated PR`.

## Cleanup

- [ ] Run: `gx branch finish --branch agent/claude/add-guardex-auto-ship-opt-in-gated-auto-2026-06-29-08-28 --base main --via-pr --wait-for-merge --cleanup`
- [ ] Record PR URL + `MERGED` state in the completion handoff.
- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`).
14 changes: 13 additions & 1 deletion src/cli/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const {
path,
DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES,
TARGETED_FORCEABLE_MANAGED_PATHS,
envFlagIsTruthy,
} = require('../context');
const { DEFAULT_NESTED_REPO_MAX_DEPTH } = require('../git');

Expand Down Expand Up @@ -1140,6 +1141,13 @@ function parseMergeArgs(rawArgs) {
}

function parseFinishArgs(rawArgs, defaults = {}) {
// `GUARDEX_AUTO_SHIP=1` (opt-in) makes a bare `gx finish` / `gx branch finish`
// behave like `gx ship`: open a PR from the worktree, wait for merge, clean up,
// and enforce the merge gate (clean AI review + green CI) before merging to base.
// via-pr / wait-for-merge / cleanup are already the defaults, so the only delta
// is flipping the gate-review default on. Explicit flags (e.g. --no-gate-review)
// still override below.
const autoShip = envFlagIsTruthy(process.env.GUARDEX_AUTO_SHIP);
const options = {
target: process.cwd(),
base: '',
Expand All @@ -1156,7 +1164,11 @@ function parseFinishArgs(rawArgs, defaults = {}) {
commitMessage: '',
mergeMode: defaults.mergeMode || 'pr',
skipPreflight: false,
gateReview: defaults.gateReview ?? false,
// Precedence: explicit flag (set in the loop below) > defaults (caller) >
// GUARDEX_AUTO_SHIP env > hardcoded off. Note `defaults.gateReview === false`
// (not undefined) also wins over the env toggle, since `??` only falls
// through on null/undefined.
gateReview: defaults.gateReview ?? autoShip,
reviewProvider: defaults.reviewProvider || 'codex',
allowNoChecks: false,
};
Expand Down
76 changes: 76 additions & 0 deletions test/auto-ship-toggle.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const test = require('node:test');
const assert = require('node:assert/strict');

const { parseFinishArgs } = require('../src/cli/args');

// Run `fn` with GUARDEX_AUTO_SHIP forced to `value` (or deleted when null),
// restoring the prior value afterwards so tests stay isolated.
function withAutoShip(value, fn) {
const prev = process.env.GUARDEX_AUTO_SHIP;
if (value === null) {
delete process.env.GUARDEX_AUTO_SHIP;
} else {
process.env.GUARDEX_AUTO_SHIP = value;
}
try {
fn();
} finally {
if (prev === undefined) {
delete process.env.GUARDEX_AUTO_SHIP;
} else {
process.env.GUARDEX_AUTO_SHIP = prev;
}
}
}

test('GUARDEX_AUTO_SHIP=1 makes a bare finish resolve like `gx ship` (gated)', () => {
withAutoShip('1', () => {
const options = parseFinishArgs([]);
assert.equal(options.gateReview, true, 'gate-review default flips on under auto-ship');
assert.equal(options.mergeMode, 'pr', 'merges via PR');
assert.equal(options.waitForMerge, true, 'waits for merge');
assert.equal(options.cleanup, true, 'cleans up the worktree');
});
});

test('explicit --no-gate-review wins over GUARDEX_AUTO_SHIP', () => {
withAutoShip('1', () => {
const options = parseFinishArgs(['--no-gate-review']);
assert.equal(options.gateReview, false, 'explicit opt-out overrides the toggle');
});
withAutoShip('1', () => {
const options = parseFinishArgs(['--skip-review-gate']);
assert.equal(options.gateReview, false, '--skip-review-gate also overrides the toggle');
});
});

test('without GUARDEX_AUTO_SHIP the finish defaults are unchanged', () => {
withAutoShip(null, () => {
const options = parseFinishArgs([]);
assert.equal(options.gateReview, false, 'gate-review stays opt-in by default');
// The rest of the finish defaults are independent of the toggle.
assert.equal(options.mergeMode, 'pr');
assert.equal(options.waitForMerge, true);
assert.equal(options.cleanup, true);
});
});

test('caller defaults still drive gateReview independently of the env toggle', () => {
// defaults win over the env when set: explicit true enables the gate with the
// toggle off; explicit false suppresses it even with the toggle on.
withAutoShip(null, () => {
assert.equal(parseFinishArgs([], { gateReview: true }).gateReview, true);
});
withAutoShip('1', () => {
assert.equal(parseFinishArgs([], { gateReview: false }).gateReview, false);
});
});

test('GUARDEX_AUTO_SHIP falsy values are treated as off', () => {
for (const falsy of ['0', 'false', 'no', 'off', '']) {
withAutoShip(falsy, () => {
const options = parseFinishArgs([]);
assert.equal(options.gateReview, false, `"${falsy}" must not enable the gate`);
});
}
});
Loading