diff --git a/openspec/changes/doctor-current-single-repo-alias/proposal.md b/openspec/changes/doctor-current-single-repo-alias/proposal.md new file mode 100644 index 0000000..5e62336 --- /dev/null +++ b/openspec/changes/doctor-current-single-repo-alias/proposal.md @@ -0,0 +1,17 @@ +## Why + +- `gx doctor` recurses into nested repos by default, so users need a short explicit way to keep a repair pass scoped to the target repo only. +- `--single-repo` already provides that behavior, but `--current` is currently rejected and the recursive hint text does not advertise it. +- The user explicitly wants `gx doctor --current` to leave nested repos under the target path untouched. + +## What Changes + +- Accept `--current` as a doctor-only alias for `--single-repo`. +- Update the recursive doctor hint to mention `--current`. +- Add regression coverage proving a nested repo under the target path stays broken when `gx doctor --current` is used. + +## Impact + +- Affected surface: `src/cli/main.js`, `test/doctor.test.js`. +- Expected outcome: `gx doctor --current` scopes repairs to the target repo without mutating nested repos. +- Risk: low, because the alias is wired only inside `parseDoctorArgs()` and reuses existing single-repo behavior. diff --git a/openspec/changes/doctor-current-single-repo-alias/specs/nested-repo-doctoring/spec.md b/openspec/changes/doctor-current-single-repo-alias/specs/nested-repo-doctoring/spec.md new file mode 100644 index 0000000..2f7abfb --- /dev/null +++ b/openspec/changes/doctor-current-single-repo-alias/specs/nested-repo-doctoring/spec.md @@ -0,0 +1,10 @@ +## ADDED Requirements + +### Requirement: doctor current alias limits repairs to the target repo +The system SHALL support `gx doctor --current` as a doctor-only alias for the existing single-repo repair path. + +#### Scenario: current alias skips nested repo repairs +- **GIVEN** a parent repo contains a nested standalone git repo with Guardex-managed drift +- **WHEN** `gx doctor --target --current` runs +- **THEN** the doctor flow SHALL repair only `` +- **AND** the nested repo SHALL not be traversed or repaired during that run. diff --git a/openspec/changes/doctor-current-single-repo-alias/tasks.md b/openspec/changes/doctor-current-single-repo-alias/tasks.md new file mode 100644 index 0000000..8213e5b --- /dev/null +++ b/openspec/changes/doctor-current-single-repo-alias/tasks.md @@ -0,0 +1,32 @@ +## 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, add a `BLOCKED:` line under section 4 and stop. + +## 1. Specification + +- [x] 1.1 Capture the `gx doctor --current` alias scope and acceptance criteria. +- [x] 1.2 Add normative OpenSpec coverage for the single-repo alias behavior. + +## 2. Implementation + +- [x] 2.1 Accept `--current` as a doctor-only alias for `--single-repo`. +- [x] 2.2 Update the recursive doctor hint text to mention `--current`. +- [x] 2.3 Add a regression proving nested repos under the target path stay untouched. + +## 3. Verification + +- [x] 3.1 Run `node --check bin/multiagent-safety.js`. +- [x] 3.2 Run `node --test test/doctor.test.js`. +- [x] 3.3 Run `openspec validate doctor-current-single-repo-alias --type change --strict`. +- [x] 3.4 Run `openspec validate --specs`. + +## 4. Cleanup + +- [ ] 4.1 Commit the change with a Lore commit message. +- [ ] 4.2 Run `gx branch finish --branch agent/codex/scope-gx-doctor-current-to-current-repo-2026-04-22-13-13 --via-pr --wait-for-merge --cleanup`. +- [ ] 4.3 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.4 Confirm the sandbox worktree and branch refs are gone after cleanup. diff --git a/src/cli/main.js b/src/cli/main.js index b0a5ce2..1f20901 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -2088,6 +2088,10 @@ function parseDoctorArgs(rawArgs) { for (let index = 0; index < rawArgs.length; index += 1) { const arg = rawArgs[index]; + if (arg === '--current') { + forwardedArgs.push('--single-repo'); + continue; + } if (arg === '--verbose-auto-finish') { doctorDefaults.verboseAutoFinish = true; continue; @@ -6056,7 +6060,7 @@ function doctor(rawArgs) { if (!options.json) { console.log( `[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. ` + - `Repairing each with doctor (use --single-repo to limit to the target).`, + `Repairing each with doctor (use --single-repo or --current to limit to the target).`, ); } diff --git a/test/doctor.test.js b/test/doctor.test.js index 1da9b49..1db8497 100644 --- a/test/doctor.test.js +++ b/test/doctor.test.js @@ -830,6 +830,59 @@ test('doctor recurses into nested frontend repos and repairs protected-main drif }); +test('doctor --current limits repairs to the target repo only', () => { + 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); + const initialFrontendGitignore = fs.readFileSync(frontendGitignorePath, 'utf8'); + + fs.rmSync(path.join(frontendDir, 'AGENTS.md')); + fs.rmSync(path.join(frontendDir, 'scripts', 'guardex-env.sh')); + fs.rmSync(path.join(frontendDir, '.githooks', 'pre-commit')); + fs.writeFileSync( + frontendGitignorePath, + initialFrontendGitignore + .replace(/^scripts\/guardex-env\.sh\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, '--current'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.doesNotMatch(result.stdout, /Detected 2 git repos under/); + assert.doesNotMatch(result.stdout, new RegExp(`Doctor target: ${escapeRegexLiteral(frontendDir)}`)); + + assert.equal(fs.existsSync(path.join(frontendDir, 'AGENTS.md')), false, 'nested frontend AGENTS.md should stay broken'); + assert.equal( + fs.existsSync(path.join(frontendDir, 'scripts', 'guardex-env.sh')), + false, + 'nested frontend managed script should stay broken', + ); + assert.equal( + fs.existsSync(path.join(frontendDir, '.githooks', 'pre-commit')), + false, + 'nested frontend hook should stay broken', + ); + assert.equal(fs.readFileSync(frontendGitignorePath, 'utf8'), initialFrontendGitignore + .replace(/^scripts\/guardex-env\.sh\n/m, '') + .replace(/^\.githooks\n/m, '')); + assert.equal( + fs.readFileSync(path.join(frontendDir, '.omx', 'state', 'agent-file-locks.json'), 'utf8'), + '{broken json', + ); +}); + + test('recursive doctor forwards no-wait-for-merge to protected nested sandbox repairs', () => { const repoDir = initRepo(); const frontendDir = path.join(repoDir, 'frontend');