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
17 changes: 17 additions & 0 deletions openspec/changes/doctor-current-single-repo-alias/proposal.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 <parent-repo> --current` runs
- **THEN** the doctor flow SHALL repair only `<parent-repo>`
- **AND** the nested repo SHALL not be traversed or repaired during that run.
32 changes: 32 additions & 0 deletions openspec/changes/doctor-current-single-repo-alias/tasks.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 5 additions & 1 deletion src/cli/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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).`,
);
}

Expand Down
53 changes: 53 additions & 0 deletions test/doctor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down