diff --git a/bin/multiagent-safety.js b/bin/multiagent-safety.js index 2025e494..d972b4a3 100755 --- a/bin/multiagent-safety.js +++ b/bin/multiagent-safety.js @@ -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', @@ -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)) { diff --git a/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/proposal.md b/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/proposal.md new file mode 100644 index 00000000..c2bed6a2 --- /dev/null +++ b/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/proposal.md @@ -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`. diff --git a/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/specs/ignore-scripts-star-and-githooks/spec.md b/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/specs/ignore-scripts-star-and-githooks/spec.md new file mode 100644 index 00000000..eeb096e5 --- /dev/null +++ b/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/specs/ignore-scripts-star-and-githooks/spec.md @@ -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 ` +- **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 ` or `gx setup --target ` +- **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 ` +- **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 ` +- **THEN** the nested repo regains `AGENTS.md` +- **AND** the nested repo's managed `.gitignore` block contains `scripts/*` and `.githooks` diff --git a/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/tasks.md b/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/tasks.md new file mode 100644 index 00000000..2d1cbac3 --- /dev/null +++ b/openspec/changes/agent-codex-ignore-scripts-star-and-githooks-2026-04-21-10-40/tasks.md @@ -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. diff --git a/test/install.test.js b/test/install.test.js index c939b1cb..b8066003 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -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\//); @@ -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); @@ -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); @@ -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); @@ -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/);