--- name: sdlc-doctor description: Diagnose SDLC pipeline health — dependency preflight, agent-role registration, model-routing integrity, and token baseline. Read-only apart from seeding a token-baseline scaffold when absent. --- # $sdlc-doctor Snapshot of the pipeline's runtime environment. Reuses the same Step 0a preflight code-path that `$sdlc-start` runs on every invocation, but in a diagnostic mode that never aborts. ## What this command does 1. **Locate the runtime dependencies file.** Read `{SDLC_HOME}/runtime-dependencies.json`. It declares optional capabilities from other Codex marketplaces; the stack provider dependency graph is already bundled and is resolved separately in step 3. If it does not exist, print `🔌 Dependency preflight: no runtime-dependencies.json found.` and skip step 2. 2. **Run the same resolver as the orchestrator:** ```bash python3 {SDLC_HOME}/scripts/check-runtime-dependencies.py \ --declarations {SDLC_HOME}/runtime-dependencies.json ``` It uses `codex plugin list --json` for installed/enabled state and verifies the declared skill files in the installed snapshot or shared skill roots. Do not enforce policy in `$sdlc-doctor`; collect and report every status. 3. **Resolve active stack profiles.** Run `python3 {SDLC_HOME}/scripts/detect-stack.py --repo .` and report its primary profile, aspect winners, ties, provider dependency closure, selected agents/skills and parse errors. This is the same executable contract as the orchestrator; do not reproduce detection in prose. 4. **Read token baseline.** Try `/docs/token-baseline.md`. If it has a fenced JSON block tagged `summary` (e.g. ```` ```json summary ````) parse and extract `avg_output_tokens_per_medium_run`, `p90_output_tokens_per_medium_run`, `cache_hit_ratio`, `runs_aggregated`. If the file is absent, seed it by copying the template shipped with this plugin — `{SDLC_HOME}/templates/token-baseline.md` — to `/docs/token-baseline.md`, then report the "not yet baselined" state it contains. This is the one write `$sdlc-doctor` performs; it creates a scaffold and never overwrites an existing file. If the template cannot be located, fall back to reporting "no baseline file and no template found" and continue. 5. **Check model-routing integrity.** Named Codex role files are authoritative for their model and reasoning effort. Native-only installs use the orchestrator's bundled-role mode: generated role instructions plus an explicit model/effort on a `default` subagent. The hook only corrects an explicit mismatch exposed in a lifecycle payload. Report: - `agents.enabled` in `$CODEX_HOME/config.toml`. If `false`, **no subagent spawns at all** — the pipeline runs as one context, every phase on the session model, and every telemetry number becomes meaningless. This is the single most important line of this report. - `agents.default_subagent_model` / `agents.default_subagent_reasoning_effort`. These win over the parent session but lose to the role file. Report them so a surprising tier has an explanation. - `agents.max_concurrent_threads_per_session`. Below 2 the aspect fan-out serialises; correct but slow, and worth stating. - **Hook registration.** First use `codex plugin list --json` to determine whether `codex-sdlc` is installed and enabled natively. Native hooks come from the installed plugin snapshot and may not be copied into `$CODEX_HOME/hooks.json`. For a compatibility install, read `$CODEX_HOME/hooks.json` and count groups carrying the `_codex_sdlc` marker. Expect SessionStart, three PreToolUse groups, PostToolUse, SubagentStart and SubagentStop (seven groups total). | Finding | Meaning | |---|---| | native plugin enabled and hooks trusted | bundled stack cache, guards, formatting and telemetry active | | 7 marked compatibility groups | globally registered stack cache, guards, formatting and telemetry active | | 0 marked groups, file exists with other content | **another tool owns this file and the install did not merge.** Model pinning and phase telemetry are both off; the tiers in telemetry are declarations, not observations. Fix: `bash {SDLC_HOME}/scripts/install.sh` — it merges and preserves third-party entries | | file absent | never installed | | broad `PreToolUse` matcher | works, but should be narrowed to the tool family each guard consumes | Also list any **third-party** hook groups (those without the marker), with event and command. They are not a problem in themselves, but a hook belonging to another tool that returns non-JSON on a stop-family event makes Codex report `hook returned invalid stop hook JSON output` at the end of every turn — an error that looks like it comes from this pipeline and does not. - Any `[agents.]` entry in `config.toml` whose `config_file` points at a path that no longer exists — a stale registration silently falls back to the default role. 6. **Check MCP runtime resolution.** Only runs when the active stack profile declares an MCP server whose command is one of this repo's launchers (currently `laravel-boost`). A launcher exists because Codex MCP config is static TOML while the answer — Sail vs plain compose vs Herd vs system PHP, and where the app root lives inside a container — is a property of the project. That makes it the one part of the setup that can silently resolve to something surprising, so report it explicitly: ```bash bash {SDLC_HOME}/scripts/laravel-boost-launcher.sh --explain ``` Report the resolved command and the reason. Flag these cases: - **Non-zero exit** — no runtime could be determined. The pipeline still works (agents fall back to running artisan through Bash), but the Artisan-aware tools are gone. Show the `mcp.laravel_boost.command` override snippet from the launcher's own error output. - **`laravel/boost` missing from `composer.json`** — the server will start and then fail. - **Resolved to `docker compose` while `vendor/bin/sail` exists** — should not happen; it means the compose file names a PHP service that Sail does not manage. - **Resolved via an override** — say so. An override that outlived the setup it was written for is a common cause of "the MCP tools stopped working". 7. **Check for local-agent shadowing and a stale run marker.** - **Shadowing.** `Glob /.codex/agents/*.toml` and `$CODEX_HOME/agents/*.toml`. For each file whose basename (minus `.toml`) matches a role named by the active stack profile's `agents_per_phase` (e.g. a local `developer.toml`, or `qa_engineer.toml`), report it as a collision. Codex selects an agent type by name from a single registry, so a project-local role of the same name is genuinely ambiguous. During a pipeline run the Step 2 run marker plus `enforce-agent-model.sh` deny the local one; outside a run, nothing does. See `pipeline-orchestrator/SKILL.md` Step 3c and Step 2. - **Stale run marker.** Check `/.codex/.sdlc-run-active.json`. If present, read `started_at` and compare to now. If older than 6 hours (the same threshold `enforce-agent-model.sh` uses to treat a marker as inactive), report it as stale — it is inert for enforcement purposes but indicates a crashed or force-quit `$sdlc-start` run; suggest `rm .codex/.sdlc-run-active.json`. If younger than 6 hours, report it as an apparently active run (informational — doctor does not treat this as an error). This step reads the filesystem only — it changes nothing. 8. **Check the git branching model.** Run `{SDLC_HOME}/scripts/detect-git-flow.sh` and report what `$sdlc-start` would decide. Always run it **fresh** — never read the cache for this report. A doctor that echoes a stale cache cannot diagnose a stale cache. Report: - **Effective source** — whether an explicit `git:` block in `/.codex/sdlc.local.yaml` overrides detection, and which keys it sets. An override means the detected values below are informational only. - **Detected model** — model, confidence, and the `sources[]` provenance. - **Branches** — default branch, develop branch (or "—"), any `release/*` branches. - **Naming convention** — separator, word separator, ticket pattern, and the observed prefix histogram. - **Documented conventions** — which of the files in `references/GIT-FLOW.md` Step B exist and whether any states a branch convention. Read them; do not assume. - **Cache state** — `/.codex/.sdlc-git-flow.json`: absent, fresh (younger than 30 days and `user_confirmed`), unconfirmed, or stale. When it is present and its `model` disagrees with the fresh detection, flag it — that is a repo whose branching model changed under a cached answer. This step reads the filesystem and runs read-only git plumbing. It creates no branch and writes no cache. 9. **Render output.** Default = human-readable table. With `--json` flag, emit a single valid JSON object to stdout and exit. ## Human output format ``` 🩺 SDLC Doctor Dependencies (from runtime-dependencies.json): superpowers >=1.0.0 [policy=warn] status: ✅ available skills: using-superpowers, verification-before-completion acme-internal >=2.0.0 [policy=block] status: ❌ missing missing skills: code-style, internal-api-style install: codex plugin marketplace add acme/internal-tools codex plugin add acme-internal@acme-internal-tools Stack profiles: 🎯 active: laravel (priority=100, from laravel-plugin/stack.md) also installed: vanilla (priority=0) Token baseline (docs/token-baseline.md, last updated 2026-05-04, 22 runs): avg medium-run output: 44,800 tokens p90 medium-run output: 68,200 tokens cache hit ratio: 0.61 note: subagent phases only — orchestrator overhead is not metered Model routing: ⚠️ agents.enabled = false — NO subagents spawn; every phase runs on the session model. Every phase will run in the parent context instead of its registered role. Per-dispatch telemetry does not apply while this is set. declared tiers for active profile (laravel): business_analyst gpt-5.6-sol/high laravel_architect_plan gpt-5.6-sol/high laravel_architect gpt-5.6-terra/medium artisan_specialist gpt-5.6-terra/low qa_engineer gpt-5.6-terra/medium security_analyst gpt-5.6-sol/xhigh document_writer gpt-5.6-luna/low Local-agent shadowing: ⚠️ .codex/agents/developer.toml shadows profile role 'developer' ⚠️ .codex/agents/qa_engineer.toml shadows profile role 'qa_engineer' (denied by the run marker + PreToolUse hook during a pipeline run; outside a run nothing prevents the local role from winning the name) Run marker: ✅ no .codex/.sdlc-run-active.json present Git flow: source: detection (no `git:` block in .codex/sdlc.local.yaml) 🎯 model: github-flow (confidence=high) — topology:no-develop-branch, topology:prefix-histogram branches: default=main, develop=—, release=— convention: {prefix}/{kebab-slug} separator=/ word_separator=- ticket=— observed prefixes: feature=7, fix=4 (singletons discarded) documented conventions: CONTRIBUTING.md (no branch statement), AGENTS.md (absent) cache: .codex/.sdlc-git-flow.json absent — next $sdlc-start will detect and ask would branch: feature/ from main → PR base main Heads-up: ❌ 1 blocking dependency missing — $sdlc-start would abort. Run the install commands above, then retry. ``` When `agents.enabled` is true and the PreToolUse hook is registered, print `✅ subagents enabled, model enforcement hook active` in place of the warning. In the Git flow section, flag these conditions instead of the plain `🎯` line when they apply: - `⚠️ config overrides detection: model={model} (keys: {list})` — the `git:` block wins, so the detected values are informational. - `⚠️ cache disagrees with fresh detection: cached={cached_model}, detected={detected_model} — run $sdlc-start --redetect-git-flow` - `⚠️ rules/topology conflict: {one line}` — a documented convention that the branch topology does not corroborate. - `⚠️ model=unknown — no commits or branches; $sdlc-start will not create a branch` When no local-agent name collides with the active profile, print `✅ no local-agent shadowing detected` in place of the warning list. When a run marker exists and is fresh (< 6h), print `🏃 run marker active (task_slug={task_slug}, started {N}m ago) — a pipeline appears to be running`. When it exists and is stale (≥ 6h), print `⚠️ stale run marker (started {N}h ago, task_slug={task_slug}) — likely a crashed run. Remove with: rm .codex/.sdlc-run-active.json`. If a section is absent (no baseline file, no missing deps, etc.) say so explicitly with one line — never silently omit a section. ## JSON output format (`--json`) ```json { "deps_preflight": { "superpowers": { "status": "available", "policy": "warn", "missing_skills": [] }, "acme-internal": { "status": "missing", "policy": "block", "missing_skills": ["code-style", "internal-api-style"], "install_command": [ "codex plugin marketplace add acme/internal-tools", "codex plugin add acme-internal@acme-internal-tools" ] } }, "stack": { "primary_profile": "laravel", "active_profiles": {"backend": "laravel", "database": "laravel"}, "providers": ["sdlc", "php-foundation", "laravel-plugin"] }, "token_baseline": { "available": true, "runs_aggregated": 22, "avg_output_tokens_per_medium_run": 44800, "p90_output_tokens_per_medium_run": 68200, "cache_hit_ratio": 0.61, "token_scope": "subagent_phases_only", "last_updated": "2026-05-04" }, "model_routing": { "agents_enabled": true, "default_subagent_model": "gpt-5.6-terra", "enforcement_hook_registered": true, "enforcement_hook_matcher": "spawn_agent|Agent", "override_active": false, "declared_tiers": { "business_analyst": { "model": "gpt-5.6-sol", "effort": "high" }, "laravel_architect": { "model": "gpt-5.6-terra", "effort": "medium" }, "laravel_architect_plan": { "model": "gpt-5.6-sol", "effort": "high" }, "qa_engineer": { "model": "gpt-5.6-terra", "effort": "medium" }, "security_analyst": { "model": "gpt-5.6-sol", "effort": "xhigh" }, "document_writer": { "model": "gpt-5.6-luna", "effort": "low" } }, "stale_registrations": [] }, "local_agent_shadowing": [ { "path": ".codex/agents/developer.toml", "shadows": "developer" }, { "path": ".codex/agents/qa_engineer.toml", "shadows": "qa_engineer" } ], "run_marker": { "present": false, "stale": null, "task_slug": null, "started_at": null, "age_seconds": null }, "git_flow": { "source": "detection", "config_override_keys": [], "detected": { "model": "github-flow", "confidence": "high", "sources": ["topology:no-develop-branch", "topology:prefix-histogram"], "default_branch": "main", "develop_branch": null, "release_branches": [], "prefix_style": "conventional", "prefix_histogram": { "feature": 7, "fix": 4 }, "naming": { "separator": "/", "word_separator": "-", "ticket_pattern": null, "ticket_position": null, "observed_max_length": 42 } }, "documented_conventions": [ { "path": "CONTRIBUTING.md", "states_convention": false } ], "topology_conflict": null, "cache": { "present": false, "user_confirmed": null, "stale": null, "disagrees_with_detection": null, "detected_at": null } }, "would_abort_pipeline": true } ``` `git_flow.source` is `"config"` when a `git:` block exists in `.codex/sdlc.local.yaml` (with the overridden keys listed in `config_override_keys`), otherwise `"detection"`. `git_flow.detected` is the verbatim `detect-git-flow.sh` output, always freshly computed. Every `cache.*` field is `null` when `cache.present` is `false`. `local_agent_shadowing` is `[]` when no collision exists. `run_marker.stale` is `null` when `present` is `false`; otherwise `true` when `age_seconds >= 21600` (6h, matching `enforce-agent-model.sh`'s `MARKER_MAX_AGE_SECONDS`), else `false`. `would_abort_pipeline` is `true` iff any dependency with `policy=block` is missing. `model_routing.override_active` is `true` only when subagents are disabled or a selected named role resolves to a different registration. A native bundled-role dispatch remains valid without compatibility hooks. ## Hard rules - **Effectively read-only.** Do NOT install plugins, run pipelines, or modify any existing file. The single exception is seeding `docs/token-baseline.md` from the shipped template when that file does not exist (step 4) — a create-if-absent scaffold, never an overwrite. Step 6 (shadowing check, run-marker staleness) never deletes the marker itself — it only reports and suggests the `rm` command; the operator runs it. Step 7 never creates a branch, never fetches, and never writes the git-flow cache. - **Do not enforce policy.** A missing `block` dep here is just reported, not actioned. - **Reuse, don't reimplement.** The dependency-status algorithm is described in `pipeline-orchestrator/SKILL.md` Step 0a-2 / 0a-3. If those steps change, this command's behavior must follow — this command is documentation that delegates to those steps, not a parallel implementation. - **Exit code semantics with `--json`:** exit 0 normally; exit 1 only if the runtime-dependencies file itself is malformed JSON. Missing blocking dependencies still exit 0 because doctor diagnoses rather than enforces. ## When to use - After installing or updating the marketplace plugin — verify provider and external capability wiring. - Before kicking off a long pipeline run — confirm `$sdlc-start` won't abort at Step 0a. - In CI / automation — `$sdlc-doctor --json` gives a machine-checkable health report. - When a token regression is suspected — compare current `token_baseline` against historical values. - After a project changes its branching model (adds or drops `develop`) — confirm the cached detection has not gone stale, and see which base branch PRs would target.