| name | gaia-brownfield |
| description | Apply GAIA to an existing project — deep discovery, multi-scan gap analysis, NFR assessment, and template-driven artifact generation. Use when "onboard existing project" or /gaia-brownfield. Runs multi-scan logic (doc-code, hardcoded, integration-seam, runtime-behavior, security) plus NFR assessment via test-architect subagent. |
| argument-hint | [project-path] |
| context | main |
| allowed-tools | ["Read","Write","Edit","Grep","Glob","Bash","Agent"] |
| model | inherit |
| orchestration_class | heavy-procedural |
Orchestration Mode
SESSION_MODE=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/detect-orchestration-mode.sh")
WARNING_OUTPUT=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration-warning.sh" --skill-class heavy-procedural --mode "$SESSION_MODE")
if printf '%s' "$WARNING_OUTPUT" | grep -q '^SURFACE-WARNING: '; then
SENTINEL_PATH=$(printf '%s' "$WARNING_OUTPUT" | sed -n 's/^SURFACE-WARNING: //p' | head -n1)
cat "$SENTINEL_PATH"
fi
Surface contract. When the prelude cats a sentinel file — which happens once per session under Mode A (subagent dispatch) — you MUST mirror that cat'd warning text VERBATIM as the FIRST user-visible text of your response, before any skill-phase output. Claude Code auto-collapses Bash tool-call output, so the warning is invisible to users unless re-emitted as LLM turn text. Skip this step only when the prelude produced no sentinel output (Mode B, repeat invocation in same session, or out-of-scope skill class).
Setup
!${CLAUDE_PLUGIN_ROOT}/skills/gaia-brownfield/scripts/setup.sh
Memory
!${CLAUDE_PLUGIN_ROOT}/scripts/memory-loader.sh architect all
Mission
You are applying the GAIA framework to an existing codebase. This skill runs deep project discovery, parallel documentation subagents, multi-scan gap analysis, NFR assessment, gap consolidation, PRD/architecture generation, and optional ground-truth bootstrap, then writes the canonical brownfield onboarding artifact set.
This skill is the native Claude Code conversion of the legacy brownfield-onboarding workflow. The step ordering, prompts, subagent delegation, template-driven output generation, and post-complete quality gates are preserved from the legacy instructions.xml — parity confirmed.
Main context semantics: This skill runs under context: main with full tool access. It orchestrates a large discovery pipeline that reads the target project and produces a canonical artifact set under .gaia/artifacts/planning-artifacts/ and .gaia/artifacts/test-artifacts/.
Path resolution. All Phase 2 brownfield artifact paths in this SKILL.md use the canonical locations under .gaia/artifacts/planning-artifacts/ and .gaia/artifacts/test-artifacts/. The ~10 produced artifacts (brownfield-assessment.md, project-documentation.md, api-documentation.md, ux-design.md, event-catalog.md, dependency-map.md, dependency-audit-{date}.md, brownfield-subagent-summary.md, brownfield-scan-*.md, brownfield-onboarding.md) all target canonical destinations.
Per-artifact destination split:
planning-artifacts/: brownfield-assessment.md, project-documentation.md, api-documentation.md, ux-design.md, event-catalog.md, dependency-map.md, brownfield-subagent-summary.md, brownfield-scan-*.md, brownfield-onboarding.md (the "what is this project" lens).
test-artifacts/: dependency-audit-{date}.md (the "what's risky about its dependencies" lens — the only Phase-2 artifact that lives under test-artifacts/ because dependency audit IS a test-lens activity). Spelled out here so subagents don't have to scroll up to the dispersion note + correctly route the dependency-audit write.
Scripts-over-LLM: Deterministic operations (config resolution, checkpoint writes, gate validation, lifecycle events) are delegated to the shared foundation scripts under plugins/gaia/scripts/ via inline !scripts/*.sh calls. The canonical foundation set includes: resolve-config.sh, checkpoint.sh (with write / read / validate subcommands — the consolidated checkpoint surface), validate-gate.sh (deployed equivalent for spec's file-gate.sh), template-header.sh, memory-loader.sh, lifecycle-event.sh. See the Reconciliation Note under Critical Rules for the one remaining spec-vs-deployed name mapping.
Critical Rules
- Document existing state before proposing changes. Stories written downstream must cover gaps only, not re-implement existing features.
- Gap-only PRD: When generating the brownfield PRD, fill every section with gap-focused content. Do NOT re-document working features as new requirements.
- Mermaid diagrams only: Every diagram in generated artifacts must use Mermaid syntax — no ASCII art, no prose descriptions of diagrams.
- Swagger/OpenAPI for APIs: All API documentation must use Swagger/OpenAPI format. If an OpenAPI spec exists, validate it against actual routes; if not, generate one from code.
- Limit flow diagrams to 3–5 key flows to avoid output bloat.
- Subagent completion MUST NOT auto-advance. After parallel subagents return, pause for user review before proceeding to the next phase. Halt-on-failure is scoped per subagent — individual scanner failures do not block the overall workflow (see the Failure Semantics section), but the post-complete gates halt when their required files are absent.
- Sprint-status.yaml is NEVER written by this skill (Sprint-Status Write Safety rule). This skill writes only planning and test artifacts.
- Parallel invocation isolation (AC-EC7): Each invocation uses an isolated checkpoint path and independent
_resolved config derived from resolve-config.sh. Two concurrent runs on different project roots never share mutable state or contaminate each other's artifacts.
- Token budget (AC-EC1): Keep the SKILL.md body under the activation budget. Scanners stream/chunk results and emit a "scan truncated — review manually" advisory rather than exceed the budget (AC-EC6).
- Fail-fast on missing foundation scripts (AC-EC2):
setup.sh aborts with an actionable error identifying the missing / non-executable script path. No partial scan output is written if the setup step fails.
Reconciliation Note — Architecture Spec vs Deployed Scripts
The architecture specifies the foundation-script surface. The live plugins/gaia/scripts/ set exposes checkpoint.sh (with write / read / validate subcommands — the same canonical name) alongside validate-gate.sh, which is the deployed equivalent for the spec's file-gate.sh. This skill calls the deployed names for parity with the live script set. If the file-gate.sh spec name is added later under a separate story, the inline calls in setup.sh / finalize.sh can be updated without touching the skill body. The checkpoint surface no longer requires reconciliation — checkpoint.sh is the canonical name in both the spec and the product.
Inputs
This skill accepts the following inputs (from $ARGUMENTS when invoked via slash command, or from interactive prompt otherwise):
- Project path — absolute or relative path to the target codebase. Defaults to the current working directory.
- Execution mode —
normal (pause for user review at checkpoints) or yolo (auto-advance). YOLO mode always uses the safe default of merge when resolving test-environment.yaml conflicts.
Pipeline Overview
The skill runs nine phases in strict order:
- Deep Project Discovery — capability detection and project classification
- Parallel Documentation Subagents — API, UX, events, dependencies
- Deep Analysis Multi-Scan Subagents — five scan branches + doc-code + config-contradiction + dead-code
- Test Execution During Discovery — non-blocking test runner probe
- Auto-Generate test-environment.yaml — from detected test infrastructure (conditional)
- NFR Assessment & Performance Test Plan — test-architect subagent (Sable)
- Gap Consolidation & Deduplication — merge, rank, budget-check
- PRD + Adversarial Review + Code-Verified Review — gap-focused PRD generation
- Architecture + Ground-Truth Bootstrap — optional Val seed + Tier 1 agent extraction
Each phase is independent in its write targets but must run sequentially because later phases consume earlier outputs.
Phase 1 — Deep Project Discovery
- Scan the project root for the primary tech stack, frameworks, runtime versions, and conventions.
- Set capability flags by scanning source files:
{has_apis} — route/controller definitions, OpenAPI/Swagger specs present
{has_events} — Kafka / RabbitMQ / SNS-SQS / Redis pub-sub / NATS patterns
{has_external_deps} — outbound HTTP clients, SDKs, service URLs, database connections
{has_frontend} — call the shared detectProjectType module; set true when result.type is frontend, fullstack, or mobile
- Classify infrastructure markers across six categories (Terraform, Docker, Helm, Kubernetes, Pulumi, CloudFormation) to set
{has_infra}.
- Detect framework imports (Express, Spring Boot, Django, FastAPI, Angular, React, Next.js, NestJS, Flask, Gin, Fiber) to set
{has_app_code}.
- Apply the classification decision tree to set
{project_type}:
has_infra + has_app_code → platform
has_infra + no has_app_code → infrastructure
- no
has_infra → application (default)
5a. Detection-driven config draft. After the boolean capability flags are set, run detect-signals.sh to produce a structured signal inventory and (optionally) merge it into the project's .gaia/config/project-config.yaml:
- Run
!${CLAUDE_PLUGIN_ROOT}/scripts/detect-signals.sh --project-root <project> --merge-into <project>/.gaia/config/project-config.yaml --output <project>/.gaia/config/project-config.draft.yaml --schema ${CLAUDE_PLUGIN_ROOT}/config/project-config.schema.yaml --format json. The draft output path MUST live under .gaia/config/ (canonical), not at repo-root config/. On any clean checkout config/ doesn't exist and the script would die with FileNotFoundError, exit 1, making this step unreachable.
- The script emits a JSON document with five keys —
stacks, platforms, ci_platform, tool_providers, warnings — plus a top-level verdict (PASS | WARNING | CRITICAL).
- Detected sections are merged into the existing
project-config.yaml using RFC 7396 JSON Merge Patch semantics: existing user-edited values are preserved unchanged; only null or absent fields are filled. The merged draft is written to project-config.draft.yaml for user review before promotion to project-config.yaml.
- When the
--schema flag is provided, the script invokes resolve-config.sh --shared <draft> --schema <schema> to validate the merged draft. A schema rejection collapses the verdict to CRITICAL and exits non-zero.
- Verdict surfacing — mandatory: the parent skill MUST surface the verdict to the user verbatim — no silent swallowing. PASS = all sections populated, no conflicts. WARNING = conflicts detected (e.g., multiple test runners) or partial signals (e.g., empty project). CRITICAL = post-merge schema validation failure; HALT until resolved.
- Empty-project advisory: when no signals are detected, the script emits a
warnings entry directing the user to configure manually via /gaia-config-stack, /gaia-config-platform, /gaia-config-ci, /gaia-config-tools. Surface this advisory to the user.
- Mobile signals out of scope: Package.swift, Android Gradle, Flutter mobile, react-native, Xcode/Android Studio detection do NOT fire here — those land downstream.
- Plugin-project classification.
detect-signals.sh ALSO invokes plugin-detection.sh and emits a top-level project_kind field. Three or more co-occurring signals from {SKILL.md, adapter.json, plugin manifest, commands/, settings.json hooks, .claude/} set project_kind: claude-code-plugin. Single-signal detection is rejected to avoid false positives on stray SKILL.md or manifest files in non-plugin repos. Surface project_kind to the user so the downstream /gaia-trace plugin chain can attach to it.
5b. Multi-stack stacks[].path proposal / audit. When the deterministic-tools master flag (brownfield.deterministic_tools) and per-tool override (brownfield.detect_signals_enabled, default true) are on, run detect-signals.sh in the OPT-IN stacks-path mode to give multi-stack monorepos advisory partitioning help. This is distinct from the root-only detection above (that path is unchanged):
ds_start=$(date +%s)
DECLARED="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh project_config_path 2>/dev/null \
| xargs -I{} yq eval '[.stacks[].path | select(. != null)] | join(",")' {} 2>/dev/null || true)"
ds_mode="proposal"; [ -n "$DECLARED" ] && ds_mode="audit"
bash "${CLAUDE_PLUGIN_ROOT}/scripts/detect-signals.sh" --project-root "${GAIA_PROJECT_PATH:-.}" \
--stacks-path-mode "$ds_mode" --declared-paths "$DECLARED" \
--draft-out "${GAIA_CONFIG_DIR:-.gaia/config}/project-config.draft.yaml" \
--audit-out "${GAIA_MEMORY_DIR:-.gaia/memory}/brownfield-audit/partitioning-audit.json" \
--format json || true
ds_seconds=$(( $(date +%s) - ds_start ))
- Proposal mode (no
stacks[].path declared): scans ecosystem manifests and writes a stacks[].path mapping to project-config.draft.yaml (advisory — the user accepts by renaming to project-config.yaml OR merging the entries via /gaia-config-stack; declared truth always wins). A single root-level stack emits "nothing to propose" and writes no draft. Nested manifests (a manifest inside another stack's path) scope to the parent — ignore_nested_manifests: true default; they do NOT spawn a phantom child stack.
- Audit mode (
stacks[].path IS declared): compares declared vs auto-detected and logs disagreement to .gaia/memory/brownfield-audit/partitioning-audit.json ({auto_detected_partitioning, declared_partitioning, disagreement_count}); it does NOT regenerate the draft (auto-detection vs explicit precedence). Never overrides the declared config.
- Telemetry (
brownfield-telemetry.sh; detect_signals owns its fields, single-author): populate phase_runtime_seconds.detect_signals / deterministic_tool_seconds.detect_signals / llm_token_count:0 / detect_signals_mode: proposal|audit|skipped (skipped when the flags are off). Advisory, never gating — runs in ≤2s on a 10-manifest fixture.
-
Generate the brownfield assessment artifact. The canonical template lives at ${CLAUDE_PLUGIN_ROOT}/templates/brownfield-assessment-template.md and ships with the plugin. Read it as the starting shape (mode/schema_version/generated_by frontmatter + the seven section headers: Project Overview, Repository Layout, Existing Documentation Surface, Stack Signals, Known Gaps, Scan-readiness checklist, Continuation pointer). Fill the placeholder fields with concrete project data: component inventory, technical debt, migration constraints, coexistence strategy, and adoption path. Include {project_type} in the output. Write to .gaia/artifacts/planning-artifacts/brownfield-assessment.md.
-
Write the enhanced project documentation — all standard sections plus detected capability flags, {project_type}, testing infrastructure summary, and CI/CD pipeline summary. Write to .gaia/artifacts/planning-artifacts/project-documentation.md.
-
Install the project-root CLAUDE.md so the onboarded project has GAIA context loaded into Claude Code sessions (runtime tree, how-to-start, hard rules, upstream bug-report policy). A brownfield project very often ALREADY has its own CLAUDE.md with the team's project-specific instructions — the helper handles that: it APPENDS a marker-delimited GAIA block to the existing file, preserving the user's content verbatim above it (never clobbers). On a project with no CLAUDE.md it seeds the template; on re-run it is a no-op (marker-sentinel idempotency):
!${CLAUDE_PLUGIN_ROOT}/scripts/install-claude-md.sh --target "${PROJECT_PATH}"
Exit codes: 0 success (seeded, appended, or no-op), 1 plugin source template missing (reinstall via marketplace), 2 usage error.
Checkpoint after Phase 1 via the canonical checkpoint.sh write subcommand. The --step VALUE TYPE MUST be a non-negative integer (the script's case-validator at plugins/gaia/scripts/checkpoint.sh:226-227 enforces this). Use the bare phase number; a named-phase string is rejected. Brownfield's Phase 1 checkpoint:
!${CLAUDE_PLUGIN_ROOT}/scripts/checkpoint.sh write \
--workflow brownfield \
--step 1 \
--var status=complete \
--var phase_name=discovery \
--file ".gaia/artifacts/planning-artifacts/brownfield-assessment.md"
Per phase, --step advances 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9. The phase NAME (discovery, documentation, scans, tests, env, nfr, consolidation, prd, architecture) is carried as a --var phase_name=... since the --step value is an integer. The non-gating contract of brownfield checkpoints means an unknown-flag error from a stale invocation is informational, not blocking — but the canonical form above is what the helper recognizes.
Phase 2 — Parallel Documentation Subagents
Spawn the following subagents in parallel (single message, multiple Agent tool calls). Only spawn subagents for detected capabilities:
- If
{has_apis} — API Documenter subagent. Scan for routes, controllers, and specs. Validate existing OpenAPI specs against routes or generate a new OpenAPI 3.x spec from code. Document all endpoints with method, path, handler, auth, parameters, request/response schemas, error formats. Include a Mermaid API flow diagram. List undocumented endpoints as gaps. Output to .gaia/artifacts/planning-artifacts/api-documentation.md.
- If
{has_frontend} — UX Assessor subagent. Scan UI frameworks, components, design patterns, styling. Document UI patterns, navigation structure (Mermaid sitemap), interaction patterns, accessibility (WCAG, ARIA, keyboard nav). Propose improvements for gaps only. Output to .gaia/artifacts/planning-artifacts/ux-design.md.
- If
{has_events} — Event Cataloger subagent. Scan messaging infrastructure, produced/consumed events with schemas, external events, delivery guarantees (retry, DLQ, idempotency). Include Mermaid event flow diagrams (2–3 key flows). Output to .gaia/artifacts/planning-artifacts/event-catalog.md.
- Always — Dependency Mapper subagent. Document external service dependencies, infrastructure dependencies, key library dependencies (ORM, auth lib — check version currency and CVE risk). Build a Mermaid dependency graph. Document contracts, SLAs, fallback strategies. Identify dependency risks. Output to
.gaia/artifacts/planning-artifacts/dependency-map.md. After writing, run the shared review-dependency-audit task to generate a dependency audit report at .gaia/artifacts/test-artifacts/dependency-audit-{date}.md.
Post-subagent validation: verify each expected output file exists. If any subagent failed to write its output file, the orchestrator (this skill) MUST write a stub file on the subagent's behalf using the paths declared in the legacy output.artifacts contract. Dependency-audit goes to .gaia/artifacts/test-artifacts/; all other Phase 2 artifacts go to .gaia/artifacts/planning-artifacts/. Do NOT use hardcoded paths.
After all subagents return, write a subagent summary at .gaia/artifacts/planning-artifacts/brownfield-subagent-summary.md (which subagents ran, artifacts produced, file paths, any errors). Pause for user review in normal mode before continuing.
Phase 3 — Deep Analysis Multi-Scan Subagents (Infra-Aware)
Phase 3 pre-flight — deterministic-tools pre-warm
Before the Phase 3 scan timer starts, run the deterministic-tools pre-flight. This primes the Grype vulnerability DB and cdxgen package-registry caches so a cold runner does not pay the 15–30s cold-fetch against the 120s WARNING budget.
DET_TOOLS="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.deterministic_tools 2>/dev/null)"
PREWARM_ON="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.prewarm_enabled 2>/dev/null)"
export GAIA_BROWNFIELD_DETERMINISTIC_TOOLS="${DET_TOOLS:-true}"
export GAIA_BROWNFIELD_PREWARM_ENABLED="${PREWARM_ON:-true}"
prewarm_start=$(date +%s)
bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/pre-warm.sh" || true
prewarm_seconds=$(( $(date +%s) - prewarm_start ))
REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md"
TELEM="${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/brownfield-telemetry.sh"
if [ -f "$REPORT" ]; then
bash "$TELEM" --report "$REPORT" --field phase_runtime_seconds.pre_warm --value "$prewarm_seconds" || true
bash "$TELEM" --report "$REPORT" --field deterministic_tool_seconds.pre_warm --value "$prewarm_seconds" || true
fi
start_ts=$(date +%s)
prewarm_seconds feeds the phase_runtime_seconds.pre_warm / deterministic_tool_seconds.pre_warm telemetry fields, populated via brownfield-telemetry.sh (the shared writer). When the flags are off, pre-warm.sh emits an INFO skip line and contributes 0.
Phase 3 per-stack file-list intersection
When the master flag is on, the deterministic-tools orchestrator computes, for each stacks[] entry, the file-list passed to that stack's per-tool adapters as (path_root ∩ paths[]) − excludes[] (path_root = stack.path || '.'; excludes ALWAYS win on collision). The intersection is applied BEFORE adapter dispatch so per-stack adapters see only files within their declared stack scope — the run.sh --input <file-list> contract is byte-stable (adapters receive a flat file-list, never path/paths/excludes metadata). Single-stack repos (stacks[].path: null) collapse to '.' ∩ paths − excludes, byte-identical to pre-deploy (zero-regression invariant — the dominant deployment shape).
orch_start=$(date +%s)
orch_log="$(GAIA_BROWNFIELD_DETERMINISTIC_TOOLS=true \
ORCH_CONFIG="${GAIA_CONFIG_DIR:-.gaia/config}/project-config.yaml" \
ORCH_ROOT="${GAIA_PROJECT_PATH:-.}" \
bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/orchestrator.sh" 2>&1 || true)"
printf '%s\n' "$orch_log"
orch_seconds=$(( $(date +%s) - orch_start ))
REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md"
TELEM="${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/brownfield-telemetry.sh"
if [ -f "$REPORT" ]; then
bash "$TELEM" --report "$REPORT" --field phase_runtime_seconds.orchestrator_intersection --value "$orch_seconds" || true
bash "$TELEM" --report "$REPORT" --field deterministic_tool_seconds.orchestrator_intersection --value "$orch_seconds" || true
bash "$TELEM" --report "$REPORT" --field llm_token_count --value 0 || true
for pair in $(printf '%s' "$orch_log" | sed -n 's/.*per_stack_file_counts: //p'); do
s="${pair%%=*}"; c="${pair##*=}"
[ -n "$s" ] && bash "$TELEM" --report "$REPORT" --field "per_stack_file_counts.$s" --value "$c" || true
done
fi
Adapter dispatch then consumes each <stack>.files list via the existing run.sh --input contract — no adapter change. (Detection of nested ecosystem manifests is handled separately; this orchestrator only respects the path_root boundary and does not double-count files under a nested manifest.)
Phase 3 SBOM completeness check
After the cdxgen SBOM is produced, run the completeness check: compare the declared dependency count (from lock files) against the SBOM component count, and WARN when abs(divergence_pct) exceeds 10% — or 15% when any of five per-ecosystem carve-outs auto-detects (Yarn Berry PnP, conda, Go vendor, Gradle no-lockfile, Gradle shadow/shade) — so real-dependency CVEs don't silently fail to surface from an incomplete SBOM. NEVER aborts. When the SBOM is absent (the cdxgen SBOM-persist producer is not yet wired — tracked separately), the check INFO-skips.
SBOM-format note. cdxgen emits CycloneDX 1.7 (current spec); grype 0.112.0 rejects 1.7 input ("sbom format not recognized") because Anchore's parser tracks the CycloneDX spec a few revisions behind. When feeding an SBOM to grype (e.g. grype sbom:<file> for a re-scan without re-walking the project tree), prefer syft as the SBOM producer (syft scan dir:. -o cyclonedx-json=<file>) — the Anchore tools are version-matched and the handoff is reliable. cdxgen output remains the canonical source for the completeness check above (which doesn't pass through grype) and for any downstream consumer that accepts current CycloneDX. The brownfield Phase 3 pipeline therefore runs BOTH SBOMs: syft for the grype-feeding path, cdxgen for the broader-language-coverage path.
SBOM_ON="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.sbom_completeness_enabled 2>/dev/null)"
export GAIA_BROWNFIELD_SBOM_COMPLETENESS_ENABLED="${SBOM_ON:-true}"
sbomck_start=$(date +%s)
SBOM_PROJECT_ROOT="${GAIA_PROJECT_PATH:-.}" \
SBOM_FILE="${GAIA_MEMORY_DIR:-.gaia/memory}/brownfield-audit/sbom.json" \
SBOM_REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md" \
bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/sbom-completeness-check.sh" || true
sbomck_seconds=$(( $(date +%s) - sbomck_start ))
REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md"
TELEM="${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/brownfield-telemetry.sh"
if [ -f "$REPORT" ]; then
bash "$TELEM" --report "$REPORT" --field phase_runtime_seconds.sbom_completeness --value "$sbomck_seconds" || true
bash "$TELEM" --report "$REPORT" --field deterministic_tool_seconds.sbom_completeness --value "$sbomck_seconds" || true
fi
Spawn seven scan subagents in parallel. These run alongside Phase 2 documentation to detect gaps that structural analysis misses. Each scanner receives {tech_stack}, {project-path}, and {project_type} as context. When {project_type} is infrastructure or platform, infra-specific detection patterns are applied alongside application patterns; for application, only application patterns run.
Standardized gap-entry schema. Every scan subagent MUST emit gap entries conforming to the canonical schema at ${CLAUDE_PLUGIN_ROOT}/schemas/brownfield-gap-entry.schema.json. The schema defines required fields (gap_id, category, severity, title, evidence) and authoritative enums (category, severity, claim_type, confidence). When dispatching each subagent, the parent skill MUST include this fragment in the subagent prompt so the scanner can self-validate before emission:
<gap-entry-schema-ref>
path: ${CLAUDE_PLUGIN_ROOT}/schemas/brownfield-gap-entry.schema.json
example: ${CLAUDE_PLUGIN_ROOT}/tests/fixtures/brownfield-gap-entry-example.json # canonical worked example — three entries demonstrating positive, negative, and contradiction claim_types; copy this shape rather than inventing your own
required: [gap_id, category, severity, title, evidence]
category-enum: [doc-code-drift, hardcoded-value, integration-seam, runtime-behavior, security, sbom-completeness, call-graph, stale-claim]
severity-enum: [CRITICAL, WARNING, INFO]
claim_type-enum: [positive, negative, contradiction] # positive default; negative for absence claims like "no __main__"; contradiction for two-sided doc↔code mismatch
evidence-required: [file]; evidence-optional: [line_range, snippet, tool]
id-prefix-convention: {SCANNER}-{NNN}, SCANNER short-name per the schema (DC=doc-code, HC=hardcoded, ISEAM=integration-seam, RB=runtime-behavior, SEC=security, SBOM=sbom-completeness, CG=call-graph) — see the example fixture above; Phase 7 dedup may rewrite and record originals in aliases
EMISSION RULES (apply before emitting every gap entry):
1. evidence.line_range anchoring: For a finding whose evidence is a specific
YAML/config key or code token, `evidence.line_range` MUST anchor to the
line of the matched key/token itself — not a neighbouring or
section-header line. For a multi-line construct, use the start-end range
of the matched construct.
2. claim_type for absence/negative findings: Absence or negative findings
(asserting something is missing, e.g. "no __main__", "no scheduler",
"endpoint X absent from docs") MUST set `claim_type: negative` — the
line_range then points at the representative witness line proving the
absence. Two-sided doc-vs-code contradictions MUST set
`claim_type: contradiction`. Positive findings (the default) may omit
the field.
</gap-entry-schema-ref>
Gap-entry emission rules. Every scan subagent MUST apply the following rules before emitting each gap entry:
-
evidence.line_range anchoring. When a finding's evidence is a specific YAML key, config key, or code token, evidence.line_range MUST anchor to the line of the matched key/token itself, not a neighbouring or section-header line. For a multi-line construct the range covers the start-end of the matched construct.
-
claim_type for absence/negative findings. Findings that assert something is missing (e.g. "no __main__", "no scheduler", "endpoint absent from docs") MUST set claim_type: negative. The evidence.line_range then points at the representative witness line that proves the absence. Two-sided doc-vs-code contradictions MUST set claim_type: contradiction. Positive findings (the default) may omit the field.
This schema fixture closes the cross-scanner drift identified across scanners: 5-6 scanners independently hit the same ambiguities (slash-field description/evidence, single evidence_line can't express two-sided contradictions, no category enum, mixed-case severity vocab, no claim_type marker for negative claims). The schema is the single source of truth; the consolidation subagent rejects entries that don't conform.
Doc-Code Scan
Read the doc-vs-code scan prompt template from the bundled knowledge. Scan the project for mismatches between documentation and code — stale claims, missing endpoints in docs, config values that differ from the documented defaults. Output gap entries to .gaia/artifacts/planning-artifacts/brownfield-scan-doc-code.md using the standardized gap-entry schema. Contradictory signals between docs and code produce gap rows tagged with evidence_file and evidence_line.
Hardcoded Values Scan
Scan for hard-coded logic, magic numbers, embedded literals that should be configuration. For infrastructure / platform projects, also detect hard-coded IPs, magic ports, embedded secrets / AMI IDs, and hard-coded resource limits in IaC files. Output to .gaia/artifacts/planning-artifacts/brownfield-scan-hardcoded.md.
Integration Seam Scan
Scan for integration seams between modules, services, and external systems — contracts, shared state, coupling patterns. For infrastructure / platform, also map service mesh topology, ingress / egress routes, and cross-namespace dependencies. Output to .gaia/artifacts/planning-artifacts/brownfield-scan-integration-seam.md.
Runtime Behavior Scan
Catalog runtime behavior: @Scheduled, Quartz, startup hooks, background threads, health checks. For infrastructure / platform, also catalog CronJobs, DaemonSets, init containers, sidecar patterns, health probes. Output to .gaia/artifacts/planning-artifacts/brownfield-scan-runtime-behavior.md.
Security Scan
Audit security posture: mutating endpoints, IDOR candidates, authorization gaps, missing CSRF. For infrastructure / platform, also detect exposed ports in k8s manifests, permissive ingress rules, overly broad RBAC bindings, missing NetworkPolicy. Output to .gaia/artifacts/planning-artifacts/brownfield-scan-security.md.
Config Contradiction Scan (infra-aware)
Detect contradictions between configuration files (e.g., different service limits in values.yaml vs deployment.yaml). For infrastructure / platform, apply patterns for terraform.tfvars, values.yaml, and kustomize overlays. Output to .gaia/artifacts/planning-artifacts/brownfield-scan-config-contradiction.md.
Dead Code & Dead State Scan
Identify unused modules, orphaned routes, dead migrations, unused feature flags. Output to .gaia/artifacts/planning-artifacts/brownfield-scan-dead-code.md.
Per-stack deterministic dead-code adapters
When brownfield.deterministic_tools: true, the LLM dead-code heuristic is
replaced by three sound per-stack adapters under
scripts/adapters/dead-code/. Each is independently gated by its per-tool
override (brownfield.deadcode_go_enabled / deadcode_python_enabled /
deadcode_jvm_enabled, default true) and degrades gracefully (WARN + exit 0)
when its toolchain is absent — Phase 3 never aborts.
AUDIT="${GAIA_MEMORY_DIR:-.gaia/memory}/brownfield-audit"
ADAPT="$GAIA_PLUGIN_ROOT/scripts/adapters/dead-code"
GAIA_BROWNFIELD_DETERMINISTIC_TOOLS=true DEADCODE_PROJECT_ROOT="$PROJECT_PATH" \
DEADCODE_OUT_DIR="$AUDIT" bash "$ADAPT/go-deadcode/adapter.sh"
GAIA_BROWNFIELD_DETERMINISTIC_TOOLS=true PY_PROJECT_ROOT="$PROJECT_PATH" \
PY_OUT_DIR="$AUDIT" bash "$ADAPT/python-vulture/adapter.sh"
GAIA_BROWNFIELD_DETERMINISTIC_TOOLS=true JVM_PROJECT_ROOT="$PROJECT_PATH" \
JVM_OUT_DIR="$AUDIT" bash "$ADAPT/jvm-spotbugs/adapter.sh"
Per-stack precision is the contract, not a bug. Go reports a
whole-program reachability binary verdict (<pkg>.<Function>), Python a
confidence percentage (<line>:<symbol>@<conf>), JVM a priority×rank ordinal
(<FQCN>.<method>(<sig>)). The framework MUST NOT synthesize a unified
cross-stack confidence score — file_path is the universal JOIN key, and each
stack-native qualifier is preserved verbatim. The unified "Test Quality"
report section is rendered in Phase 7 (see the Phase 7 render sub-step) as THREE
labeled per-stack sub-sections — never one flat list. Telemetry
(phase_runtime_seconds.deadcode_{go,python,jvm} etc.) is written by each
adapter through the single-author brownfield-telemetry.sh.
CVE + SBOM adapters
When brownfield.deterministic_tools: true, Phase 3 ALSO runs the
deterministic CVE-scan and SBOM-producer adapters that the dead-code block
above doesn't cover. The grype adapter at
scripts/adapters/grype/adapter.sh and the syft SBOM-producer path must
both be actually invoked — if referenced in the Phase-3 telemetry but never
called, on a stock run the CVE scan and SBOM-persist are never produced,
defeating the "deterministic tools default-on" contract.
AUDIT="${GAIA_MEMORY_DIR:-.gaia/memory}/brownfield-audit"
mkdir -p "$AUDIT/sarif"
. "$GAIA_PLUGIN_ROOT/scripts/lib/docker-runner.sh"
_BROWNFIELD_RUNNER="$(docker_runner_mode 2>/dev/null || echo native)"
_BROWNFIELD_DOCKER_READY=0
if [ "$_BROWNFIELD_RUNNER" = "docker" ] && docker_runner_available >/dev/null 2>&1; then
_BROWNFIELD_DOCKER_READY=1
fi
SBOM_FILE="$AUDIT/sbom-syft.json"
if [ "$_BROWNFIELD_DOCKER_READY" = "1" ]; then
ADAPTER_OUT_DIR="$AUDIT" docker_runner_dispatch syft scan dir:/workspace -o "cyclonedx-json=/out/sbom-syft.json" 2>/dev/null \
|| printf 'INFO: syft (docker) returned non-zero — SBOM unavailable (graceful degrade)\n' >&2
elif command -v syft >/dev/null 2>&1; then
syft scan dir:"$PROJECT_PATH" -o cyclonedx-json="$SBOM_FILE" 2>/dev/null \
|| printf 'INFO: syft returned non-zero — SBOM unavailable (graceful degrade)\n' >&2
else
printf 'INFO: syft not on PATH and runner != docker — SBOM step skipped (run gaia-doctor --install or set brownfield.tools.runner: docker)\n' >&2
fi
if [ "$_BROWNFIELD_DOCKER_READY" = "1" ] || command -v grype >/dev/null 2>&1; then
if [ -s "$SBOM_FILE" ]; then
GRYPE_INPUT="sbom:$SBOM_FILE"
else
GRYPE_INPUT="dir:$PROJECT_PATH"
fi
GAIA_BROWNFIELD_DETERMINISTIC_TOOLS=true \
ADAPTER_OUT_DIR="$AUDIT/sarif" \
GRYPE_INPUT="$GRYPE_INPUT" \
bash "$GAIA_PLUGIN_ROOT/scripts/adapters/grype/adapter.sh" \
|| printf 'INFO: grype adapter exited non-zero — CVE scan absent (graceful degrade)\n' >&2
else
printf 'INFO: grype not on PATH and runner != docker — CVE scan skipped (run gaia-doctor --install or set brownfield.tools.runner: docker)\n' >&2
fi
Both invocations follow the same graceful-degrade contract as the dead-code
adapters: when a toolchain is absent the step emits ONE INFO line pointing
at the remediation command (gaia-doctor --install) and continues. The
Phase 3 verdict is unchanged by their absence — they enrich the gap report
when present, they don't block when missing. The runner cascade
(brownfield.tools.runner = docker) still applies: when
the docker runner is selected, the same invocations transparently dispatch
through scripts/lib/docker-runner.sh via the per-adapter helper and the
host doesn't need grype/syft on PATH. The adapters are
actually called by the orchestrator that the
telemetry credits.
Partial-failure semantics (AC-EC8): If a scanner crashes mid-run, the other scanners continue. The failed scan writes a gap row tagged scan failed: {reason}. The overall skill exits non-zero with a partial-result summary listing which scanners succeeded, which failed, and what recoverable evidence is available. The remaining scanners continue — one failure does not block the cohort.
Language-aware INFO degrade log. When a stack signal is present but the matching deterministic dead-code adapter cannot run because its toolchain is absent, emit ONE INFO line per missing toolchain at Phase 3 scan time — do NOT wait for the Phase 7 banner to be the only fidelity disclosure. The emission rule, applied per detected stack:
- Python signal present (e.g.
pyproject.toml, setup.py, requirements.txt, or .py files under a configured stack path) AND vulture not on PATH:
[INFO] python project detected — install vulture for tool-grade dead-code (gaia-doctor --install)
- Go signal present (
go.mod or .go files) AND the deadcode tool (golang.org/x/tools/cmd/deadcode) not on PATH:
[INFO] go project detected — install golang.org/x/tools/cmd/deadcode for tool-grade dead-code (gaia-doctor --install)
- JVM signal present (
pom.xml, build.gradle, build.gradle.kts, or .java/.kt files) AND spotbugs not on PATH:
[INFO] jvm project detected — install spotbugs for tool-grade dead-code (gaia-doctor --install)
Stack detection re-uses the signal set already computed by detect-signals.sh in Phase 1 — do NOT re-scan. The INFO lines are non-blocking and DO NOT change the Phase 3 verdict; they exist to make the toolchain-absence visible at the scan step rather than only at the Phase 7 banner.
Per-Subagent Scan Diagnostic Table
After all seven Phase 3 scan subagents return (doc-code, hardcoded, integration-seam, runtime-behavior, security, config-contradiction, dead-code), collect exit status and timing metadata for each subagent and surface a structured diagnostic table to the user before the Phase 3 user-review pause point. The table is lightweight metadata (subagent name + status + duration + reason); it is not gated by the token budget that applies to scanner output.
Table format:
| Scan Subagent | Status | Duration | Reason |
|---|
| doc-code | success | 12s | — |
| hardcoded | success | 9s | — |
| integration-seam | timeout | 300s | exceeded 5-minute scanner budget |
| runtime-behavior | success | 14s | — |
| security | resource-capped | 60s | scan output truncated — review manually |
| config-contradiction | errored | 4s | parser crash on unrecognized YAML anchor |
| dead-code | success | 11s | — |
Canonical scan statuses (four values):
success — the scan completed and wrote its expected output file under .gaia/artifacts/planning-artifacts/.
timeout — the scan exceeded its time / token budget. The reason string MUST capture the budget threshold and the scanner identity (e.g., exceeded 5-minute scanner budget).
resource-capped — the scan output was truncated per AC-EC6. The reason string MUST surface the truncation advisory (scan truncated — review manually) so the user knows the gap list is partial.
errored — the scan crashed mid-run per AC-EC8 partial-failure semantics. The reason string MUST contain the underlying error (parser crash, file read failure, subagent unreachable) — do not silently omit failed scans from the diagnostic log.
The table is rendered to the conversation after Phase 3 scans complete, before the Phase 3 user-review pause point. Timed-out and errored scans MUST appear with their canonical status and reason string — they are never silently omitted from the log even though their gap rows are also tagged scan failed: {reason} per AC-EC8.
Phase 3 deterministic-tools telemetry
The brownfield report frontmatter records deterministic-tools telemetry so the
gap-consolidation report (Phase 7) can attribute runtime and token cost:
| Frontmatter field | Source | Notes |
|---|
phase_runtime_seconds.pre_warm | prewarm_seconds (pre-flight) | Wall-clock of the pre-warm pre-flight; tracked WARNING-only against the 120s budget — no hard timeout abort. |
deterministic_tool_seconds.pre_warm | prewarm_seconds | Deterministic-tool runtime contribution (same value; separated so LLM vs deterministic cost is distinguishable). |
llm_token_count | 0 for pre-warm | Pre-warm is fully deterministic — zero LLM tokens. |
gap_count_before_dedup | gap-consolidation | Populated by Phase 7 dedup; pre-warm contributes 0. |
gap_count_after_dedup | gap-consolidation | Populated by Phase 7 reconciliation; pre-warm contributes 0. |
phase_runtime_seconds.sarif_merge / deterministic_tool_seconds.sarif_merge | SARIF merge pre-step | Wall-clock of the SARIF Multitool merge; SARIF-merge-owned. |
phase_runtime_seconds.dedup / deterministic_tool_seconds.dedup | dedup sub-step | Wall-clock of the cross-tool dedup; dedup-owned. |
phase_runtime_seconds.grype / deterministic_tool_seconds.grype | Grype adapter | Wall-clock of the Grype CVE scan; Grype-owned. |
grype_db_checksum | Grype adapter | SHA-256 of the resolved grype-db.sqlite at scan time (trust-boundary). Grype-owned. |
grype_db_built_age | Grype adapter | Seconds since the Grype DB build timestamp. Grype-owned. |
phase_runtime_seconds.orchestrator_intersection / deterministic_tool_seconds.orchestrator_intersection | orchestrator | Wall-clock of the per-stack file-list intersection; orchestrator-owned. |
per_stack_file_counts.<stack> | orchestrator | Post-intersection file count per declared stack (explicit 0 for empty stacks). Orchestrator-owned. |
phase_runtime_seconds.detect_signals / deterministic_tool_seconds.detect_signals | detect-signals | Wall-clock of the Phase 1 stacks[].path proposal/audit; detect-signals-owned. |
detect_signals_mode | detect-signals | proposal | audit | skipped — the stacks-path mode taken. detect-signals-owned. |
phase_runtime_seconds.sbom_completeness / deterministic_tool_seconds.sbom_completeness | sbom-completeness | Wall-clock of the SBOM completeness check; sbom-completeness-owned. |
sbom_completeness_warning / divergence_pct / applied_threshold / detected_carve_outs | sbom-completeness | Lock-vs-SBOM divergence WARNING + the percentage, applied 10/15% threshold, and matched carve-outs. sbom-completeness-owned. |
phase_runtime_seconds.deadcode_go / deterministic_tool_seconds.deadcode_go | go-deadcode adapter | Wall-clock of the Go deadcode scan; go-deadcode-owned. |
phase_runtime_seconds.deadcode_python / deterministic_tool_seconds.deadcode_python | python-vulture adapter | Wall-clock of the Python vulture scan; python-vulture-owned. |
phase_runtime_seconds.deadcode_jvm / deterministic_tool_seconds.deadcode_jvm | jvm-spotbugs adapter | Wall-clock of the JVM SpotBugs scan; jvm-spotbugs-owned. |
phase_runtime_seconds.phase_4b / deterministic_tool_seconds.phase_4b | reconciliation | Wall-clock of the Phase 4b reconciliation JSON-join; reconciliation-owned. |
findings_demoted_by_reconciliation | reconciliation | Count of file-only findings demoted to INFO (reachable from entry points); reconciliation-owned. gap_count_* stay dedup-owned (read-through). |
phase_runtime_seconds.phase_4b_cross_stack / deterministic_tool_seconds.phase_4b_cross_stack | cross-stack analysis | Wall-clock of the Phase 4b cross-stack edge inspection; cross-stack-owned. |
cross_stack_warnings | cross-stack analysis | Array of {source_stack, source_file, target_stack, target_file} detail rows (possibly empty); cross-stack-owned. |
cross_stack_bypass_applied | cross-stack analysis | Bool — whether --bypass cross-stack-refs suppressed WARNINGs this run; cross-stack-owned. |
Single-author writer. Each field
is written by exactly ONE owning phase via brownfield-telemetry.sh — no fan-out.
Ownership: the pre-warm pre-flight owns *.pre_warm; the SARIF merge owns
*.sarif_merge; the dedup sub-step owns *.dedup + gap_count_* +
llm_token_count; the Grype adapter owns *.grype + grype_db_checksum +
grype_db_built_age; the orchestrator owns *.orchestrator_intersection +
per_stack_file_counts.*; detect-signals owns *.detect_signals +
detect_signals_mode; sbom-completeness owns *.sbom_completeness +
sbom_completeness_warning + divergence_pct + applied_threshold +
detected_carve_outs; the per-stack dead-code adapters own
*.deadcode_{go,python,jvm}; the cross-stack analysis owns
*.phase_4b_cross_stack + cross_stack_warnings + cross_stack_bypass_applied.
The gap_count_* values are populated for real by
the dedup / reconciliation phases.
Phase 4 — Test Execution During Discovery
After Phases 2 / 3 scans complete, execute the existing test suite at the project path to capture test failures as gap entries. This step is non-blocking — test execution failures must not halt the overall brownfield onboarding workflow.
Spawn a Test Execution Scanner subagent:
- Auto-detect test runners (package.json with
test script, pytest, Maven, Gradle, Go, Flutter) in priority order.
- Execute each detected runner with a 5-minute timeout.
- Parse test output for metrics (total, passing, failing, skipped).
- Convert failing tests to gap entries with severity mapped by test type (unit → medium, integration → high, e2e → critical).
- Detect infrastructure errors (ECONNREFUSED, missing env vars) and log as warning gaps rather than test-failure gaps.
- For monorepo / polyglot projects, execute all detected runners sequentially and aggregate results.
- Truncate output per token budget if needed.
- If no test suite is detected, log an info-level gap entry
GAP-TEST-INFO-001.
Output to .gaia/artifacts/planning-artifacts/brownfield-scan-test-execution.md. If the subagent fails to write its output file, log a warning and continue.
Phase 4b — Reconciliation
Inserted between Phase 4 (test execution) and Phase 5 (test-environment.yaml). When brownfield.deterministic_tools: true AND brownfield.phase_4b_enabled: true, Phase 4b reconciles Phase 3 file-only findings against the dependency graph and demotes (never removes) findings whose file is reachable from an application entry point — the barrel-file / dynamic-import false-positive guard that keeps FP rates tolerable for the deterministic-tools rollout.
It is a pure JSON-join — NO tool re-invocation — consuming already-computed outputs: the deduped finding stream + per-stack call-graphs (callgraph-{js,go,python}.json).
AUDIT="${GAIA_MEMORY_DIR:-.gaia/memory}/brownfield-audit"
REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md"
bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/reconcile.sh"
Demote, don't remove (audit integrity). A reachable finding keeps every identity field (file_path, qualifier, source_tool, ruleId, start_line — UNCHANGED) and gains severity: info, reconciled: true, original_severity, entry_points: [...], reconciliation_reason. Files not reachable retain their original severity. Single-level reachability suffices (the call-graphs already encode transitivity), so the join is O(n log n) build + O(n) lookup — < 5s on a 1M-line monorepo. Telemetry: findings_demoted_by_reconciliation, phase_runtime_seconds.phase_4b (single-author; gap_count_* stay dedup-owned). See scripts/adapters/brownfield/reconcile.README.md.
The Phase 4 → Phase 4b → Phase 5 ordering is preserved on every brownfield run. The cross-stack scope sub-step below composes WITHIN this Phase 4b body.
Phase 4b cross-stack scope + WARNING emission
A sub-step within Phase 4b. When brownfield.deterministic_tools: true AND brownfield.phase_4b_cross_stack_enabled: true, the reconciliation body above is extended with a cross-stack scope check that catches unintended coupling in multi-stack monorepos. It respects stacks[].path partitioning (per-stack reconciliation runs in isolation) and inspects the dependency-graph for edges that cross a stack boundary.
AUDIT="${GAIA_MEMORY_DIR:-.gaia/memory}/brownfield-audit"
bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/reconcile-cross-stack.sh" \
--bypass "$BYPASS_SKILL" --reason "$BYPASS_REASON"
An edge from stack A to stack B where B is NOT in A's cross_refs[] allowlist emits the canonical WARNING (exact — operators/CI may grep it):
unsanctioned-cross-stack-reference: <source_stack>:<file> -> <target_stack>:<file>
cross_refs[] is a per-source-stack outbound allowlist (A.cross_refs: [B] ⇒ A may reference B). Shared subdirs require explicit declaration on EACH consuming stack — there is no "shared resource" concept. Asymmetric allowlists are valid: if api declares [shared] but web does not, only web→shared warns.
Worked shared-subdir example. /shared imported by both /services/api and /services/web:
- Both declare
cross_refs: [shared] → no WARNING (both edges sanctioned).
- Drop
shared from web only → one WARNING for web→shared; api→shared stays silent.
Bypass. --bypass cross-stack-refs --reason "<text>" suppresses the WARNINGs for the run and appends to .gaia/memory/brownfield-audit/bypass-log.json. The flag is parsed by the shared scripts/lib/parse-bypass-flag.sh (required-reason, length 10–500); the reason char-class (^[A-Za-z0-9 ._-]+$, shell metachars rejected) is enforced in the adapter. See the canonical bypass-vocabulary doc (not duplicated here).
Performance. A {file→stack} reverse-index (longest-path-prefix match over stacks[].path) makes each edge an O(1) lookup — no per-edge graph walk; per-stack-pair detection is well under 100ms. Single-stack (path: null) collapses to one catch-all stack → zero cross-stack edges → byte-identical to the baseline (zero-regression). See scripts/adapters/brownfield/reconcile-cross-stack.README.md.
Phase 5 — Auto-Generate test-environment.yaml from Detected Infrastructure
This phase delegates manifest generation to the shared library helper at ${CLAUDE_PLUGIN_ROOT}/scripts/lib/test-environment-manifest.sh. The helper is the SINGLE canonical generator for .gaia/config/test-environment.yaml — both /gaia-brownfield Phase 5 (this section) and /gaia-bridge-enable Step 4 invoke it.
-
Invoke the shared helper with --target <project-path> --write to detect stack signals and emit the manifest. Set GAIA_TEST_ENV_CALLER=/gaia-brownfield so the generated manifest header is attributed to this skill (the helper defaults to /gaia-bridge-enable, which mislabels the file on brownfield runs):
GAIA_TEST_ENV_CALLER=/gaia-brownfield \
${CLAUDE_PLUGIN_ROOT}/scripts/lib/test-environment-manifest.sh \
--target "${PROJECT_PATH}" \
--write
The helper uses detect-signals.sh under the hood (same detection-signals.yaml registry brownfield consumes elsewhere). It writes to .gaia/config/test-environment.yaml with copy-if-absent semantics — if the file already exists, the helper preserves it byte-identical and exits 0.
-
Conflict resolution is handled by the helper:
- File does not exist → helper writes the stack-specific manifest. Log
Created test-environment.yaml from detected infrastructure.
- File exists → helper preserves it byte-identical. Log
test-environment.yaml already exists — preserved byte-identical (copy-if-absent).
- Detection finds no stack → helper writes a generic single-tier-1 placeholder so Layer 0 readiness has something valid to read. Log
No stack detected — generic placeholder written; user should customize.
-
If the helper exits non-zero, log the stderr message as a WARN-level entry and proceed. The conditional test_environment_yaml_required_when_infra_detected gate is NOT triggered when the helper succeeds OR when no stack is detected.
-
Normal-mode review pause: in normal mode, present a summary of the generated .gaia/config/test-environment.yaml (file path + detected stack name + runner names) and pause for user review before continuing to Phase 6. In yolo mode, skip the pause entirely and auto-continue. When the helper reported "preserved byte-identical" (existing file), the review pause is also skipped — the user is presumed to have already engaged with their existing manifest.
-
Record the helper exit code, detected stack, and chosen file disposition in the brownfield onboarding report for traceability.
Phase 6 — NFR Assessment & Performance Test Plan
Invoke the test-architect subagent (Sable) via the Agent tool:
- Analyze the codebase for non-functional requirements across code quality (linting, complexity, duplication), security posture (dependency vulnerabilities, secrets handling, auth quality), performance (bundle size for frontend, query patterns, caching, resource management), accessibility (ARIA, semantic HTML, keyboard nav for frontend), test coverage (framework, count, coverage %, untested areas, quality), and CI/CD (pipeline, deploy strategy, environments, IaC).
- Create an NFR Baseline Summary Table with measured values (not placeholders).
- Output the NFR assessment to
.gaia/artifacts/planning-artifacts/nfr-assessment/nfr-assessment-{date}.md (dated-snapshot subdir under planning-artifacts; moved out of flat test-artifacts/). Legacy ungrouped test-artifacts/nfr-assessment.md remains read-only fallback for projects pre-migration.
- Generate a performance test plan: load k6 patterns; if frontend, also load Lighthouse-CI patterns. Define performance budgets (P50/P95/P99), load test scenarios (gradual, spike, soak), backend profiling targets (slow queries, N+1, connection pools), CI performance gates. If frontend, define Core Web Vitals targets (LCP < 2.5s, INP < 200ms, CLS < 0.1).
- Non-deployable project routing. For a HEADLESS project (
compliance.ui_present: false AND no server / web platform AND no HTTP/RPC surface — e.g. a CLI, library, build tool), the k6 + Lighthouse defaults do not fit. The performance test plan in this case MUST follow the canonical non-deployable shape documented at ${CLAUDE_PLUGIN_ROOT}/skills/gaia-brownfield/tutorials/phase-5-for-non-deployable-projects.md rather than the k6/Lighthouse defaults. Replace load-testing scenarios with micro-benchmark targets (pytest-benchmark / Go's testing.B / criterion.rs / JMH, as appropriate to the stack); replace Core Web Vitals with cold-import / single-invocation / memory-peak budgets; replace CI performance gates with regression-threshold gates on the same benchmark suite. The deterministic test-architect (Sable) is told this from the subagent dispatch prompt; the dispatch prompt MUST include the non-deployable: true flag derived from the headless detection so Sable doesn't author a server-shape plan for a CLI.
- Output the performance test plan to
.gaia/artifacts/planning-artifacts/performance-test-plan/performance-test-plan-{date}.md (dated-snapshot subdir under planning-artifacts; legacy ungrouped test-artifacts/performance-test-plan-{date}.md remains read-only fallback for projects pre-migration).
AC-EC5 fallback — test-architect unavailable: If the test-architect subagent is not installed or unreachable at runtime, log a non-blocking warning and write a stub nfr-assessment.md with a clear banner:
> WARNING: test-architect subagent (Sable) unavailable at runtime.
> This is a stub file emitted by gaia-brownfield to satisfy the
> post-complete nfr_assessment_exists gate. Re-run /gaia-nfr after
> installing the test-architect agent to populate real content.
The post-complete gate then reports the gap rather than crashing. Also write a stub performance-test-plan-{date}.md with the same banner and re-run instruction.
AC-EC9 — both outputs required: The gate requires BOTH nfr-assessment.md AND performance-test-plan-{date}.md. If the subagent completes but emits only one of the two, the orchestrator MUST write the second (at minimum as a stub) so both exist before the post-complete gate fires. Missing either one halts the skill at the gate with the error text: HALT: NFR assessment not found at {test_artifacts}/nfr-assessment.md. or HALT: Performance test plan not found at {test_artifacts}/. Run /gaia-perf-testing. Both files are required for pass.
Gate check after Phase 6: Invoke the shared validate-gate pathway inline — see the Post-Complete Gates section at the end of this skill for the three gates enforced via !${CLAUDE_PLUGIN_ROOT}/scripts/validate-gate.sh.
Phase 7 — Gap Consolidation & Deduplication
Phase 7 grading rules
The deterministic-scan grading + gap-entry id convention used by the consolidation subagent:
- Empty-but-UNVERIFIED scan = WARNING, not PASS. When grype produces an empty
reconciled-findings.json AND the trust-boundary checksum is "unavailable" (the docker-runner case where under-runner checksum capture can fail), the consolidation MUST grade this as WARNING with the explanation "deterministic scan completed but DB trust-boundary unverifiable — re-run after refreshing the grype-db cache". A bare empty result alone (with a fresh, attributable DB) IS PASS. The distinction matters: an empty result from a deterministic scanner running against an up-to-date DB is a real signal; an empty result from a scanner whose DB age cannot be vouched for is not.
- Scan gap-entry id-prefix convention. Each Phase-3 scan subagent OWNS its prefix to avoid cross-file id collisions. The canonical prefix set:
DCD- (doc-code drift), HCV- (hardcoded values), ISEAM- (integration seam), RTB- (runtime behavior), SEC- (security), CFGC- (config contradiction), DC- (dead code), CVE- (grype CVE), SBM- (sbom-completeness). The consolidation subagent verifies prefix uniqueness when merging into consolidated-gaps.md; on collision it suffixes a stable -{counter} and logs a NOTICE.
Phase 7 PRE-step 0 — Scan-fidelity banner
Before SARIF merge runs, invoke /gaia-doctor (via its check-tools.sh) to determine the achievable scan tier and stamp it into consolidated-gaps.md:
DOCTOR_JSON=$(bash ${CLAUDE_PLUGIN_ROOT}/skills/gaia-doctor/scripts/check-tools.sh --json 2>/dev/null || echo '{}')
TIER=$(printf '%s' "$DOCTOR_JSON" | jq -r '.tier // "tier-0"')
TIER_REASON=$(printf '%s' "$DOCTOR_JSON" | jq -r '.tier_reason // "LLM-only (deterministic tools missing — heuristic fidelity)"')
MISSING_TOOLS=$(printf '%s' "$DOCTOR_JSON" | jq -r '[.tools[] | select(.state=="missing") | .id] | join(", ")')
REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md"
if [ -f "$REPORT" ] && head -1 "$REPORT" | grep -q '^---$'; then
awk -v tier="$TIER" -v reason="$TIER_REASON" '
NR==1 { print; print "scan_fidelity: " tier; print "scan_fidelity_reason: " reason; next }
/^---$/ && nseen<1 { nseen++; print; next }
{ print }
' "$REPORT" > "$REPORT.tmp" && mv "$REPORT.tmp" "$REPORT"
else
{
echo "---"
echo "scan_fidelity: $TIER"
echo "scan_fidelity_reason: $TIER_REASON"
echo "---"
echo ""
[ -f "$REPORT" ] && cat "$REPORT"
} > "$REPORT.tmp" && mv "$REPORT.tmp" "$REPORT"
fi
{
echo ""
TIER_UC="$(printf '%s' "$TIER" | tr '[:lower:]' '[:upper:]')"
echo "> **Scan fidelity: ${TIER_UC} (${TIER_REASON}).**"
if [ -n "$MISSING_TOOLS" ]; then
echo "> Deterministic CVE/SBOM/dead-code did not run (${MISSING_TOOLS} absent)."
echo "> This gap list is heuristic, not tool-verified. Re-run after \`gaia-doctor --install\` for tool-grade results."
fi
echo ""
} >> "$REPORT"
Why this exists (guiding principle): "Never degrade silently." Before this banner, an LLM-only scan looked byte-identical to a clean full-tier scan — operators read PASS verdicts on gap reports where no actual tool had run. The banner makes the degradation transparent: every consolidated-gaps.md now declares its achievable tier in frontmatter (scan_fidelity: tier-0|tier-1|tier-2) and renders a human-readable degradation notice in the report body when applicable. The frontmatter field is also machine-readable so downstream consumers (review skills, dashboards) can refuse to grade a Tier 0 scan as equivalent to a Tier 2 scan.
Phase 7 PRE-step — SARIF Multitool merge
Before the 6-step gap-consolidation recipe runs, merge all scanner SARIF outputs into one merged SARIF. This gives the recipe (and downstream dedup) a single uniform interchange format instead of bespoke per-tool JSON.
DET_TOOLS="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.deterministic_tools 2>/dev/null)"
SARIF_ON="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.sarif_merge_enabled 2>/dev/null)"
export GAIA_BROWNFIELD_DETERMINISTIC_TOOLS="${DET_TOOLS:-true}"
export GAIA_BROWNFIELD_SARIF_MERGE_ENABLED="${SARIF_ON:-true}"
sarif_merge_start=$(date +%s)
bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/sarif-merge.sh" || true
sarif_merge_seconds=$(( $(date +%s) - sarif_merge_start ))
SARIF_REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md"
SARIF_TELEM="${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/brownfield-telemetry.sh"
if [ -f "$SARIF_REPORT" ]; then
bash "$SARIF_TELEM" --report "$SARIF_REPORT" --field phase_runtime_seconds.sarif_merge --value "$sarif_merge_seconds" || true
bash "$SARIF_TELEM" --report "$SARIF_REPORT" --field deterministic_tool_seconds.sarif_merge --value "$sarif_merge_seconds" || true
fi
DD_ON="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.defectdojo_enabled 2>/dev/null)"
export GAIA_BROWNFIELD_DEFECTDOJO_ENABLED="${DD_ON:-false}"
if [ "${GAIA_BROWNFIELD_DEFECTDOJO_ENABLED}" = "true" ]; then
export GAIA_BROWNFIELD_DEFECTDOJO_API_URL="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.defectdojo_api_url 2>/dev/null)"
DD_TOKEN_VAR="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.defectdojo_api_token 2>/dev/null)"
export GAIA_BROWNFIELD_DEFECTDOJO_API_TOKEN="${!DD_TOKEN_VAR:-}"
export GAIA_BROWNFIELD_DEFECTDOJO_ENGAGEMENT_ID="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.defectdojo_engagement_id 2>/dev/null)"
fi
bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/defectdojo-export.sh" \
"${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/brownfield-sarif-merged.json" || true
SARIF-as-interchange rationale (AC6). SARIF 2.1.0 is the consensus interchange format — named independently by Zara (security), Soren (DevOps), Hugo (Java), and Derek (PM) in the brownfield deterministic-tools meeting. Microsoft's Sarif.Multitool is the canonical merger: it preserves per-tool attribution by concatenating one run per scanner (each carrying its tool.driver.name). Migrating bespoke per-tool JSON to merged SARIF removes per-tool parser maintenance and enables uniform downstream consumption (dedup, reconciliation, ranking). See scripts/adapters/brownfield/sarif-merge.README.md.
Migration shim (1-sprint deprecation). When no *.sarif inputs exist (or the flag is off), sarif-merge.sh emits a WARN/INFO line and the 6-step recipe below falls back to its prior per-tool JSON consumption at Step 1. The legacy per-tool JSON path is slated for removal in the next sprint.
sarif_merge_seconds feeds the phase_runtime_seconds.sarif_merge telemetry field (see the Phase 3 telemetry subsection; the populating writer is brownfield-telemetry.sh).
Phase 7 dedup sub-step
Immediately after the SARIF merge PRE-step and BEFORE Phase 4b reconciliation, run the cross-tool dedup over the merged SARIF. Dedup is the FIRST sub-step of the 6-step recipe (reordered to load → dedup → validate → rank → budget → write) — dedup-first shrinks the working set the downstream validate/rank/budget steps process. Dedup uses two key shapes (CVE-class keyed (CVE-ID, file, severity) with Grype-canonical tie-break; non-CVE-class grouped (file, symbol) with the precision ladder) per docs/dedup-contract.md.
DEDUP_ON="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field brownfield.dedup_enabled 2>/dev/null)"
export GAIA_BROWNFIELD_DEDUP_ENABLED="${DEDUP_ON:-true}"
dedup_start=$(date +%s)
dedup_log="$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/dedup.sh" 2>&1 || true)"
printf '%s\n' "$dedup_log"
dedup_seconds=$(( $(date +%s) - dedup_start ))
gap_before="$(printf '%s' "$dedup_log" | sed -n 's/.*gap_count_before_dedup=\([0-9][0-9]*\).*/\1/p' | head -n1)"
gap_after="$(printf '%s' "$dedup_log" | sed -n 's/.*gap_count_after_dedup=\([0-9][0-9]*\).*/\1/p' | head -n1)"
REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md"
TELEM="${CLAUDE_PLUGIN_ROOT}/scripts/adapters/brownfield/brownfield-telemetry.sh"
if [ -f "$REPORT" ]; then
bash "$TELEM" --report "$REPORT" --field phase_runtime_seconds.dedup --value "$dedup_seconds" || true
bash "$TELEM" --report "$REPORT" --field deterministic_tool_seconds.dedup --value "$dedup_seconds" || true
bash "$TELEM" --report "$REPORT" --field llm_token_count --value 0 || true
[ -n "$gap_before" ] && bash "$TELEM" --report "$REPORT" --field gap_count_before_dedup --value "$gap_before" || true
[ -n "$gap_after" ] && bash "$TELEM" --report "$REPORT" --field gap_count_after_dedup --value "$gap_after" || true
fi
When the dedup flag is off (or the master flag is off), dedup.sh passes the raw stream through unchanged (gap_count_before_dedup == gap_count_after_dedup) and logs an INFO skip. The deduped stream is written to .gaia/memory/brownfield-audit/deduped-findings.json for the Phase 4b reconciliation consumer.
Single-author-per-field telemetry. brownfield-telemetry.sh is the shared writer mechanism, but each field has exactly ONE owning phase: the pre-warm pre-flight owns *.pre_warm, the SARIF merge owns *.sarif_merge, and this dedup sub-step owns *.dedup + gap_count_* + llm_token_count. No field is written by more than one phase (no fan-out).
Phase 7 dead-code "Test Quality" render sub-step
After the dedup sub-step, render the unified Test Quality section into
consolidated-gaps.md from the three per-stack dead-code adapter outputs
collected in Phase 3. This is a NET-NEW section (created, not edited) and is
intentionally rendered as ONE ## Test Quality H2 with THREE per-stack H3
sub-sections (Go / Python / JVM) — each showing its stack-native qualifier
verbatim. The renderer is idempotent (re-running replaces, never duplicates).
AUDIT="${GAIA_MEMORY_DIR:-.gaia/memory}/brownfield-audit"
REPORT="${GAIA_ARTIFACTS_DIR:-.gaia/artifacts}/planning-artifacts/consolidated-gaps.md"
bash "${CLAUDE_PLUGIN_ROOT}/scripts/adapters/dead-code/render-test-quality.sh" \
--out-dir "$AUDIT" --report "$REPORT" || true
Anti-pattern guard (AC4). The section MUST NOT collapse the three
stacks into one flat list with a synthesized cross-stack confidence column — Go's
binary reachability verdict, Python's confidence %, and JVM's priority×rank ordinal
are NOT commensurable. Per-stack precision is the contract this section preserves.
The per-stack SARIF runs (Phase 3) ALSO feed
the dedup precision ladder above via .properties.symbol, so the dead-code findings
participate in cross-tool dedup without forfeiting their native qualifier.
Spawn a Gap Consolidation subagent:
Step 1 — Load all scan outputs. When the SARIF merge pre-step produced .gaia/artifacts/planning-artifacts/brownfield-sarif-merged.json, load gap entries from that MERGED SARIF as the primary scanner-finding source (one run per contributing scanner, attributed by tool.driver.name). When the merge was skipped/fell back (no SARIF inputs or flag off), load gap entries from the legacy per-tool sources below instead. In BOTH paths, also load the non-scanner gap sources. If a file is empty or missing, log a warning noting which scanner produced no results and continue.
- Deep analysis scans (7 files from Phase 3): config-contradiction, dead-code, hardcoded, security, runtime-behavior, doc-code, integration-seam.
- Test execution scan (1 file from Phase 4): brownfield-scan-test-execution.md.
- Phase 2 documentation outputs (4 files): api-documentation.md (API gaps), event-catalog.md (messaging gaps), ux-design.md (frontend/UX gaps), dependency-map.md (dependency gaps).
- Phase 6 NFR: nfr-assessment.md (NFR gap findings).
Step 2 — Validate entries against schema. Required fields: id, category, severity, title, description (or evidence), evidence_file, evidence_line, recommendation. Entries missing required fields are logged as warnings (noting source file and missing field) and skipped from consolidation.
Step 3 — Deduplicate (LLM entry-level). Cross-tool scanner-finding dedup already ran as the deterministic dedup sub-step BEFORE this recipe (dedup.sh, the FIRST sub-step of the reordered load → dedup → validate → rank → budget → write recipe). This Step 3 is a SECONDARY, coarser entry-level dedup over the consolidated gap entries (including the non-scanner sources). Group gap entries by evidence_file + evidence_line exact match. For each group:
- Retain the entry with the highest severity (critical > high > medium > low).
- Merge recommendations from all duplicate entries into the retained entry.
- Add a
merged_from field listing all original gap IDs.
- If duplicates have different categories, retain the primary category from the highest-severity entry and note the alternate category in the description.
Step 4 — Rank. Sort by severity DESC, then confidence DESC, then category alphabetical. Assign sequential numbering.
Step 5 — Budget check. Estimate token count (~100 tokens per gap entry). If the total exceeds the 40K token budget, truncate low-severity and info entries with a count summary N additional low/info gaps omitted for budget. Stay within budget (AC-EC6).
Step 6 — Generate consolidated output. Write .gaia/artifacts/planning-artifacts/consolidated-gaps.md with summary statistics at the top:
- Total raw gaps (pre-dedup count)
- Duplicates removed
- Final unique count
- Breakdown by category
- Breakdown by severity
- Per-scanner source counts
Phase 8 — PRD + Adversarial Review + Code-Verified Review
8a — Create PRD for Gaps
PRD template plugin-relative location. The template files named below all live under ${CLAUDE_PLUGIN_ROOT}/skills/gaia-create-prd/. Reference ${CLAUDE_PLUGIN_ROOT}/skills/gaia-create-prd/prd-template.md, ${CLAUDE_PLUGIN_ROOT}/skills/gaia-create-prd/infra-prd-template.md, ${CLAUDE_PLUGIN_ROOT}/skills/gaia-create-prd/platform-prd-template.md. Spelling out the location saves the consolidation subagent from having to find them.
5-tier (scan severity) ↔ 3-tier (config-severity) mapping. Deterministic scans use a 5-tier severity scale (Critical / High / Medium / Low / Info); the operator's config-severity knob is a 3-tier scale (high / medium / low). When the consolidation subagent reconciles scan findings into the PRD's gap list, apply this canonical mapping:
| Scan severity | config-severity bucket |
|---|
| Critical | high |
| High | high |
| Medium | medium |
| Low | low |
| Info | (dropped from PRD; logged in scan-fidelity banner only) |
Apply the bucket as the gap row's priority — so a Critical CVE and a High dead-code finding both land in the high bucket without losing the original scan-tier in the source-of-truth scan report.
Select the PRD template based on {project_type}:
| project_type | Template File | Requirement ID Scheme |
|---|
| application | prd-template.md | FR-###, NFR-### |
| infrastructure | infra-prd-template.md | IR-###, OR-###, SR-### |
| platform | platform-prd-template.md | FR-###, NFR-### and IR-###, OR-###, SR-### |
Verify the template file exists. If missing, halt with Template {selected_template} not found. Ensure the infra and platform templates are installed. If {project_type} is unrecognized, default to application.
Read upstream artifacts to inform gap analysis:
project-documentation.md → project context (tech stack, patterns, conventions, capability flags, CI/CD).
consolidated-gaps.md → primary input (deduplicated, ranked, code-verified gap list). If a ## Verification Corrections for PRD section exists (from Phase 8c), apply its corrections.
nfr-assessment.md → NFR "Current Baseline" and "Target" columns with real values.
api-documentation.md (if exists) → API gaps.
event-catalog.md (if exists) → messaging gaps.
dependency-map.md → dependency risks.
dependency-audit-{date}.md → critical / high findings.
ux-design.md (if exists) → UX gaps.
Generate the PRD in brownfield mode — every section filled with gap-focused content only. Overview = existing project summary + what gaps this PRD addresses. Goals = gap closure goals only. Non-Goals = existing features that will NOT be re-implemented. User Stories = gap stories only. Functional Requirements = gap requirements organized by priority. NFRs = NFR gaps with baseline and target from the NFR assessment.
If prd.md already exists, warn the user: A PRD already exists. Continuing will overwrite it with brownfield gap content. Choose: (a) overwrite, (b) save as prd-brownfield-gaps.md instead. If the user chooses (b), adjust the output path.