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
18 changes: 3 additions & 15 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,8 @@ const GITIGNORE_MARKER_END = '# multiagent-safety:END';
const MANAGED_GITIGNORE_PATHS = [
'.omx/',
'.omc/',
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/codex-agent.sh',
'scripts/review-bot-watch.sh',
'scripts/agent-worktree-prune.sh',
'scripts/agent-file-locks.py',
'scripts/guardex-env.sh',
'scripts/install-agent-git-hooks.sh',
'scripts/openspec/init-plan-workspace.sh',
'scripts/openspec/init-change-workspace.sh',
'.githooks/pre-commit',
'.githooks/pre-push',
'.githooks/post-merge',
'.githooks/post-checkout',
'scripts/*',
'.githooks',
'oh-my-codex/',
'.codex/skills/gitguardex/SKILL.md',
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
Expand Down Expand Up @@ -1592,7 +1580,7 @@ function finishDoctorSandboxBranch(blocked, metadata) {

const finishResult = run(
'bash',
[finishScript, '--branch', metadata.branch, '--via-pr', '--wait-for-merge'],
[finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr', '--wait-for-merge'],
{ cwd: metadata.worktreePath, timeout: finishTimeoutMs },
);
if (isSpawnFailure(finishResult)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Why

- The managed `.gitignore` block currently lists Guardex-owned `scripts/...` files and `.githooks/...` files one by one. That works, but it is noisy and drifts whenever new bootstrap files land under those directories.
- Users expect `gx setup` / `gx doctor` to ignore the Guardex-managed script surface and the `.githooks` directory as a whole, not a hand-maintained list of individual files.
- Users also expect `AGENTS.md` to come back when Guardex repairs a repo, especially on protected `main` where `gx doctor` has to repair through the sandbox flow.

## What Changes

- `bin/multiagent-safety.js`:
- Replace the per-file managed `.gitignore` entries for Guardex bootstrap scripts with a single `scripts/*` entry.
- Replace the per-file managed `.gitignore` entries for git hooks with a single `.githooks` entry.
- Keep protected-branch doctor auto-finish on the actual protected base branch instead of falling back to the default `dev` base.
- `test/install.test.js`:
- Update setup assertions to require the wildcard-managed entries instead of the old per-file ignore lines.
- Extend protected-`main` and nested-repo doctor regressions so they prove `AGENTS.md` is restored and the wildcard `.gitignore` entries are repaired.

## Impact

- **New behavior**: `gx setup` / `gx doctor` write a smaller managed `.gitignore` block with `scripts/*` and `.githooks`.
- **Repair proof**: the regression suite now pins `AGENTS.md` restoration for protected-`main` doctor flow and nested repo doctor flow, alongside the new wildcard ignore entries.
- **Out of scope**: no package version change is needed here because `package.json` and `README.md` are already at `7.0.13`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## ADDED Requirements

### Requirement: Managed `.gitignore` block ignores Guardex bootstrap directories with wildcard entries

The marker-delimited `.gitignore` block written by `gx setup` and refreshed by `gx doctor` SHALL ignore the Guardex-managed bootstrap directories using stable directory-wide entries instead of enumerating individual files.

#### Scenario: Fresh setup writes wildcard Guardex ignore entries
- **GIVEN** a repo without an existing managed `.gitignore` block
- **WHEN** the user runs `gx setup --target <repo>`
- **THEN** the resulting managed block contains `scripts/*`
- **AND** the resulting managed block contains `.githooks`

#### Scenario: Repair refresh rewrites older per-file ignore entries
- **GIVEN** a repo whose managed `.gitignore` block was written by an earlier Guardex version with per-file `scripts/...` and `.githooks/...` entries
- **WHEN** the user runs `gx doctor --target <repo>` or `gx setup --target <repo>`
- **THEN** the managed block is rewritten to contain `scripts/*` and `.githooks`
- **AND** the managed block no longer depends on individual script or hook path entries for Guardex-managed files

### Requirement: Doctor repairs restore `AGENTS.md` alongside wildcard ignore entries

When `gx doctor` repairs Guardex drift, the repair flow SHALL restore `AGENTS.md` and the managed wildcard `.gitignore` entries together, including protected-branch sandbox repairs and nested-repo repairs.

#### Scenario: Protected-main doctor restores AGENTS and wildcard ignore entries
- **GIVEN** a protected-`main` repo where `AGENTS.md` has drifted away
- **WHEN** the user runs `gx doctor --target <repo>`
- **THEN** the repo regains `AGENTS.md`
- **AND** its managed `.gitignore` block contains `scripts/*` and `.githooks`
- **AND** the protected-branch finish flow keeps `main` as the base branch instead of falling back to `dev`

#### Scenario: Recursive doctor restores nested repo AGENTS and wildcard ignore entries
- **GIVEN** a parent repo with a nested standalone frontend repo on protected `main`
- **AND** the nested repo is missing `AGENTS.md`
- **AND** the nested repo's managed `.gitignore` block is missing `scripts/*` and `.githooks`
- **WHEN** the user runs `gx doctor --target <parent-repo>`
- **THEN** the nested repo regains `AGENTS.md`
- **AND** the nested repo's managed `.gitignore` block contains `scripts/*` and `.githooks`
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40`.
- [x] 1.2 Define normative requirements in `specs/ignore-scripts-star-and-githooks/spec.md`.

## 2. Implementation

- [x] 2.1 Replace per-file script entries in `MANAGED_GITIGNORE_PATHS` with `scripts/*`.
- [x] 2.2 Replace per-file hook entries in `MANAGED_GITIGNORE_PATHS` with `.githooks`.
- [x] 2.3 Keep doctor sandbox auto-finish on the current protected base branch so main-only repos do not fall back to `dev`.
- [x] 2.4 Update setup and doctor regressions in `test/install.test.js` to pin the wildcard-managed entries and `AGENTS.md` restoration.
- [x] 2.5 Confirm no version bump is needed because the repo is already on `7.0.13`.

## 3. Verification

- [x] 3.1 `node --check bin/multiagent-safety.js`
- [x] 3.2 Focused `node --test` coverage for the affected setup/doctor cases.
- [x] 3.3 `openspec validate agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40 --type change --strict`

## 4. Cleanup

- [ ] 4.1 Finish the agent branch via PR merge + cleanup after verification.
34 changes: 25 additions & 9 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,15 +407,10 @@ test('setup provisions workflow files and repo config', () => {

const gitignoreContent = fs.readFileSync(path.join(repoDir, '.gitignore'), 'utf8');
assert.match(gitignoreContent, /# multiagent-safety:START/);
assert.match(gitignoreContent, /scripts\/agent-branch-start\.sh/);
assert.match(gitignoreContent, /scripts\/codex-agent\.sh/);
assert.match(gitignoreContent, /scripts\/review-bot-watch\.sh/);
assert.match(gitignoreContent, /scripts\/agent-file-locks\.py/);
assert.match(gitignoreContent, /scripts\/guardex-env\.sh/);
assert.match(gitignoreContent, /scripts\/openspec\/init-change-workspace\.sh/);
assert.match(gitignoreContent, /\.githooks\/pre-commit/);
assert.match(gitignoreContent, /\.githooks\/pre-push/);
assert.match(gitignoreContent, /\.githooks\/post-merge/);
assert.match(gitignoreContent, /^scripts\/\*$/m);
assert.match(gitignoreContent, /^\.githooks$/m);
assert.doesNotMatch(gitignoreContent, /^scripts\/agent-branch-start\.sh$/m);
assert.doesNotMatch(gitignoreContent, /^\.githooks\/pre-commit$/m);
assert.match(gitignoreContent, /\.omx\//);
assert.match(gitignoreContent, /\.omc\//);
assert.match(gitignoreContent, /oh-my-codex\//);
Expand Down Expand Up @@ -1092,6 +1087,10 @@ exit 1
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /Auto-committed doctor repairs in sandbox branch/);
assert.match(result.stdout, /Auto-finish flow completed for sandbox branch/);
assert.equal(fs.existsSync(path.join(repoDir, 'AGENTS.md')), true, 'protected main checkout should regain AGENTS.md');
const repairedRootGitignore = fs.readFileSync(path.join(repoDir, '.gitignore'), 'utf8');
assert.match(repairedRootGitignore, /^scripts\/\*$/m);
assert.match(repairedRootGitignore, /^\.githooks$/m);

const createdBranch = extractCreatedBranch(result.stdout);
result = runCmd('git', ['show-ref', '--verify', '--quiet', `refs/heads/${createdBranch}`], repoDir);
Expand Down Expand Up @@ -2360,6 +2359,8 @@ test('setup appends managed gitignore block without clobbering existing entries'
const first = fs.readFileSync(path.join(repoDir, '.gitignore'), 'utf8');
assert.match(first, /node_modules\//);
assert.match(first, /# multiagent-safety:START/);
assert.match(first, /^scripts\/\*$/m);
assert.match(first, /^\.githooks$/m);
assert.match(first, /# multiagent-safety:END/);

result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
Expand Down Expand Up @@ -3913,19 +3914,31 @@ test('doctor repairs setup drift and confirms repo is safe', () => {
test('doctor recurses into nested frontend repos and repairs protected-main drift', () => {
const repoDir = initRepo();
const frontendDir = path.join(repoDir, 'frontend');
const frontendGitignorePath = path.join(frontendDir, '.gitignore');
fs.mkdirSync(frontendDir, { recursive: true });

let result = runCmd('git', ['init', '-b', 'main'], frontendDir);
assert.equal(result.status, 0, result.stderr || result.stdout);
fs.writeFileSync(path.join(frontendDir, 'package.json'), '{}\n', 'utf8');
seedCommit(frontendDir);

result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(fs.existsSync(path.join(frontendDir, 'AGENTS.md')), true, 'nested frontend should be bootstrapped by setup');
const initialFrontendGitignore = fs.readFileSync(frontendGitignorePath, 'utf8');
assert.match(initialFrontendGitignore, /^scripts\/\*$/m);
assert.match(initialFrontendGitignore, /^\.githooks$/m);

fs.rmSync(path.join(frontendDir, 'AGENTS.md'));
fs.rmSync(path.join(frontendDir, 'scripts', 'agent-branch-start.sh'));
fs.rmSync(path.join(frontendDir, '.githooks', 'pre-commit'));
fs.writeFileSync(
frontendGitignorePath,
initialFrontendGitignore
.replace(/^scripts\/\*\n/m, '')
.replace(/^\.githooks\n/m, ''),
'utf8',
);
fs.writeFileSync(path.join(frontendDir, '.omx', 'state', 'agent-file-locks.json'), '{broken json', 'utf8');

result = runNode(['doctor', '--target', repoDir], repoDir);
Expand All @@ -3940,6 +3953,9 @@ test('doctor recurses into nested frontend repos and repairs protected-main drif
true,
'nested frontend sandbox starter should be restored',
);
const repairedFrontendGitignore = fs.readFileSync(frontendGitignorePath, 'utf8');
assert.match(repairedFrontendGitignore, /^scripts\/\*$/m);
assert.match(repairedFrontendGitignore, /^\.githooks$/m);
const repairedFrontendHook = fs.readFileSync(path.join(frontendDir, '.githooks', 'pre-commit'), 'utf8');
assert.match(repairedFrontendHook, /AGENTS\.md\|\.gitignore/);

Expand Down