tooling/ai_harness/doctor/SPECIFICATION/04_scenarios.md
Each scenario describes a state of the repository and the expected doctor behavior. Scenarios are organized by check and by happy-path vs error cases.
Scenario headings use category: description format. Cross-references
within this file use the heading text (e.g., "same Given as
parity: missing CLAUDE.md at root").
Given: A repo with:
AGENTS.md and CLAUDE.md at root with identical contentAGENTS.md references .ai/git.md and .ai/testing.md.ai/git.md and .ai/testing.md exist.gitignore contains AGENTS.local.md, CLAUDE.local.md, and .ai/*When: scripts/ai_harness/doctor
Then: All checks show OK, exit code 0
Given: Same as happy: clean repo passes all checks, plus:
sub/AGENTS.md and sub/CLAUDE.md with identical contentWhen: scripts/ai_harness/doctor
Then: All checks show OK, exit code 0
When: scripts/ai_harness/doctor --help
Then: Help text printed to stdout (includes "Usage:", "--fix", "--help"),
exit code 0, no checks run. ParseArgv sets print_help: true on the
ok path. HandleAction sets stdout_text from HelpText and skips the
PerformDoctorChecks sub-chain.
Given: Same as happy: clean repo passes all checks, plus:
.claude/rules/my-rule.md exists on disk but is gitignored.opencode/config.json exists on disk but is gitignoredWhen: scripts/ai_harness/doctor
Then: All checks show OK, exit code 0 (gitignored files are not flagged)
Given: Same as happy: clean repo passes all checks (everything valid)
When: scripts/ai_harness/doctor --fix
Then: All checks show OK, exit code 0, no files changed
Given: AGENTS.md exists at root, CLAUDE.md does not
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with detail
CLAUDE.md not found (AGENTS.md exists) — no directory prefix for root-level
issues. Exit code 1.
Given: CLAUDE.md exists at root, AGENTS.md does not
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with detail
AGENTS.md not found (CLAUDE.md exists) — no directory prefix for root-level
issues. Exit code 1.
Given: Both exist at root but have different content
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with "differs from AGENTS.md" — no
directory prefix for root-level issues. Exit code 1.
Given: Root pair is valid. sub/AGENTS.md and sub/CLAUDE.md exist
with different content
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with "sub/" in details, exit code 1
Given: Root pair is valid. sub/AGENTS.md exists but sub/CLAUDE.md
does not
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with "sub/" in details, exit code 1
Given: Root pair is valid. a/b/c/AGENTS.md exists but
a/b/c/CLAUDE.md does not
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with "a/b/c/" in details (full relative
path from repo root, not just the leaf directory name). Exit code 1.
Given: Both exist at root, content differs
When: scripts/ai_harness/doctor --fix
Then: CLAUDE.md content now matches AGENTS.md, parity check shows FIXED
Given: AGENTS.md exists at root, CLAUDE.md does not
When: scripts/ai_harness/doctor --fix
Then: CLAUDE.md created with AGENTS.md content, parity check shows FIXED
Given: CLAUDE.md exists at root, AGENTS.md does not
When: scripts/ai_harness/doctor --fix
Then: AGENTS.md created with CLAUDE.md content, parity check shows FIXED
Given: Root pair is valid. sub/AGENTS.md exists but sub/CLAUDE.md
does not
When: scripts/ai_harness/doctor --fix
Then: sub/CLAUDE.md created with sub/AGENTS.md content, parity
check shows FIXED
Given: AGENTS.md exists at root as a regular file. CLAUDE.md is a
symlink pointing to AGENTS.md.
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with detail including "symlink" and
"CLAUDE.md". Exit code 1.
Given: CLAUDE.md exists at root as a regular file. AGENTS.md is a
symlink pointing to CLAUDE.md.
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with detail including "symlink" and
"AGENTS.md". Exit code 1.
Given: Root pair is valid. sub/AGENTS.md is a regular file.
sub/CLAUDE.md is a symlink.
When: scripts/ai_harness/doctor
Then: Parity check shows FAIL with "sub/" prefix and "symlink" in
details. Exit code 1.
Given: AGENTS.md exists at root as a regular file. CLAUDE.md is a
symlink pointing to AGENTS.md.
When: scripts/ai_harness/doctor --fix
Then: Symlink is replaced with a regular file containing the same
content. Parity check shows FIXED. CLAUDE.md is no longer a symlink.
Given: AGENTS.md contains .ai/missing.md, file does not exist
When: scripts/ai_harness/doctor
Then: Reference check shows FAIL with ".ai/missing.md" and "does not exist",
exit code 1
Given: AGENTS.md references .ai/git.md, file exists
When: scripts/ai_harness/doctor
Then: Reference check shows OK
Given: AGENTS.md contains no .ai/* references
When: scripts/ai_harness/doctor
Then: Reference check shows OK
Given: sub/AGENTS.md references .ai/git.md. sub/.ai/git.md exists
but repo_root/.ai/git.md does not.
When: scripts/ai_harness/doctor
Then: Reference check shows OK — .ai/git.md is resolved relative to
sub/, finding sub/.ai/git.md. This allows directories containing
AGENTS.md to be moved without breaking their .ai/ references.
Given: sub/AGENTS.md references .ai/git.md. Neither sub/.ai/git.md
nor repo_root/.ai/git.md exists.
When: scripts/ai_harness/doctor
Then: Reference check shows FAIL with sub/.ai/git.md — the reference is
resolved relative to sub/ and displayed with the full relative path from the
repo root.
Given: Same as references: missing reference target
When: scripts/ai_harness/doctor --fix
Then: Reference check still shows FAIL (not auto-fixable), exit code 1
Given: .gitignore has .ai/* but not AGENTS.local.md
When: scripts/ai_harness/doctor
Then: Gitignore check shows FAIL with "AGENTS.local.md", exit code 1
Given: .gitignore has AGENTS.local.md and .ai/* but not
CLAUDE.local.md
When: scripts/ai_harness/doctor
Then: Gitignore check shows FAIL with "CLAUDE.local.md", exit code 1
Given: .gitignore has AGENTS.local.md and CLAUDE.local.md but not .ai/*
When: scripts/ai_harness/doctor
Then: Gitignore check shows FAIL with ".ai/*", exit code 1
Given: No .gitignore file exists
When: scripts/ai_harness/doctor
Then: Gitignore check shows FAIL, exit code 1
Given: .gitignore contains /AGENTS.local.md (rooted) instead of
AGENTS.local.md (non-rooted)
When: scripts/ai_harness/doctor
Then: Gitignore check shows FAIL with "AGENTS.local.md", exit code 1.
Rooted patterns only match at the repository root, but these entries must
be non-rooted to match at all directory levels.
Given: .gitignore exists but is empty
When: scripts/ai_harness/doctor --fix
Then: Both entries appended, gitignore check shows FIXED
Given: .claude/rules/my-rule.md is tracked by git (staged or committed)
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL with the file path, exit code 1
Given: .claude/skills/my-skill.md is tracked by git
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL, exit code 1
Given: .claude/settings.json is tracked by git
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL, exit code 1
Given: .opencode/config.json is tracked by git
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL, exit code 1
Given: .gitlab/duo/chat-rules.md is tracked by git
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL, exit code 1
Given: .gitlab/duo/mcp.json is tracked by git
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL, exit code 1
Given: AGENTS.local.md is tracked by git (force-added with
git add --force) at the repo root
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL with the file path, exit code 1.
This prevents personal local overrides from being accidentally shared.
Given: CLAUDE.local.md is tracked by git at the repo root
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL, exit code 1.
Given: sub/AGENTS.local.md is tracked by git
When: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL with the file path, exit code 1.
The **/AGENTS.local.md pattern catches local files at any depth.
Given: .claude/rules/my-rule.md exists on disk but is NOT tracked
by git (gitignored or in .git/info/exclude)
When: scripts/ai_harness/doctor
Then: Forbidden files check shows OK
Given: The following files are each tracked by git:
.claude/agents/my-agent.md.claude/commands/my-cmd.md.claude/settings.local.json.claude/settings.local.jsoncWhen: scripts/ai_harness/doctor
Then: Forbidden files check shows FAIL listing all four file paths,
exit code 1. (This supplements the per-pattern scenarios above, which cover
the remaining forbidden patterns individually.)
Given: Same as forbidden: .claude/rules/ file committed
When: scripts/ai_harness/doctor --fix
Then: Forbidden files check still shows FAIL (hard fail, not auto-fixable),
exit code 1
Given: CLAUDE.md missing AND .gitignore missing AGENTS.local.md entry
When: scripts/ai_harness/doctor
Then: Both parity check and gitignore check show FAIL, exit code 1.
All four checks run — the chain does not short-circuit on individual failures.
Given: CLAUDE.md content differs (fixable) AND .claude/rules/foo.md
committed (not fixable)
When: scripts/ai_harness/doctor --fix
Then: Parity check shows FIXED, forbidden files check shows FAIL,
exit code 1. All checks run regardless of individual outcomes.
Given: The ROP chain produces a Result variant that does not match
any arm in Main.main's pattern match (e.g., a new Result.err message
type is added to ParseArgv but not handled in Main)
When: Main.main pattern-matches the final Result
Then: Gitlab::Fp::UnmatchedResultError is raised
When: scripts/ai_harness/doctor --unknown
Then: Error message printed to stderr (includes "Unknown option: --unknown"
and the help text), exit code 1, no checks run. ParseArgv returns
Result.err(Messages::InvalidArguments) with stderr_text containing
both the error and the help text.
When: scripts/ai_harness/doctor --fix --help
Then: Help text printed to stdout, exit code 0, no checks run. --help
takes precedence over --fix. ParseArgv sets print_help: true.
When: scripts/ai_harness/doctor
Then: ParseArgv adds fix: false, print_help: false to context.
HandleAction delegates to PerformDoctorChecks sub-chain.