Claude Skill

skf-verify-stack

Pre-code stack feasibility verification against architecture and PRD documents. Use when the user requests to "verify a tech stack" or "verify stack."

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download armelhbobdad-bmad-module-skill-forge-src_skf-verify-stack-492e73e.zip · 47 KB
Part of armelhbobdad/bmad-module-skill-forge — 15 skills

Install

skills CLI npx skills add https://github.com/armelhbobdad/bmad-module-skill-forge/tree/main/src/skf-verify-stack
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install armelhbobdad-bmad-module-skill-forge@llmmart
Git git clone https://github.com/armelhbobdad/bmad-module-skill-forge.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole armelhbobdad/bmad-module-skill-forge collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Verify Stack

Overview

Cross-references generated skills against architecture and PRD documents to produce a feasibility report with evidence-backed integration verdicts, coverage analysis, and requirements mapping. Read-only: it reads skills and input documents and writes only the feasibility report (see Workflow Rules).

Schema contract: This skill is the producer of the SKF shared feasibility report schema — every report conforms to it.

Conventions

  • Bare paths (e.g. references/<name>.md) resolve from the skill root.
  • references/ holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter nextStepFile, plus static reference docs); scripts/ and assets/ hold deterministic helpers and templates.
  • {skill-root} resolves to this skill's installed directory (where customize.toml lives, if present).
  • {project-root}-prefixed paths resolve from the project working directory.
  • {skill-name} resolves to the skill directory's basename.

Role

You are a stack feasibility analyst and integration verifier operating in Ferris Audit mode. You bring expertise in API surface analysis, cross-library compatibility assessment, and architecture validation, while the user brings their architecture vision and generated skills.

Workflow Rules

These rules apply to every step in this workflow:

  • Read-only — never modify skills, architecture docs, or PRD files
  • Every verdict must cite evidence from the generated skills
  • Only load one step file at a time — never preload future steps
  • If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread
  • Always communicate in {communication_language}
  • At any interactive prompt, the inputs cancel, exit, [X], q, or :q exit cleanly with exit code 6 (halt_reason: "user-cancelled")
  • If {headless_mode} is true, auto-proceed through confirmation gates with their default action and log each auto-decision

Stages

# Step File Auto-proceed
1 Initialize & Load Inputs references/init.md No (confirm)
2 Coverage Analysis references/coverage.md Yes
3 Integration Verification references/integrations.md Yes
4 Requirements Mapping references/requirements.md Yes
5 Synthesize Verdict references/synthesize.md Yes
6 Report references/report.md No (confirm)
7 Workflow Health Check references/health-check.md Yes

Invocation Contract

Aspect Detail
Inputs architecture_doc_path [required], prd_path [optional], previous_report_path [optional]
Flags --headless / -H (auto-resolve all gates); --architecture-doc <path> (skip step 1 prompt for the required input); --prd <path> (skip step 1 prompt for the optional PRD); --previous-report <path> (skip step 1 prompt for delta comparison)
Gates step 1 Input Gate (use args); step 6 Report Menu ([R] review / [X] exit, headless default X). Steps 2-3 also hold elective vacuous-analysis guards (0% coverage, all-Blocked) that only fire in degenerate cases; every guard auto-resolves to Continue in headless.
Outputs feasibility-report-{projectSlug}-{timestamp}.md and feasibility-report-{projectSlug}-latest.md (copy, not symlink) per the SKF shared feasibility report schema (_bmad/skf/shared/references/feasibility-report-schema.md; src/shared/references/… in a dev checkout) — with integration verdicts, coverage analysis, recommendations, and evidence sources; plus verify-stack-result-{timestamp}.json and verify-stack-result-latest.json
Headless All gates auto-resolve with default action when {headless_mode} is true. Per-flag args (--architecture-doc, --prd, --previous-report) consumed at the gates that would otherwise prompt.
Exit codes See references/exit-codes.md

Result Contract (Headless)

When {headless_mode} is true, step 6 emits a single-line JSON envelope on stdout before chaining to step 7, and every headless hard halt emits the same envelope shape on stderr with status: "error":

SKF_VERIFY_STACK_RESULT_JSON: {"status":"success|error","report_path":"…|null","report_latest_path":"…|null","overall_verdict":"…|null","coverage_percentage":0,"recommendation_count":0,"exit_code":0,"halt_reason":null}

status is "success" on the terminal happy path, "error" on any halt. halt_reason is one of: null (success), "input-missing", "input-invalid", "skills-folder-missing", "insufficient-skills", "forge-folder-unconfigured", "resolution-failure", "previous-report-collision", "inventory-unreliable", "schema-violation", "write-failed", "user-cancelled". exit_code matches references/exit-codes.md (the analysis-halted / exit-8 gates are interactive-only, so they never reach this envelope). overall_verdict uses the schema tokens (FEASIBLE/CONDITIONALLY_FEASIBLE/NOT_FEASIBLE).

On Activation

  1. Load config from {project-root}/_bmad/skf/config.yaml and resolve:

    • project_name, user_name, communication_language, document_output_language
    • skills_output_folder, forge_data_folder, sidecar_path
  2. Compute run-scoped variables (same place as config so every stage can reference them without re-derivation):

    • project_slug ← slugify project_name (lowercase, hyphens only, no unicode, no whitespace)
    • timestamp ← UTC YYYYMMDD-HHmmss captured at activation time
    • These two combine in init.md §4 into {outputFile} per the stage frontmatter template, but the values themselves are fixed for the entire workflow run — every later reference to {outputFile} resolves consistently.
  3. Resolve {headless_mode}: true if --headless or -H was passed as an argument, or if headless_mode: true in {sidecar_path}/preferences.yaml. Default: false.

  4. Resolve workflow customization. Run:

    python3 {project-root}/_bmad/scripts/resolve_customization.py \
        --skill {skill-root} --key workflow
    

    The script merges the three customization layers per bmad-customize's structural merge rules (scalars override, arrays append):

    • {skill-root}/customize.toml — bundled defaults
    • _bmad/custom/<skill-name>.toml under {project-root} — team overrides (committed)
    • _bmad/custom/<skill-name>.user.toml under {project-root} — personal overrides (gitignored)

    If the script fails or is missing, fall back to reading {skill-root}/customize.toml directly — the bundled defaults are an empty string for each path scalar.

    Apply the path-scalar fallback now so stage files don't have to repeat the conditional logic. For each of the four scalars, if the merged value is empty or absent, use the bundled default:

    • {reportTemplatePath} ← workflow.report_template_path if non-empty, else assets/feasibility-report-template.md
    • {integrationRulesPath} ← workflow.integration_rules_path if non-empty, else references/integration-verification-rules.md
    • {coveragePatternsPath} ← workflow.coverage_patterns_path if non-empty, else references/coverage-patterns.md
    • {outputFolderPath} ← workflow.output_folder_path if non-empty, else {forge_data_folder}

    Stash all four as workflow-context variables. Stage files reference them directly — no conditional at the usage site. Empty-string overrides cleanly fall through to the bundled default.

    The same merge resolves workflow.on_complete (default empty = no-op); report.md §5 executes it, if non-empty, at the terminal stage.

    Also apply the array surfaces so they are not silent no-ops: execute each entry in workflow.activation_steps_prepend in order now; treat every entry in workflow.persistent_facts as standing context for the whole run (file:-prefixed entries load their file/glob contents as facts — the bundled default glob is {project-root}/**/project-context.md); then execute each entry in workflow.activation_steps_append after activation completes.

  5. Pre-flight write probe. Verify {outputFolderPath} is writable. A read-only mount, full disk, or permissions-denied path otherwise only surfaces at init.md §4 atomic write — by then the user has already gone through the input prompts:

    mkdir -p "{outputFolderPath}" && \
      printf 'probe' > "{outputFolderPath}/.skf-write-probe" && \
      rm "{outputFolderPath}/.skf-write-probe"
    

    On any non-zero exit: HALT (exit code 4, halt_reason: "write-failed"). In headless mode, emit the error envelope per Result Contract (Headless) with report_path: null, report_latest_path: null, overall_verdict: null.

  6. Load, read the full file, and then execute references/init.md to begin the workflow.

Files (bmad-module-skill-forge)
  • assets
    • feasibility-report-template.md 1.7 KB
      ---
      schemaVersion: "1.0"
      reportType: feasibility
      projectName: ""
      projectSlug: ""
      generatedAt: ""
      generatedBy: skf-verify-stack
      overallVerdict: "CONDITIONALLY_FEASIBLE"
      coveragePercentage: 0
      pairsVerified: 0
      pairsPlausible: 0
      pairsRisky: 0
      pairsBlocked: 0
      recommendationCount: 0
      prdAvailable: false
      # Producer-local bookkeeping (not part of the shared consumer contract):
      workflowType: 'verify-stack'
      architectureDoc: ''
      prdDoc: ''
      previousReport: ''
      skillsAnalyzed: 0
      stepsCompleted: []
      requirementsPass: ''
      requirementsFulfilled: null
      requirementsPartial: null
      requirementsNotAddressed: null
      deltaImproved: null
      deltaRegressed: null
      deltaNew: null
      deltaUnchanged: null
      ---
      
      # Stack Feasibility Report: {projectName}
      
      **Verification Date:** {generatedAt}
      **Architecture Document:** {architectureDoc}
      **PRD Document:** {prdDoc}
      **Skills Analyzed:** {skillsAnalyzed}
      
      > Schema contract: `{feasibilitySchemaRef}` (schemaVersion `1.0`). Consumers MUST halt on `schemaVersion` mismatch.
      
      ## Executive Summary
      
      **Overall Verdict:** {FEASIBLE | CONDITIONALLY_FEASIBLE | NOT_FEASIBLE}
      
      {1-2 sentence summary}
      
      ---
      
      ## Coverage Analysis
      
      <!-- Appended by coverage -->
      
      ---
      
      ## Integration Verdicts
      
      <!-- Appended by integrations.
      Consumers grep for the `## Integration Verdicts` heading to locate the pair table.
      The table header is fixed and MUST be emitted exactly as shown below: -->
      
      | lib_a | lib_b | verdict | rationale |
      |-------|-------|---------|-----------|
      
      ---
      
      ## Recommendations
      
      <!-- Appended by synthesize -->
      
      ---
      
      ## Evidence Sources
      
      <!-- Appended by synthesize — cite each skill's SKILL.md path, metadata_schema_version,
           confidence_tier, stack manifest (if any), and architecture/PRD doc paths -->
      
  • references
    • coverage-patterns.md 1.1 KB
      # Coverage Patterns
      
      ## Purpose
      
      Rules for detecting technology/library references in architecture and PRD documents, and matching them against generated skills.
      
      ---
      
      ## Technology Detection in Documents
      
      ### Direct Name Matching
      
      Search the architecture document for exact mentions of:
      1. Library names from generated skills (case-insensitive)
      2. Common aliases (e.g., "React" also matches "ReactJS", "react.js")
      3. Framework names that encompass libraries (e.g., "Tauri" encompasses the Tauri ecosystem)
      
      ### Section-Based Detection
      
      Parse document section headers for technology groupings:
      - `## Desktop App` → technologies listed under this section
      - `## Backend Core` → technologies in backend layer
      - `## AI Layer` → AI-related technologies
      
      **Mermaid Diagram Handling:** Do not parse Mermaid diagram syntax (`graph`, `flowchart`, `sequenceDiagram`, etc.) for technology detection — use only prose text (headings, paragraphs, lists, tables). If the architecture document appears to list technologies exclusively inside Mermaid diagrams, note this in the coverage results as a detection limitation and recommend the user add prose-based technology listings.
      
    • coverage.md 11.5 KB
      ---
      nextStepFile: 'integrations.md'
      coveragePatternsData: '{coveragePatternsPath}'
      coverageTallyScript: 'scripts/skf-coverage-tally.py'
      feasibilitySchemaProbeOrder:
        - '{project-root}/_bmad/skf/shared/references/feasibility-report-schema.md'
        - '{project-root}/src/shared/references/feasibility-report-schema.md'
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
      outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
      ---
      
      <!-- Config: communicate in {communication_language}. Append the Coverage Analysis section to the report in {document_output_language}. -->
      
      # Step 2: Technology Coverage Analysis
      
      ## STEP GOAL:
      
      Verify that a generated skill exists for every technology, library, or framework referenced in the architecture document. Produce a coverage matrix showing which technologies are covered and which are missing. Detect extra skills not referenced in the architecture.
      
      ## Rules
      
      - Focus only on technology-to-skill coverage mapping — do not analyze API surfaces (Step 03) or requirements (Step 04)
      - Coverage verdicts must be binary: Covered or Missing
      
      ## MANDATORY SEQUENCE
      
      ### 1. Load Coverage Patterns
      
      Load `{coveragePatternsData}` for detection rules.
      
      Extract: technology name patterns, section heading indicators, common aliases, and framework-to-library mappings.
      
      ### 2. Extract Technology References
      
      Parse the architecture document for technology, library, and framework names.
      
      **Detection methods (apply in order):**
      
      **Section-based detection:**
      - Identify section headings that indicate technology listings (e.g., "Tech Stack", "Dependencies", "Technologies", "Libraries", layer-specific headings)
      - Extract technology names listed under these headings
      
      **Direct name matching:**
      - Scan the full document for names that match loaded skill names (case-insensitive)
      - Apply alias resolution from {coveragePatternsData} (e.g., "React" matches "react", "PostgreSQL" matches "postgres")
      
      **Contextual detection:**
      - Identify technology names mentioned in prose alongside architectural descriptions
      - Look for version-pinned references (e.g., "Express v4", "Tailwind CSS 3.x")
      
      **Build a deduplicated list** of all referenced technologies with the document section where each was found.
      
      ### 3. Cross-Reference Against Skills
      
      For each referenced technology in the list:
      
      **Check if a matching skill exists** in the skill inventory from Step 01.
      - Match by skill name (case-insensitive)
      - Match by alias from {coveragePatternsData}
      - Match by `source_repo` or `source_root` field in metadata.json if skill name differs from technology name, using this algorithm:
        1. For `source_repo`: extract the basename (last URL segment after the final `/`), strip any trailing `.git` suffix, lowercase
        2. For `source_root`: take the last path segment (after the final `/` or `\`), lowercase
        3. Lowercase each architecture tech token
        4. Compare the resulting basenames/segments against the tech tokens via case-insensitive equality (no substring/fuzzy matching)
        5. A match on either `source_repo` basename or `source_root` last segment counts as a hit
      
      **Detect a deliberate-removal signal (from the architecture document):** Before assigning Covered/Missing, check whether the referenced technology is explicitly marked for removal or replacement in the architecture document itself. Be conservative — recognize a removal signal only when one of these is present, and when in doubt leave it as a normal reference (a false removal signal silently drops a real coverage gap):
      - The technology is listed under a section whose heading matches (case-insensitive) one of: "deprecated", "removed", "legacy", "migrating away", "being replaced", "to be removed", "sunset", "retiring".
      - The technology's own mention carries an inline removal annotation, e.g. "(deprecated)", "(being replaced by …)", "(removing)", "(to be removed)", "(legacy)".
      
      Record the cited section heading or annotation text as evidence for every technology flagged this way.
      
      **Assign verdict:**
      - **Covered** — a matching skill exists in the inventory
      - **Replaced** — no matching skill exists AND a deliberate-removal signal (above) was found; the technology is intentionally being removed/replaced, so no skill should exist for it
      - **Missing** — no matching skill found and no removal signal
      
      Build the coverage matrix as a structured table.
      
      **Tally the matrix deterministically.** Assigning each verdict is judgment; counting the classes and computing the percentage has one correct answer, so delegate it. Serialize the matrix as `{"rows": [{"technology": "…", "verdict": "Covered|Missing|Replaced"}, …]}` and run:
      
      ```bash
      echo '<rows JSON>' | uv run {coverageTallyScript} --stdin
      ```
      
      The script (run `uv run {coverageTallyScript} --help` for the contract) returns `covered_count`, `missing_count`, `replaced_count`, `live_count` (the denominator — Covered + Missing, with Replaced excluded because a technology being removed is not a gap to close), `total_referenced`, and `coverage_percentage` (Replaced excluded from the denominator, half-up rounding pinned so the same matrix always yields the same integer). Consume these values in §5 and §6 rather than recomputing them. If `uv` is unavailable (e.g. claude.ai web), compute the same values inline per the `--help` contract: `live_count = covered + missing`; `coverage_percentage = round-half-up(covered / live_count * 100)`, or `0` when `live_count` is `0`.
      
      ### 4. Detect Extra Skills
      
      Check if any skills in the inventory are not referenced in the architecture document.
      
      **Subdivide into two categories (both informational — not errors):**
      - **Extra (unreferenced)** — The skill's `source_repo` / `source_root` resolves cleanly (both non-empty and well-formed), but no architecture document tech token matches it.
      - **Orphan (source_repo unresolvable)** — The skill's `source_repo` is empty, malformed (not a valid URL-like string), OR its basename cannot be deterministically extracted. Cross-reference against architecture tokens is not possible for this skill.
      
      **For each extra skill:**
      - If `source_repo` resolves → mark as **Extra (unreferenced)**, note: "Skill `{skill_name}` exists and has a resolvable `source_repo`, but no architecture reference was found."
      - If `source_repo` does not resolve → mark as **Orphan (source_repo unresolvable)**, note: "Skill `{skill_name}` has no resolvable `source_repo` — cannot cross-reference against architecture. Re-run [CS] or update the skill's metadata."
      
      Extra and Orphan skills are informational only. They do not affect the coverage verdict.
      
      ### 5. Display Coverage Results
      
      "**Pass 1: Technology Coverage**
      
      | Technology | Source Section | Skill Match | Verdict |
      |------------|---------------|-------------|---------|
      | {tech_name} | {section_heading} | {skill_name or '—'} | {Covered / Missing / Replaced} |
      
      **Coverage: {covered_count}/{live_count} ({coverage_percentage}%)** (from the §3 tally; `live_count` excludes **Replaced** technologies)
      
      {IF 100% coverage AND no Extra skills:}
      **All referenced technologies have a matching skill. No extra skills detected.**
      
      {IF any Missing:}
      **Missing Skills — Action Required:**
      {For each missing technology:}
      - `{tech_name}` → Run **[CS] Create Skill** or **[QS] Quick Skill** for `{tech_name}`, then re-run **[VS]**
      
      {IF any Replaced:}
      **Replaced / Being Removed (informational — no skill needed):**
      {For each replaced technology:}
      - `{tech_name}` — marked for removal/replacement in the architecture document ({cited_removal_evidence}); excluded from coverage. No skill should be created; if the technology is not actually being removed, correct the architecture document and re-run **[VS]**.
      
      {IF any Extra:}
      **Extra Skills (informational):**
      {For each extra skill:}
      - `{skill_name}` — not referenced in architecture document"
      
      ### 6. Append to Report
      
      **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
      
      **Resolve `{feasibilitySchemaRef}`** from `{feasibilitySchemaProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback).
      
      Write the **Coverage Analysis** section to `{outputFile}` (see `{feasibilitySchemaRef}` — section headings are fixed and ordered: `## Executive Summary`, `## Coverage Analysis`, `## Integration Verdicts`, `## Recommendations`, `## Evidence Sources`):
      - Include the full coverage table
      - Include coverage percentage
      - Include missing skill recommendations
      - Include the Replaced (being removed/replaced) subdivision from section 3, with the cited removal evidence — these are not gaps and carry no [CS]/[QS] recommendation
      - Include the Extra (unreferenced) and Orphan (source_repo unresolvable) subdivisions from section 4
      - Update frontmatter: append `'coverage'` to `stepsCompleted`; set `coveragePercentage` to the `coverage_percentage` value from the §3 tally (integer 0..100)
      - Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
      
      ### 7. Auto-Proceed to Next Step
      
      {IF live_count is 0 — every referenced technology is marked Replaced:}
      "**⚠️ Every referenced technology is marked for removal/replacement — there is no live technology to verify.** Coverage is reported as 0%; the architecture document describes only technologies being removed, so the stack cannot be assessed as described.
      
      **Recommended:** Update the architecture document to describe the technologies that remain after the pivot, then re-run [VS].
      
      **Select:** [X] Halt workflow (recommended) | [C] Continue anyway"
      
      **GATE [default: C]** — Interactive-only guard. If `{headless_mode}`: auto-proceed with [C] Continue, log: "headless: continuing past all-Replaced coverage gate (nothing live to verify)". Headless never takes [X], so step 6 still emits the result contract — the run resolves to `NOT_FEASIBLE` via the synthesize zero-coverage short-circuit.
      
      - IF X: "**Workflow halted.** Coverage Analysis saved to `{outputFile}`. Update the architecture document and re-run [VS] when ready." HALT (exit code 8, `halt_reason: "analysis-halted"`).
      - IF C: "**Continuing — the analysis covers only technologies marked for removal and will be limited.**" Load, read the full file and then execute `{nextStepFile}`.
      
      {IF coveragePercentage is 0% AND live_count > 0:}
      "**⚠️ 0% coverage — no matching skills found for any referenced technology.** All subsequent analysis (integration, requirements) will be vacuous and produce empty tables.
      
      **Recommended:** Generate skills with [CS] or [QS] for your architecture technologies, then re-run [VS].
      
      **Select:** [X] Halt workflow (recommended) | [C] Continue anyway"
      
      **GATE [default: C]** — Interactive-only guard. If `{headless_mode}`: auto-proceed with [C] Continue, log: "headless: continuing past 0%-coverage gate". Headless never takes [X], so step 6 still emits the result contract — the run resolves to `NOT_FEASIBLE` via the synthesize zero-coverage short-circuit.
      
      - IF X: "**Workflow halted.** Coverage Analysis saved to `{outputFile}`. Generate skills and re-run [VS] when ready." HALT (exit code 8, `halt_reason: "analysis-halted"`).
      - IF C: "**Continuing with 0% coverage — results will be limited.**"
      
        Load, read the full file and then execute `{nextStepFile}`.
      
      {IF coveragePercentage is not 0:}
      "**Proceeding to integration analysis...**"
      
      Load, read the full file and then execute `{nextStepFile}`.
      
      
    • exit-codes.md 2.2 KB
      # Exit Codes
      
      Every hard halt in this workflow exits with a stable code so headless automators can branch on the failure class without grepping message text. Each code pairs with a `halt_reason` string carried in the headless result envelope.
      
      | Code | Meaning              | Raised by                                                                                    |
      | ---- | -------------------- | -------------------------------------------------------------------------------------------- |
      | 0    | success              | step 7 (terminal)                                                                           |
      | 2    | input-missing / input-invalid | step 1 §1 (headless missing `architecture-doc` arg, or invalid path) → `input-missing`; non-existent file → `input-invalid` |
      | 3    | resolution-failure   | step 1 §2 (`{skills_output_folder}` does not exist or is empty → `skills-folder-missing`); step 1 §3 (forge_data_folder unconfigured → `forge-folder-unconfigured`); any stage that cannot resolve a required shared helper (atomic-write, schema ref, validate-feasibility-report) from its probe order → `resolution-failure` |
      | 4    | write-failure        | On-Activation §5 pre-flight write probe; step 1 §4 (atomic write of report skeleton failed); step 6 §4b (result-contract write failed) |
      | 5    | state-conflict       | step 1 §3 (fewer than 2 valid skills found — stack requires ≥2 → `insufficient-skills`); step 1 §1 (`previousReport` resolves to same inode as `{outputFile}` → `previous-report-collision`); step 6 §1 (report section order or schemaVersion mismatch → `schema-violation`) |
      | 6    | user-cancelled       | step 1 §1 prompt cancelled; any prompt that accepted `cancel`/`exit`/`:q`; step 6 menu cancelled |
      | 7    | inventory-unreliable | step 1 §2 (>20% subagent failures or enumerate-stack-skills warnings exceed budget); step 3 §3 (>20% API-surface subagents return malformed JSON) |
      | 8    | analysis-halted      | coverage.md §7 & integrations.md §7 — user picks [X] at an elective vacuous-analysis gate. These gates are interactive-only; in headless they auto-continue, so exit 8 never fires headlessly and `analysis-halted` never appears in the result envelope. |
      
    • health-check.md 1.5 KB
      ---
      # Note: `shared/health-check.md` resolves relative to the SKF module root
      # ({project-root}/_bmad/skf/ when installed, {project-root}/src/ during
      # development), NOT relative to this step file.
      nextStepFile: 'shared/health-check.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 7: Workflow Health Check
      
      ## STEP GOAL:
      
      Chain to the shared workflow self-improvement health check at `{nextStepFile}`. This is the terminal step of verify-stack — after the shared health check completes, the workflow is fully done.
      
      ## Rules
      
      - No user-facing reports, file writes, or result contracts in this step — those belong in step 6
      - Delegate directly to `{nextStepFile}` with no additional commentary
      - Do not attempt any other action between loading this step and executing `{nextStepFile}`
      
      ## MANDATORY SEQUENCE
      
      Attempt to load `{nextStepFile}`.
      
      - **If `{nextStepFile}` loads successfully:** Read it fully, then execute it.
      - **If `{nextStepFile}` cannot be resolved or loaded** (e.g., running against a partial installation, module root not resolvable, or the file has been removed): log exactly `health-check unavailable at {path}` (substitute the attempted resolved path) to the user-visible output and exit the workflow cleanly. This is not an error halt — the health check is an optional self-improvement hook, and the feasibility report (written in step 6) is the authoritative workflow output. Exiting cleanly keeps CI and headless runs from failing on a missing optional hook.
      
    • init.md 13.5 KB
      ---
      nextStepFile: 'coverage.md'
      feasibilitySchemaProbeOrder:
        - '{project-root}/_bmad/skf/shared/references/feasibility-report-schema.md'
        - '{project-root}/src/shared/references/feasibility-report-schema.md'
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
      outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
      # Resolve `{enumerateStackSkillsHelper}` by probing
      # `{enumerateStackSkillsProbeOrder}` in order (installed SKF module path
      # first, src/ dev-checkout fallback); first existing path wins. §2 calls
      # it for the deterministic skills inventory (cascade-resolved exports,
      # metadata-hash for change-detection, confidence-tier mapping). If neither
      # candidate exists, §2 does NOT halt — it falls through to the LLM-driven
      # subagent fan-out as graceful degradation (see §2).
      enumerateStackSkillsProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-enumerate-stack-skills.py'
        - '{project-root}/src/shared/scripts/skf-enumerate-stack-skills.py'
      ---
      
      <!-- Config: communicate in {communication_language}. Initialize the feasibility report skeleton in {document_output_language}. -->
      
      # Step 1: Initialize Verification
      
      ## STEP GOAL:
      
      Load all generated skills from the skills output folder, accept the architecture document path (required) and optional PRD/vision document path from the user, validate that all inputs exist and are readable, create the feasibility report document, and present an initialization summary before auto-proceeding.
      
      ## Rules
      
      - Focus only on loading inputs, scanning skills, and creating the report skeleton — do not perform analysis
      - Auto-proceed — halts only on validation errors
      
      ## MANDATORY SEQUENCE
      
      ### 1. Accept Input Documents
      
      "**Verify Stack — Feasibility Analysis** (read-only — never modifies your skills, architecture doc, or PRD).
      
      If you meant to *generate* skills first, type `cancel` and run `[CS] Create Skill` or `[QS] Quick Skill`. Otherwise, please provide the following:
      1. **Architecture document path** (REQUIRED) — your project's architecture doc
      2. **PRD or vision document path** (OPTIONAL) — for requirements coverage analysis
      3. **Previous feasibility report path** (OPTIONAL) — for delta comparison with a prior run (provide a backup copy)
      
      Or type `cancel` / `exit` / `:q` at any prompt to abort cleanly."
      
      Wait for user input. **GATE [default: use args]** — If `{headless_mode}` and `--architecture-doc` was provided: use that path and auto-proceed, log: "headless: using provided architecture path". If `--prd` and/or `--previous-report` were provided, consume them at the corresponding sub-validations below. If `--architecture-doc` is absent in headless: HALT (exit code 2, `halt_reason: "input-missing"`) and emit the error envelope.
      
      - If the user enters `cancel`, `exit`, `[X]`, `q`, or `:q` at any sub-prompt below: Display "Cancelled — no analysis was performed." and HALT (exit code 6, `halt_reason: "user-cancelled"`).
      
      **Validate architecture document:**
      - Confirm the file exists and is readable
      - If missing or unreadable → "Architecture document not found at `{path}`. Provide a valid path."
      - HALT (exit code 2, `halt_reason: "input-invalid"`) if the user cannot provide a valid path. In headless, emit the error envelope per SKILL.md "Result Contract (Headless)" immediately.
      
      **Validate PRD document (if provided):**
      - Confirm the file exists and is readable
      - If missing → "PRD document not found at `{path}`. Proceeding without PRD — requirements pass will be skipped."
      - Store PRD availability as `prdAvailable: true|false`
      
      **Validate previous report (if provided):**
      - Confirm the file exists and is readable
      - **Collision check:** Resolve `{outputFile}` from the activation-stored `{outputFolderPath}`, `{project_slug}`, and `{timestamp}`. Then compare both the provided path and `{outputFile}` via `(st_dev, st_ino)` tuples obtained from `stat(2)` on each path (do not rely on absolute-path string equality — symlinks, bind mounts, and case-insensitive filesystems can defeat string comparison). If `{outputFile}` does not yet exist, resolve its parent via `realpath`, stat that directory, and combine `(st_dev, parent_ino, basename)` for comparison. If the two paths resolve to the same inode, warn: "The previous report path points to the same inode as the new report. This file will be overwritten during this run. Provide a path to a backup copy, or leave empty to skip delta comparison." HALT (exit code 5, `halt_reason: "previous-report-collision"`) until resolved. In headless, emit the error envelope.
      - If missing → "Previous report not found at `{path}`. Proceeding without delta comparison."
      - Store as `previousReport: {path}` (or empty string if not provided)
      
      **Auto-discover a prior report (only when none was provided above):** If neither `--previous-report` nor an interactive previous-report path was given, glob `{outputFolderPath}` for `feasibility-report-{project_slug}-*.md`, excluding this run's `{timestamp}` file and `feasibility-report-{project_slug}-latest.md`. Every run writes a new timestamped report, so a usable prior usually already persists on disk with no manual backup. If one or more matches remain, pick the most recent by the embedded `YYYYMMDD-HHmmss` timestamp:
      - Interactive: offer "Found a prior report from `{date}` — compare against it? **[Y]** use it / paste a different path / **[skip]**". On [Y], set `previousReport` to that path; on a pasted path, use it; on `skip`, leave `previousReport` empty.
      - **GATE [default: Y]** — If `{headless_mode}`: auto-select the most recent match, set `previousReport` to it, log: "headless: delta comparison against `{path}`".
      - Excluding this run's `{timestamp}` and `-latest` files guarantees the auto-selected path is a distinct inode from `{outputFile}`, so it can never trip the collision check above.
      - If no match remains, leave `previousReport` empty — first run, no delta.
      
      ### 2. Scan Skills Folder
      
      **Pre-flight — skills folder existence:**
      - If `{skills_output_folder}` does not exist on disk: HALT (exit code 3, `halt_reason: "skills-folder-missing"`) with "**Cannot proceed.** `{skills_output_folder}` does not exist — run **[SF] Setup Forge** to initialize the forge, then generate skills with [CS] or [QS]." In headless, emit the error envelope.
      - If `{skills_output_folder}` exists but is empty (no subdirectories at all): HALT (exit code 3, `halt_reason: "skills-folder-missing"`) with "**Cannot proceed.** `{skills_output_folder}` contains 0 skills. Generate skills with [CS] Create Skill or [QS] Quick Skill, then re-run [VS]." In headless, emit the error envelope.
      
      **Resolve `{enumerateStackSkillsHelper}`** from `{enumerateStackSkillsProbeOrder}`; first existing path wins.
      
      **Primary path — deterministic enumeration via shared helper:**
      
      ```bash
      python3 {enumerateStackSkillsHelper} enumerate {skills_output_folder} --reliability
      ```
      
      The helper walks `{skills_output_folder}`, reads each `metadata.json`, applies the exports cascade (metadata → references/ → SKILL.md prose), maps `confidence_tier` (T1/T2/T1-low), captures stack-skill cycles via `composes:`, and emits structured JSON with one entry per skill plus a top-level `warnings[]` array. Cache the result as `skill_inventory` (used by §3, §4, §5, and the integrations + coverage stages).
      
      Each helper-emitted entry includes: `skill_name`, `version`, `language`, `confidence_tier`, `exports` (cascade-resolved), `source_repo`, `source_root`, plus a `metadata_hash` for change-detection across runs. The helper's `warnings[]` carries per-skill skip reasons (missing manifest, malformed JSON, non-symlink `active`, orphan-versions, schema-version violations).
      
      `--reliability` additionally attaches the inventory reliability verdict: `inventory_reliable` (bool), `unreliable_ratio` (float), `skill_count`, and `warning_count`.
      
      **Failure-budget guard:** Read `inventory_reliable` from the helper JSON. If it is `false`, HALT (exit code 7, `halt_reason: "inventory-unreliable"`) with: "Inventory scan unreliable — {warning_count}/{skill_count + warning_count} skills returned malformed metadata or skip warnings. Re-run [VS] after skills stabilize." (substitute `warning_count` and `skill_count + warning_count` from the helper JSON). In headless, emit the error envelope. The helper's threshold is chosen so a single malformed skill in a small 3-5 skill inventory does not trip the halt.
      
      **Capture mtime:** For each accepted skill in `skill_inventory`, also record `metadata.json`'s mtime via `stat` into the entry as `metadata_mtime`. Step-03 will re-verify this to detect mid-run modifications.
      
      **Fallback path — graceful degradation when the helper is unavailable:** If `{enumerateStackSkillsHelper}` has no existing candidate (e.g. partial installation), fall through to the LLM-driven subagent fan-out: launch up to **8 subagents concurrently**, each reading one resolved skill package's `metadata.json` and returning the same JSON shape the helper would emit. In this branch `inventory_reliable` is unavailable, so compute the failure-budget guard inline: when `warning_count / (skill_count + warning_count) > 0.20`, HALT (exit code 7, `halt_reason: "inventory-unreliable"`) with the same message as the primary path. In headless, emit the error envelope.
      
      ### 3. Validate Minimum Requirements
      
      **Check skill count:**
      - At least 2 valid skills must exist (a stack requires multiple libraries)
      - If fewer than 2 → "**Cannot proceed.** Only {count} skill(s) found in `{skills_output_folder}`. A stack requires at least 2 skills. Generate more skills with [CS] Create Skill or [QS] Quick Skill, then re-run [VS]."
      - HALT (exit code 5, `halt_reason: "insufficient-skills"`). In headless, emit the error envelope.
      
      **Check forge_data_folder:**
      - Verify `forge_data_folder` was resolved from config.yaml and is non-empty
      - If undefined or empty → "**Cannot proceed.** `forge_data_folder` is not configured in config.yaml. Re-run [SF] Setup Forge to initialize."
      - HALT (exit code 3, `halt_reason: "forge-folder-unconfigured"`). In headless, emit the error envelope.
      
      ### 4. Create Feasibility Report
      
      **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
      
      **Resolve `{feasibilitySchemaRef}`** from `{feasibilitySchemaProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback). If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
      
      This skill produces the feasibility report schema defined in `{feasibilitySchemaRef}`; every output conforms to that schema — `schemaVersion: "1.0"`, the verdict token set (`Verified|Plausible|Risky|Blocked`; overall `FEASIBLE|CONDITIONALLY_FEASIBLE|NOT_FEASIBLE`), the filename pattern, and the section-heading order.
      
      **Filename variables:** `project_slug` and `timestamp` were fixed at activation per SKILL.md On Activation §2; reuse them, do not re-derive. `{outputFile}` and `{outputFileLatest}` resolve from them per the stage frontmatter template — the latter a copy, not a symlink (per schema).
      
      **Load** `{reportTemplatePath}` (the customize-aware template path resolved in SKILL.md On Activation §4) and stage the initial content. Substitute the template's `Schema contract:` line `{feasibilitySchemaRef}` placeholder with the resolved schema path so every emitted report cites a path that exists on the running machine.
      
      **Populate frontmatter (per shared schema — required keys):**
      - `schemaVersion: "1.0"`
      - `reportType: feasibility`
      - `projectName: "{project_name}"`
      - `projectSlug: "{project_slug}"`
      - `generatedAt: "{ISO-8601 UTC}"`
      - `generatedBy: skf-verify-stack`
      - `overallVerdict: "CONDITIONALLY_FEASIBLE"` (provisional until step 5 finalizes)
      - `coveragePercentage: 0`
      - `pairsVerified: 0`, `pairsPlausible: 0`, `pairsRisky: 0`, `pairsBlocked: 0`
      - `recommendationCount: 0`
      - `prdAvailable: true|false` (from section 1 validation)
      
      **Populate producer-local bookkeeping keys (not part of the consumer contract):**
      - `architectureDoc`, `prdDoc` (or "none"), `previousReport` (or empty string)
      - `skillsAnalyzed: {count}`
      - `stepsCompleted: ['init']`
      
      **Atomic write:** Pipe the staged content through `python3 {atomicWriteHelper} write --target {outputFile}` and then again with `--target {outputFileLatest}`. Both writes use the same staged content through the atomic helper — a plain `rm`+rewrite risks a partial write corrupting the report, and the `-latest` file is a copy, not a symlink (per the shared schema).
      
      On any non-zero exit from either write: HALT (exit code 4, `halt_reason: "write-failed"`) and emit the error envelope per SKILL.md "Result Contract (Headless)" with `report_path: null`, `report_latest_path: null`, `overall_verdict: null`.
      
      ### 5. Display Initialization Summary
      
      "**Stack Verification Initialized**
      
      | Field | Value |
      |-------|-------|
      | **Skills Loaded** | {count} |
      | **Architecture Doc** | {architecture_doc} |
      | **PRD Document** | {prd_doc or 'Not provided — requirements pass will be skipped'} |
      | **Previous Report** | {previousReport or 'Not provided — no delta comparison'} |
      
      **Skill Inventory:**
      
      | Skill | Language | Tier | Exports |
      |-------|----------|------|---------|
      | {skill_name} | {language} | {confidence_tier} | {exports_documented} |
      
      **Proceeding to coverage analysis...**"
      
      ### 6. Auto-Proceed to Next Step
      
      Load, read the full file and then execute `{nextStepFile}`.
      
      
    • integration-verification-rules.md 5.4 KB
      # Integration Verification Rules
      
      ## Purpose
      
      Rules for cross-referencing API surfaces between two skills to determine integration feasibility.
      
      ---
      
      ## Verdict Definitions
      
      Token set is defined canonically in the SKF shared feasibility report schema (`_bmad/skf/shared/references/feasibility-report-schema.md` in installed mode; `src/shared/references/feasibility-report-schema.md` in a dev checkout) — the table below restates the same set with this skill's evidence obligations. Tokens are case-sensitive (`Verified`, `Plausible`, `Risky`, `Blocked`); emitting any other token is a schema violation.
      
      | Verdict       | Meaning                                                                                        | Required Evidence                                                                                                                                                                                                                                                   |
      |---------------|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
      | **Verified**  | APIs demonstrably connect and docs cross-reference each other                                  | Check 1 (language) passes with declared evidence; Check 3 (types) passes from cited `exports` signatures; **Check 4 (docs cross-reference) passes with a literal substring/name citation** — without Check 4 evidence, cap at `Plausible`. Check 2 is best-effort only and cannot by itself promote to `Verified`. |
      | **Plausible** | Checks pass but rely on inferred or indirect evidence                                          | Language + type checks pass; Check 2 uses inferred `protocols_inferred`/`data_formats_inferred` (prose scan); Check 4 is weak or missing (no literal cross-reference). This is the mandatory cap whenever Check 4 does not surface a literal citation.             |
      | **Risky**     | Type mismatch, protocol gap, or language boundary requiring a bridge                           | A clear gap exists (e.g., TypeScript↔Rust FFI needed) but a workaround is architecturally feasible — cite a named workaround in the recommendation                                                                                                                 |
      | **Blocked**   | Fundamental incompatibility — no feasible integration path even with a bridge or adapter layer | The two libraries cannot exchange data in any documented way; requires replacing one of the libraries                                                                                                                                                              |
      
      **Promotion rule:** `Verified` requires Check 4 evidence. If Checks 1 and 3 pass but Check 4 fails (no literal substring/name citation from either skill's SKILL.md), the verdict is capped at `Plausible`. This rule is enforced by step 3 §4 and is the producer obligation declared in the shared schema.
      
      ---
      
      ## Cross-Reference Protocol
      
      For each integration pair (Library A ↔ Library B):
      
      ### 1. Language Boundary Check
      
      - Same language on both sides → no boundary, direct API calls. Different languages → the integration needs a bridge (FFI, IPC, or a network protocol; the mechanism follows from the pair — e.g. C/C++ exposes FFI most languages can bind).
      - The load-bearing nudge: **check whether a bridge library already exists in the stack** before assuming one must be built (e.g., Tauri provides JS↔Rust IPC).
      
      ### 2. Protocol Compatibility Check
      
      - Matching transports are compatible modulo format alignment: both in-process → direct calls; both HTTP/REST → compatible if endpoints match; both WebSocket → check message-format compatibility; both shared-filesystem → async, check format.
      - The load-bearing nudge: **two embedded databases may conflict on lock files — check for multi-writer support** before treating them as compatible.
      
      ### 3. Type Compatibility Check
      
      - Extract the primary data types each library produces/consumes from the skill's export list
      - Check: does Library A export a type that Library B accepts as input?
      - Common patterns: JSON serialization (universal bridge), binary formats (check codec), shared schemas (strong compatibility)
      
      ### 4. Documentation Cross-Reference (required for `Verified`)
      
      - Search Skill A's SKILL.md for a literal substring/name citation of Library B
      - Search Skill B's SKILL.md for the reciprocal citation
      - Accept literal names or aliases declared in that skill's metadata; a paraphrase or fuzzy match does not satisfy Check 4
      - A pass requires at least one literal citation in at least one direction; record the exact substring and location in the evidence block
      - If neither skill literally cites the other, Check 4 fails and the per-pair verdict caps at `Plausible` (not `Verified`)
      
      ---
      
      ## Verdict Evidence Format
      
      Each verdict includes:
      
      ```
      **{Library A} ↔ {Library B}: {VERDICT}**
      
      Evidence:
      - A exports: `{function_name}({params}) → {return_type}` [from skill: {skill_name}]
      - B accepts: `{function_name}({params})` [from skill: {skill_name}]
      - Compatibility: {explanation}
      - Language boundary: {same | bridge required via {mechanism}}
      
      {If RISKY or BLOCKED:}
      Recommendation: {actionable next step}
      ```
      
    • integrations.md 13.6 KB
      ---
      nextStepFile: 'requirements.md'
      integrationRulesData: '{integrationRulesPath}'
      coveragePatternsData: '{coveragePatternsPath}'
      feasibilitySchemaProbeOrder:
        - '{project-root}/_bmad/skf/shared/references/feasibility-report-schema.md'
        - '{project-root}/src/shared/references/feasibility-report-schema.md'
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      cycleFinderProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-find-cycles.py'
        - '{project-root}/src/shared/scripts/skf-find-cycles.py'
      outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
      outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
      ---
      
      <!-- Config: communicate in {communication_language}. Append the Integration Verdicts section to the report in {document_output_language}. -->
      
      # Step 3: Integration Verification
      
      ## STEP GOAL:
      
      Cross-reference API surfaces between library pairs that the architecture document claims work together. For each integration pair, verify language compatibility, protocol alignment, type compatibility, and documentation cross-references. Produce an evidence-backed verdict for each integration.
      
      ## Rules
      
      - Focus only on integration pair verification using skill API surfaces
      - Do not evaluate requirements coverage (Step 04) or parse Mermaid diagrams
      - Every verdict must include evidence citations from the skills
      
      ## MANDATORY SEQUENCE
      
      **Resolve `{feasibilitySchemaRef}`** from `{feasibilitySchemaProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback) — used by the verdict-cap references and the append step below.
      
      ### 1. Load Integration Verification Rules
      
      Load `{integrationRulesData}` for the cross-reference verification protocol.
      
      Extract: verification checks (language boundary, protocol compatibility, type compatibility, documentation cross-reference), verdict criteria, and evidence requirements.
      
      ### 2. Extract Integration Claims
      
      **Source preference:** If a stack skill assembled by `skf-create-stack-skill` is present in the inventory and its manifest (`bmad-skill-manifest.yaml` or its `metadata.json`) declares `integration_patterns`, use THAT as the primary source of integration claims. Record `source: stack manifest` on each resulting pair. Fall back to prose co-mention (below) only when no such manifest is available, and record `source: prose co-mention` on those pairs.
      
      Parse the architecture document for statements describing two or more technologies working together.
      
      **Detection method — prose-based co-mention analysis (fallback only):**
      - Identify sentences or paragraphs where two or more technology names appear together
      - Look for integration verbs: "connects to", "communicates with", "wraps", "extends", "consumes", "produces", "bridges", "integrates with", "sits between"
      - Look for data flow descriptions: "{A} sends data to {B}", "{A} results are consumed by {B}"
      - Look for layer boundary descriptions: "{A} at the API layer connects to {B} at the data layer"
      
      **Mermaid Diagram Handling:** See `{coveragePatternsData}` → "Mermaid Diagram Handling" for the canonical rule (single source of truth). Summary: do not parse Mermaid diagram syntax for co-mention detection; use only prose text.
      
      **Build integration pairs list:**
      - Each pair: `{library_a, library_b, architectural_context}`
      - `architectural_context`: the quoted text or paraphrased description of their relationship
      
      **Filter:** Only include pairs where BOTH libraries have a corresponding skill (Covered in Step 02). Skip pairs involving Missing skills — they cannot be verified.
      
      ### 3. Load Skill API Surfaces
      
      <!-- Subagent delegation: read SKILL.md files in parallel, return compact JSON -->
      
      For each library in an integration pair, delegate SKILL.md reading to a parallel subagent. Launch up to **8 subagents concurrently** (batch if needed — same 8-way cap as step 1 §2; keeps aggregate token window manageable while still parallelizing typical stack sizes). Each subagent receives one skill's SKILL.md path and:
      1. Reads the SKILL.md file
      2. Extracts the API surface
      3. Returns only this compact JSON — no prose, no extra commentary:
      
      ```json
      {
        "skill_name": "...",
        "language": "...",
        "exports": ["functionName(params): ReturnType", "..."],
        "protocols_inferred": ["HTTP", "gRPC", "WebSocket", "message queue", "file I/O", "IPC"],
        "data_formats_inferred": ["JSON", "protobuf", "CSV", "binary", "streaming"]
      }
      ```
      
      **Extraction rules for subagents:**
      - `skill_name`, `language`: mirror the skill's metadata fields
      - `exports`: exported functions with signatures, exported types/interfaces/classes (extracted from SKILL.md prose)
      - `protocols_inferred`: best-effort prose scan — protocol tokens mentioned in SKILL.md descriptions/examples; not a declared field in `metadata.json`
      - `data_formats_inferred`: best-effort prose scan — format tokens mentioned in SKILL.md descriptions/examples; not a declared field in `metadata.json`
      - If a field has no matches, return an empty array `[]`
      
      **These fields are inferred, not declared.** `protocols` and `data_formats` do not exist in any skill's `metadata.json` — treat them as weak evidence from prose scanning only. When either list is used to justify compatibility in Check 2, cap the per-pair verdict at `Plausible` (see the schema's producer obligations — `{feasibilitySchemaRef}`).
      
      **Schema validation (parent):** Each subagent response must contain the required keys (`skill_name`, `language`, `exports`). Reject responses missing required keys and exclude that skill from pair evaluation; if more than **20%** (same failure-budget threshold as step 1 §2; see the justification there) of subagent calls return malformed JSON, HALT (exit code 7, `halt_reason: "inventory-unreliable"`) with "API-surface extraction unreliable — more than 20% of subagent reads returned malformed JSON. Re-run [VS] after skills stabilize." In headless, emit the error envelope.
      
      **Parent collects all subagent JSON summaries.** Do not load full SKILL.md content into parent context.
      
      **From metadata.json (read in parent — lightweight), also extract:**
      - `language` — primary programming language (authoritative — overrides subagent `language` if they disagree)
      - `exports` — export names array (populated for individual skills; empty for stack skills)
      - `stats.exports_documented` — export count
      - `confidence_tier` — extraction confidence level
      
      **mtime re-verification:** Re-stat each `metadata.json` and compare against the mtime captured in step 1. If any mtime moved during the run, abort any pair involving that skill with rationale "skill modified mid-run — re-run [VS]".
      
      Store collected API surface summaries for cross-referencing.
      
      ### 4. Cross-Reference Each Integration Pair
      
      For each integration pair `{library_a, library_b}`, run the four-check protocol and assign the per-pair verdict per `{integrationRulesData}` (loaded in §1): the Cross-Reference Protocol defines Check 1 (language boundary), Check 2 (protocol compatibility), Check 3 (type compatibility), and Check 4 (documentation cross-reference, required for `Verified`); the Verdict Definitions table and promotion rule define the `Verified` / `Plausible` / `Risky` / `Blocked` thresholds and the cap-at-`Plausible` rule that applies whenever Check 4 surfaces no literal citation. Do not restate those mechanics here.
      
      **Step-specific input for Check 2:** Check 2 draws only on the `protocols_inferred` / `data_formats_inferred` lists surfaced by the §3 subagent prose scan. A shared or complementary token (e.g., "HTTP client" ↔ "HTTP server") reads as inferred compatibility; no token on either side, or conflicting tokens with no adapter, flags a risk. Any pair whose compatibility rests on this inferred evidence caps at `Plausible` per §3.
      
      **Each verdict includes:**
      - Which checks passed and which flagged
      - Evidence citations: specific exports, types, or literal substrings from the skills
      - `source: stack manifest` or `source: prose co-mention` tag (per section 2)
      - For `Verified`: the exact Check 4 literal citation (e.g., `"see also: {lib_b}"` quoted from Skill A's SKILL.md, line N)
      - **Tier annotation:** For each contributing skill, append `(evidence from Tier {n} skill)` citing that skill's `confidence_tier` (e.g., `(evidence from Tier 1 skill)`). This lets reviewers weigh evidence strength by extraction confidence.
      
      **Cycle detection (after all pairs evaluated):** Deciding the edge set is a Check-4 judgment and stays here; the traversal is deterministic and is delegated to the shared helper (the in-prose DFS misses real multi-hop cycles or invents spurious ones as the pair count grows). 
      
      1. **Build the directed pair graph in the prompt:** an edge `A → B` exists when skill A literally cites skill B via Check 4. Serialize the edges as JSON: `{"edges": [["A", "B"], ["B", "C"], ...]}` (each `[from, to]` pair is one Check-4 citation direction).
      2. **Resolve `{cycleFinderHelper}`** from `{cycleFinderProbeOrder}`; first existing path wins. Enumerate cycles deterministically (run `uv run {cycleFinderHelper} find --help` for the contract):
      
         ```bash
         uv run {cycleFinderHelper} find --edges -
         ```
         piping the edges JSON on stdin (use a temp file under `{forge_data_folder}/` if stdin piping is unavailable). The script emits:
         ```json
         {"cycles": [["A", "B", "C", "A"], ...], "cycle_count": N}
         ```
         Each `cycles[]` entry is a closed node path (first node repeated at the end); every simple directed cycle appears exactly once, de-duplicated across rotations.
      
         **Graceful degradation:** if no `{cycleFinderProbeOrder}` candidate exists (e.g. `uv` unavailable on claude.ai web), run the equivalent DFS (visited set + recursion stack) directly per the `--help` contract and proceed with the same cycle set.
      3. **For each cycle** in `cycles[]`, append a synthetic row to the verdict table with verdict `Risky` and rationale "circular integration dependency detected: `{A → B → C → A}`" (render the arrow chain from the cycle's node path). Do not otherwise modify the individual pair verdicts.
      
      ### 5. Display Integration Results
      
      "**Pass 2: Integration Verification**
      
      | Library A | Library B | Context | Source | Verdict | Evidence |
      |-----------|-----------|---------|--------|---------|----------|
      | {lib_a} | {lib_b} | {brief context} | {stack manifest / prose co-mention} | {Verified/Plausible/Risky/Blocked} | {key evidence, including Check 4 literal citation if Verified} |
      
      **Summary:** {verified_count} Verified, {plausible_count} Plausible, {risky_count} Risky, {blocked_count} Blocked
      
      {IF zero integration pairs found:}
      **No integration claims detected in the architecture document prose.** Ensure your architecture document describes relationships between technologies in text form (not exclusively in Mermaid diagrams). Coverage-only analysis was performed.
      
      {IF any Risky:}
      **Risky Integrations — Recommendations:**
      {For each risky pair:}
      - `{lib_a}` ↔ `{lib_b}`: {specific concern}. **Recommendation:** {prescriptive action}
      
      {IF any Blocked:}
      **Blocked Integrations — Action Required:**
      {For each blocked pair:}
      - `{lib_a}` ↔ `{lib_b}`: {fundamental incompatibility}. **Recommendation:** {prescriptive action}"
      
      ### 6. Append to Report
      
      **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
      
      Write the **Integration Verdicts** section to `{outputFile}` (heading is fixed — consumers grep for `## Integration Verdicts`; the table header is the canonical `| lib_a | lib_b | verdict | rationale |` per `{feasibilitySchemaRef}`, and consumers parse that exact header, so a different one breaks them; the skill-local display table with the extra Context/Source/Evidence columns can be rendered beneath it for human readers):
      - Emit the canonical `| lib_a | lib_b | verdict | rationale |` table first (verdict tokens are exactly one of `Verified`, `Plausible`, `Risky`, `Blocked`, case-sensitive — any other token is a schema violation consumers reject)
      - Include the extended table with Context, Source, and Evidence columns below it
      - Include recommendations for Risky and Blocked pairs (each Blocked recommendation cites a named candidate per step 5 H6, or the explicit no-candidate notice)
      - Update frontmatter: append `'integrations'` to `stepsCompleted`; set `pairsVerified`, `pairsPlausible`, `pairsRisky`, `pairsBlocked` counts
      - Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
      
      ### 7. Auto-Proceed to Next Step
      
      **Early halt guard:** If ALL integration pairs are Blocked, present: "**All integrations are Blocked** — fundamental incompatibilities detected across all library pairs. Remaining analysis will produce limited value. **[X] Halt workflow (recommended)** | **[C] Continue anyway**" — wait for user input.
      
      **GATE [default: C]** — Interactive-only guard. If `{headless_mode}`: auto-proceed with [C] Continue, log: "headless: continuing past all-Blocked integration gate". Headless never takes [X], so step 6 still emits the result contract — any Blocked pair resolves the run to `NOT_FEASIBLE` in synthesize.
      
      - If X: halt with: "**Workflow halted — all integrations blocked.** Integration Verdicts saved to `{outputFile}`. Run **[VS]** after applying architectural changes. **Blocked integrations:** {list each blocked pair with reason}." HALT (exit code 8, `halt_reason: "analysis-halted"`).
      - If C: continue.
      
      {IF NOT halted (user selected C, or early halt guard did not trigger):}
      
      "**Proceeding to requirements verification...**"
      
      Load, read the full file and then execute `{nextStepFile}`.
      
      
    • report.md 10.1 KB
      ---
      # {outputFile} and {outputFileLatest} resolve from the activation-stored
      # {project_slug}, {timestamp}, and {outputFolderPath} variables (set in
      # SKILL.md On Activation §2 + §4) — same template as init.md frontmatter
      # so every stage sees the same path.
      outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
      outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
      feasibilitySchemaProbeOrder:
        - '{project-root}/_bmad/skf/shared/references/feasibility-report-schema.md'
        - '{project-root}/src/shared/references/feasibility-report-schema.md'
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      validateFeasibilityReportProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-validate-feasibility-report.py'
        - '{project-root}/src/shared/scripts/skf-validate-feasibility-report.py'
      nextStepFile: 'health-check.md'
      ---
      
      <!-- Config: communicate in {communication_language}. Render the user-facing summary in {document_output_language}. -->
      
      # Step 6: Present Report
      
      ## STEP GOAL:
      
      Present the complete feasibility report to the user. Display the overall verdict prominently, walk through key findings from each analysis pass, present actionable next steps based on the verdict, and offer the user options to review the full report or exit.
      
      ## Rules
      
      - Focus only on presenting the completed report — no new analysis or changes to verdicts
      - Chains to the local health-check step via `{nextStepFile}` after completion — the user-facing report is not the terminal step
      
      ## MANDATORY SEQUENCE
      
      ### 1. Load Complete Report
      
      Read the entire `{outputFile}` to have all data available for presentation.
      
      **Resolve `{feasibilitySchemaRef}`** from `{feasibilitySchemaProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback).
      
      **Validate report structure and schema version (deterministic gate).** Resolve `{validateFeasibilityReportHelper}` from `{validateFeasibilityReportProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback). Then run:
      
      ```bash
      python3 {validateFeasibilityReportHelper} {outputFile}
      ```
      
      The script (see `--help`) deterministically confirms the five required body sections — `## Executive Summary`, `## Coverage Analysis`, `## Integration Verdicts`, `## Recommendations`, `## Evidence Sources` — are all present and in canonical order per `{feasibilitySchemaRef}`, **and** that frontmatter `schemaVersion == "1.0"`. It emits a JSON verdict on stdout (`headingsOk`, `missingHeadings`, `orderViolations`, `schemaVersionOk`, `schemaVersionFound`, `violation`) and exits `0` when valid, `1` on a schema violation, `2` on an IO/parse error.
      
      **Graceful degradation:** if no `{validateFeasibilityReportProbeOrder}` candidate exists (e.g. partial installation, or `python3` unavailable), perform the equivalent structural check inline — confirm the five headings above are all present and in that exact order, and that frontmatter `schemaVersion` is `"1.0"` — and apply the same halt semantics below. The report is already on disk from steps 1-5, so a missing validator degrades to the inline check rather than blocking presentation.
      
      On any non-zero exit (or an inline check that fails), HALT (exit code 5, `halt_reason: "schema-violation"`) — do not display partial results. Report the specific violation from the JSON:
      
      - a missing or out-of-order section (`missingHeadings` / `orderViolations`); or
      - a schemaVersion mismatch — "Report frontmatter schemaVersion `{schemaVersionFound}` does not match producer schema `1.0` — report was corrupted between steps. Re-run [VS]." (Producer never proceeds past a schema mismatch.)
      
      In headless, emit the error envelope per SKILL.md "Result Contract (Headless)" with `report_path: "{outputFile}"`, `overall_verdict: null`.
      
      With the deterministic gate passed (sections present + in order, `schemaVersion == "1.0"`), **extract metrics from `{outputFile}` frontmatter** (per shared schema in `{feasibilitySchemaRef}`): `skillsAnalyzed`, `coveragePercentage`, `pairsVerified` (as `verified_count`), `pairsPlausible` (as `plausible_count`), `pairsRisky` (as `risky_count`), `pairsBlocked` (as `blocked_count`), `requirementsFulfilled` (as `fulfilled_count`), `requirementsPartial` (as `partial_count`), `requirementsNotAddressed` (as `not_addressed_count`), `requirementsPass`, `overallVerdict`, and `recommendationCount`. Use these mapped display names in the summary table and next steps below.
      
      ### 2. Present Summary
      
      "**Verify Stack — Feasibility Report**
      
      ---
      
      **Overall Verdict: {FEASIBLE / CONDITIONALLY_FEASIBLE / NOT_FEASIBLE}** (tokens are case-sensitive and use underscores per `{feasibilitySchemaRef}`; for user-facing prose you may render them as "Feasible", "Conditionally feasible", or "Not feasible")
      
      | Metric | Value |
      |--------|-------|
      | **Skills Analyzed** | {skillsAnalyzed} |
      | **Coverage** | {coveragePercentage}% |
      | **Integrations Verified** | {verified_count} |
      | **Integrations Plausible** | {plausible_count} |
      | **Integrations Risky** | {risky_count} |
      | **Integrations Blocked** | {blocked_count} |
      | **Requirements Fulfilled** | {fulfilled_count or 'N/A — no PRD'} |
      | **Requirements Partially Fulfilled** | {partial_count or 'N/A — no PRD'} |
      | **Requirements Not Addressed** | {not_addressed_count or 'N/A — no PRD'} |
      
      {IF deltaImproved is not null (delta from previous run exists):}
      **Delta from Previous Run:**
      - Improved: {deltaImproved} items
      - Regressed: {deltaRegressed} items
      - New: {deltaNew} items
      - Unchanged: {deltaUnchanged} items
      
      ---"
      
      ### 3. Present Detailed Findings
      
      Walk through the highlights — coverage gaps, risky/blocked integrations, and partial/unaddressed requirements (when a PRD pass ran). Cite specific items by name; cap at the top ~5 per category to keep the summary scannable. The full detail is in `{outputFile}` for the user to inspect via the [R] Review menu (§5).
      
      ### 4. Present Next Steps
      
      Step 05 already wrote a **Suggested next workflow** block (keyed on the case-sensitive `overallVerdict` token) at the end of `## Recommendations`. Surface that block from the §1 load rather than re-deriving it, prefixed with the one-line verdict-specific framing:
      
      - **`FEASIBLE`:** "**Your stack is verified.** All technologies are covered, integrations are compatible, and requirements are all fulfilled (or requirements pass was skipped)."
      - **`CONDITIONALLY_FEASIBLE`:** "**Your stack is conditionally feasible.** There are {recommendationCount} items to address before proceeding." — then list the specific recommendations from the report's `## Recommendations` section.
      - **`NOT_FEASIBLE`:** "**Critical blockers must be resolved.** The stack cannot support the architecture as described." — then list the blocked-integration and missing-skill recommendations from the report's `## Recommendations` section.
      
      ### 4b. Result Contract
      
      **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
      
      Write the result contract per `shared/references/output-contract-schema.md` (this path resolves relative to the SKF module root — `{project-root}/_bmad/skf/` when installed, `{project-root}/src/` during development — not relative to this step file): the per-run record at `{forge_data_folder}/verify-stack-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_data_folder}/verify-stack-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include the feasibility report path (both `{outputFile}` and `{outputFileLatest}`) in `outputs`; include `overallVerdict` (`FEASIBLE` / `CONDITIONALLY_FEASIBLE` / `NOT_FEASIBLE`), `coveragePercentage`, and `recommendationCount` in `summary` — use the case-sensitive schema tokens.
      
      Write both JSON files through `python3 {atomicWriteHelper} write --target ...` to avoid partial-write corruption. On any non-zero exit: HALT (exit code 4, `halt_reason: "write-failed"`) and emit the error envelope.
      
      When `{headless_mode}` is true, also emit the single-line envelope on **stdout** before chaining to step 7 (matches the SKILL.md "Result Contract (Headless)" shape):
      
      ```
      SKF_VERIFY_STACK_RESULT_JSON: {"status":"success","report_path":"{outputFile}","report_latest_path":"{outputFileLatest}","overall_verdict":"{overallVerdict}","coverage_percentage":{coveragePercentage},"recommendation_count":{recommendationCount},"exit_code":0,"halt_reason":null}
      ```
      
      `{overallVerdict}` uses the schema tokens (`FEASIBLE` / `CONDITIONALLY_FEASIBLE` / `NOT_FEASIBLE`).
      
      **Result-contract ordering:** The result contract is written exactly once on the first entry to step 6 (the `[X] Exit verification` path). Re-walks of the report via the `[R] Review full report` menu option do not regenerate it — the contract captures the run, not the presentation loop. If the user selects `[R]` repeatedly before exiting, the single on-disk contract written on first entry remains authoritative.
      
      ### 5. Present Menu
      
      Display: "**[R] Review full report** | **[X] Exit verification**"
      
      #### Menu Handling Logic:
      
      - **IF R:** Walk through the report section by section, presenting each section's content from {outputFile} in a readable format. After completing the walkthrough, redisplay the menu. (Note: the R walkthrough loop terminates only when the user selects X.)
      - **IF X:** "**Feasibility report saved to:** `{outputFile}`
      
      Re-run **[VS] Verify Stack** anytime after making changes to your skills or architecture document.
      
      **Verification workflow complete.**"
      
        If `{workflow.on_complete}` is non-empty, execute it now (e.g. route the verdict onward or trigger a downstream step); in headless, log the action. Then load, read the full file, and execute `{nextStepFile}` — the health-check step is the true terminal step of this workflow.
      
      #### EXECUTION RULES:
      
      - **GATE [default: X]** — If `{headless_mode}`: auto-proceed with [X] Exit verification, log: "headless: auto-exit past report menu"
      - R may be selected multiple times — always walk through the full report
      
      
      
    • requirements.md 6 KB
      ---
      nextStepFile: 'synthesize.md'
      feasibilitySchemaProbeOrder:
        - '{project-root}/_bmad/skf/shared/references/feasibility-report-schema.md'
        - '{project-root}/src/shared/references/feasibility-report-schema.md'
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
      outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
      ---
      
      <!-- Config: communicate in {communication_language}. Append the Requirements Coverage section to the report in {document_output_language}. -->
      
      # Step 4: Requirements Coverage
      
      ## STEP GOAL:
      
      If a PRD or vision document was provided in Step 01, verify that the combined capabilities of the generated skills address each stated requirement. If no PRD was provided, skip this pass and auto-proceed. Produce a requirements coverage table with Fulfilled, Partially Fulfilled, or Not Addressed verdicts.
      
      ## Rules
      
      - Focus only on requirements-to-skills coverage assessment
      - Do not re-analyze integrations (Step 03) or synthesize verdicts (Step 05)
      - If no PRD was provided, skip immediately with a clear message
      
      ## MANDATORY SEQUENCE
      
      ### 1. Check PRD Availability
      
      **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
      
      **Read `prdAvailable` from `{outputFile}` frontmatter (set in Step 01). If `prdAvailable` is false (no PRD/vision document was provided):**
      
      "**Pass 3: Requirements Coverage — Skipped**
      
      No PRD or vision document was provided. Requirements coverage analysis requires a document describing project capabilities and constraints.
      
      To include this pass, re-run **[VS]** with a PRD or vision document path.
      
      **Proceeding to synthesis...**"
      
      Update `{outputFile}` frontmatter: append `'requirements'` to `stepsCompleted`; set `requirementsPass: "skipped"`. Pipe the updated content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`.
      
      Load, read the full file and then execute `{nextStepFile}`. The no-PRD path ends here — sections 2-6 are the PRD-present branch and do not run.
      
      **If PRD/vision document was provided:** Continue to section 2.
      
      ### 2. Extract Requirements
      
      Parse the PRD/vision document for capability requirements.
      
      **Look for:**
      - **Feature descriptions** — explicit capabilities the product must have
      - **Technical requirements** — performance targets, scalability needs, platform support
      - **Non-functional requirements** — offline-first, real-time sync, multi-language support, accessibility, security constraints
      - **Integration requirements** — third-party service dependencies, API contracts
      - **Infrastructure requirements** — deployment targets, CI/CD needs, monitoring
      
      **Build a requirements list** with each entry containing:
      - `requirement_id` — sequential identifier (R1, R2, R3...)
      - `requirement_text` — the stated requirement
      - `category` — feature, technical, non-functional, integration, or infrastructure
      - `source_section` — the PRD section where it was found
      
      ### 3. Assess Stack Coverage
      
      For each requirement, evaluate whether the combined capabilities of the generated skills address it.
      
      **Assessment method:**
      - Read each skill's SKILL.md exports, description, and capabilities sections
      - Check if skill exports provide functions, types, or patterns relevant to the requirement
      - Consider combinations of multiple skills that together address a requirement
      - For non-functional requirements, check if skills document relevant configuration or patterns
      
      **Assign verdict per requirement:**
      - **Fulfilled** — one or more skills clearly provide the needed capability, with specific exports or patterns identified
      - **Partially Fulfilled** — skills provide related capability but gaps remain (specify what is covered and what is not)
      - **Not Addressed** — no skill in the stack provides capability relevant to this requirement
      
      **Each verdict includes:**
      - Which skills contribute (if any)
      - Specific exports or capabilities from those skills that are relevant
      - For Partially Fulfilled: what gap remains
      
      ### 4. Display Requirements Results
      
      "**Pass 3: Requirements Coverage**
      
      | ID | Requirement | Category | Verdict | Contributing Skills |
      |----|-------------|----------|---------|-------------------|
      | {id} | {requirement_text} | {category} | {Fulfilled/Partially Fulfilled/Not Addressed} | {skill_names or '—'} |
      
      **Coverage: {fulfilled_count} Fulfilled, {partial_count} Partially Fulfilled, {not_addressed_count} Not Addressed**
      
      {IF any Not Addressed:}
      **Unaddressed Requirements — Recommendations:**
      {For each not addressed requirement:}
      - **{id}:** {requirement_text} → Evaluate `{category}` libraries that provide this capability, generate a skill with **[CS]** or **[QS]**, then re-run **[VS]**
      
      {IF any Partially Fulfilled:}
      **Partial Coverage — Details:**
      {For each partially fulfilled requirement:}
      - **{id}:** Covered by `{skill_names}` — **Gap:** {what remains unaddressed}"
      
      ### 5. Append to Report
      
      Write the Requirements Coverage content under the `## Recommendations` section (or as a clearly-titled subsection preceding Recommendations — the shared schema's fixed top-level headings are Executive Summary, Coverage Analysis, Integration Verdicts, Recommendations, Evidence Sources; requirements detail lives under Recommendations):
      - Include the full requirements coverage table
      - Include recommendations for Not Addressed and Partially Fulfilled items
      - Update frontmatter: append `'requirements'` to `stepsCompleted`
      - Set `requirementsPass: "completed"`
      - Set `requirementsFulfilled`, `requirementsPartial`, `requirementsNotAddressed` counts
      - Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
      
      ### 6. Auto-Proceed to Next Step
      
      "**Proceeding to synthesis...**"
      
      Load, read the full file and then execute `{nextStepFile}`.
      
      
    • synthesize.md 14.3 KB
      ---
      nextStepFile: 'report.md'
      verdictRollupScript: 'scripts/skf-verdict-rollup.py'
      reportDeltaScript: 'scripts/skf-report-delta.py'
      feasibilitySchemaProbeOrder:
        - '{project-root}/_bmad/skf/shared/references/feasibility-report-schema.md'
        - '{project-root}/src/shared/references/feasibility-report-schema.md'
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
      outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
      ---
      
      <!-- Config: communicate in {communication_language}. Append the Executive Summary, synthesized verdict, and Recommendations to the report in {document_output_language}. -->
      
      # Step 5: Synthesize Verdict
      
      ## STEP GOAL:
      
      Calculate the overall feasibility verdict based on all three analysis passes, generate prescriptive recommendations for every non-verified finding, check for a previous feasibility report to produce a delta, and compile the synthesis section of the report.
      
      ## Rules
      
      - Focus only on synthesizing findings from Steps 02-04 into a verdict — do not discover new findings
      - Recommendations must name specific tools, libraries, or actions
      
      ## MANDATORY SEQUENCE
      
      ### 1. Calculate Overall Verdict
      
      **The verdict token is deterministic — do not walk the ladder in prose.** All three passes have already persisted their counts (`coveragePercentage`, `pairsBlocked`/`pairsRisky`/`pairsPlausible`/`pairsVerified`, `requirementsPass` + `requirementsNotAddressed`/`requirementsPartial` in `{outputFile}` frontmatter; the Missing technology count in the Coverage Analysis table). Rolling those already-decided counts up into one token has a single correct answer per input, so delegate it. Count Missing rows directly from the Coverage table — half-up rounding can leave `coveragePercentage == 100` with one technology still Missing — assemble the counts, and run:
      
      ```bash
      echo '<counts JSON>' | uv run {verdictRollupScript} --stdin
      ```
      
      Input keys: `coveragePercentage`, `missingCount`, `pairsBlocked`, `pairsRisky`, `pairsPlausible`, `pairsVerified`; plus, only when the requirements pass ran (`requirementsPass == "completed"`), `requirementsEvaluated: true` with `requirementsNotAddressed`/`requirementsPartial`; plus `continuedPastZeroState: true` if the user pressed `[C] Continue anyway` past a step-2 zero-state gate (all-Replaced or 0%-coverage). The script (run `uv run {verdictRollupScript} --help` for the contract) returns `overallVerdict` (one of `FEASIBLE`/`CONDITIONALLY_FEASIBLE`/`NOT_FEASIBLE`), `matchedConditions` (the condition codes that fired), and `zeroPairsGuardFired`. If `uv` is unavailable (e.g. claude.ai web), apply the ladder below inline.
      
      **The ladder it applies (documented so the rationale can cite it — the script is the executor; evaluate top-to-bottom, first match wins):**
      - `coveragePercentage == 0` → `NOT_FEASIBLE` (short-circuit: no live coverage, analysis vacuous).
      - Any Blocked integration → `NOT_FEASIBLE` (fundamental architectural incompatibility).
      - Any Missing technology, any Risky integration, or — when requirements ran — any Not Addressed / Partially Fulfilled requirement → `CONDITIONALLY_FEASIBLE`.
      - Otherwise → `FEASIBLE`, but any pair capped at `Plausible` (including Check-4-missing caps) downgrades to `CONDITIONALLY_FEASIBLE`.
      - Post-verdict guard: when all four integration counts are 0 and the user continued past a step-2 zero-state gate, `zeroPairsGuardFired` is true — a `FEASIBLE` verdict is overridden to `CONDITIONALLY_FEASIBLE`.
      
      Technologies marked **Replaced** in Step 02 are intentionally being removed and are already excluded from `missingCount` and the coverage denominator — they never trigger `CONDITIONALLY_FEASIBLE` or a [CS]/[QS] recommendation.
      
      **Write the rationale** from `matchedConditions`, naming the specific findings behind each code:
      - `zero-coverage` → "no coverage — analysis vacuous: zero generated skills match the architecture's referenced technologies, so integration and requirements verdicts cannot produce meaningful evidence." Then proceed directly to section 2 to generate recommendations for the Missing and/or Replaced technologies surfaced by Step 02.
      - `blocked-integration` → a blocked integration is a fundamental architectural incompatibility; name each Blocked pair and note any co-occurring `missing-coverage`/`risky-integration` codes so the user sees the full set of problems.
      - `missing-coverage` / `risky-integration` / `requirements-not-addressed` / `requirements-partial` / `plausible-cap` → the stack can work but has gaps, risks, or unverified assumptions that must be addressed; name the specific items behind each code.
      - No codes (`FEASIBLE`) → {IF requirements pass completed:} the stack can support the architecture as described — all requirements fully fulfilled, every integration pair has a literal cross-reference. {IF requirements pass was skipped:} the stack can support the architecture as described — requirements were not evaluated (no PRD provided).
      - `zero-integration-pairs` (present whenever the guard fired, regardless of verdict) → append: "No integration claims were found in the architecture document prose. Manual review recommended to confirm that technology relationships are not documented exclusively in diagrams or implied without explicit co-mention."
      
      Store the verdict for use in the report.
      
      ### 2. Generate Prescriptive Recommendations
      
      For each non-verified finding across all passes, generate an actionable next step:
      
      **Missing skill (from Step 02):**
      - "Run **[CS] Create Skill** or **[QS] Quick Skill** for `{library_name}`, then re-run **[VS]** to verify coverage."
      
      **Replaced / being-removed technology (from Step 02):**
      - "`{library_name}` is marked for removal/replacement in the architecture document — no skill is needed. Remove it from the architecture document (or, if it is in fact staying, correct the document to drop the removal marker), then re-run **[VS]**."
      - Do not emit a [CS]/[QS] recommendation for a Replaced technology — forging a skill for a technology that is being deleted is exactly the misfire this category prevents.
      
      **Risky integration (from Step 03):**
      - If protocol mismatch → "Consider adding a bridge layer between `{lib_a}` and `{lib_b}` (e.g., HTTP adapter, message queue). Document the bridge in the architecture."
      - If type incompatibility → "Add a serialization/conversion layer between `{lib_a}` and `{lib_b}` to resolve the type mismatch identified in their API surfaces."
      - If weak evidence (Check 4 missing literal cross-reference) → "Run **[SS] Create Stack Skill** to compose `{lib_a}` and `{lib_b}` and surface integration evidence via the stack manifest, then re-run **[VS]** — the stack manifest's `integration_patterns` block will provide the literal cross-references that promote this pair from `Plausible` to `Verified`."
      
      **Blocked integration (from Step 03):**
      - If language barrier → "Replace `{lib_a}` with a `{lib_b_language}`-compatible alternative, or introduce an IPC/FFI bridge. Redesign the integration path in the architecture document."
      - If fundamental incompatibility → "Replace `{blocked_lib}` with an alternative that is compatible with `{other_lib}` in the same domain, or redesign the integration path in the architecture document."
      - **Named-candidate requirement:** For every Blocked integration where the recommendation proposes replacement, propose AT LEAST ONE named alternative library with a one-line justification (e.g., "Consider `{candidate_name}` — same domain as `{blocked_lib}`, native {target_language} support, compatible with `{other_lib}` via {mechanism}."). If you cannot name at least one concrete candidate, state explicitly: "No named candidate identified — manual research required" and still include one sentence on the selection criteria the user should apply. A Blocked recommendation without either a named candidate or the explicit no-candidate notice is a schema violation.
      
      **Not Addressed requirement (from Step 04):**
      - "No library in the stack covers `{requirement}`. Evaluate `{category}` libraries that provide this capability, generate a skill, then re-run **[VS]**."
      
      **Partially Fulfilled requirement (from Step 04):**
      - "Gap in `{requirement}`: {what_is_missing}. Consider extending `{contributing_skill}` or adding a dedicated library."
      
      **Zero integration pairs (from Step 03):**
      - If zero integration pairs were found AND the architecture references 2+ technologies: "No integration claims were found in the architecture document prose. Add explicit prose descriptions of how your technologies interact (not only in diagrams), then re-run **[VS]** to verify integrations."
      
      ### 3. Check for Previous Report
      
      Read `previousReport` from `{outputFile}` frontmatter (set in Step 01). Each run writes a new timestamped `feasibility-report-{projectSlug}-{timestamp}.md`, so prior reports persist on disk automatically — Step 01 auto-discovers the most recent one for delta comparison when no path is supplied. `previousReport` holds the resolved path, or is empty when no prior report exists or the user skipped the comparison.
      
      **Note:** A manual backup is only needed to compare against a *specific older* snapshot rather than the most recent prior run; provide that backup path when prompted in Step 01.
      
      **If a previous report is found:**
      - Extract from both reports (current run + the previous report's tables/inventory block): the coverage findings (`{technology, verdict}`), the integration findings (`{libA, libB, verdict}`), and each skill's `confidence_tier`. Reading the tables is judgment; classifying the difference is not — so hand the two extracted finding sets to the delta helper rather than diffing in prose (matching pair keys and applying the verdict ranking by hand drifts between runs).
      - Serialize as `{"previous": {"coverage": […], "integration": […]}, "current": {…}, "previousTiers": {skill: tier}, "currentTiers": {…}}` and run:
      
        ```bash
        echo '<delta JSON>' | uv run {reportDeltaScript} --stdin
        ```
      
        The script (run `uv run {reportDeltaScript} --help` for the contract and rankings) returns `improved`/`regressed`/`unchanged`/`new`/`dropped`/`replaced` label lists with counts, plus `tierDowngrades` (each `{skill, from, to}`) — a tier drop (Tier 1 → Tier 2, or T1 → T1-low) counts as a regression. If `uv` is unavailable, apply the ranking from `--help` inline (coverage Missing<Covered; integration Blocked<Risky<Plausible<Verified; tier T2<T1-low<T1; Replaced findings bucketed, not scored).
      - Render the delta section from those results. For each tier downgrade, flag: "skill `{skill}` regressed from `{from}` to `{to}` — re-extract with [CS] at the prior tier level".
      
      **If no previous report found:**
      - Note: "First verification run — no delta available."
      
      ### 4. Compile Synthesis Section
      
      Assemble the following for the report:
      
      **Overall verdict** with rationale citing the decision logic.
      
      **Recommendation list** ordered by priority (count total recommendations as `recommendationCount` — persist this count to `{outputFile}` frontmatter for use in step 6):
      1. Blocked integrations (if any)
      2. Missing skills
      3. Risky integrations
      4. Not Addressed requirements
      5. Partially Fulfilled requirements
      
      **Delta from previous run** (if applicable):
      - Improved, regressed, new, unchanged counts
      - Specific items that changed
      
      **Suggested next workflow** (match on case-sensitive `overallVerdict` token):
      - `FEASIBLE` → "Proceed to **[RA] Refine Architecture** to produce an implementation-ready architecture, then **[SS]** to compose your stack skill, then **[TS]** to test and **[EX]** to export."
      - `CONDITIONALLY_FEASIBLE` → "Address the {recommendationCount} recommendations above, then re-run **[VS]**. Once all clear, proceed to **[RA]**."
      - `NOT_FEASIBLE` → "Critical blockers must be resolved before proceeding. Apply the recommendations above and re-run **[VS]**."
      
      ### 5. Append to Report
      
      **Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. If no candidate exists: HALT (exit code 3, `halt_reason: "resolution-failure"`); in headless, emit the error envelope.
      
      **Resolve `{feasibilitySchemaRef}`** from `{feasibilitySchemaProbeOrder}`; first existing path wins (installed SKF module path first, dev-checkout `src/` fallback).
      
      Write the **Recommendations** and **Evidence Sources** sections to `{outputFile}` (per the fixed heading order in `{feasibilitySchemaRef}`):
      - Include overall verdict with rationale in the `## Executive Summary` section (replace the placeholder text from the template)
      - Include prioritized recommendation list under `## Recommendations`
      - Include delta from previous run (if applicable) under `## Recommendations` as a subsection
      - Include suggested next workflow at the end of `## Recommendations`
      - Populate `## Evidence Sources` with per-skill citations (SKILL.md path, `metadata_schema_version`, `confidence_tier`, stack manifest if any) and architecture/PRD doc paths
      - Update frontmatter (shared-schema keys):
        - Append `'synthesize'` to `stepsCompleted`
        - Set `overallVerdict` to one of `FEASIBLE`, `CONDITIONALLY_FEASIBLE`, `NOT_FEASIBLE` (case-sensitive, underscores not spaces)
        - Set `recommendationCount` to the total number of recommendations
        - If delta was computed (section 3), set `deltaImproved`, `deltaRegressed`, `deltaNew`, `deltaUnchanged` from the delta helper's `improvedCount` / `regressedCount` / `newCount` / `unchangedCount`
        - Verify that `pairsVerified`, `pairsPlausible`, `pairsRisky`, `pairsBlocked` match the counts from Step 03 (these were set in Step 03). If a discrepancy is found, overwrite the frontmatter counts with the values from Step 03 — the report file is the system of record
      - **Overall verdict enforcement (schema producer obligation):** write the `overallVerdict` the §1 rollup script returned verbatim — do not re-derive the ladder here. §1 (via `{verdictRollupScript}`) is its single source of truth (the 100%-coverage + zero-Blocked + zero-Check-4-missing bar for `FEASIBLE`, and the `coveragePercentage == 0` → `NOT_FEASIBLE` short-circuit included).
      - Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
      
      ### 6. Auto-Proceed to Next Step
      
      "**Proceeding to final report presentation...**"
      
      Load, read the full file and then execute `{nextStepFile}`.
      
      
  • scripts
    • skf-coverage-tally.py 6.1 KB
      #!/usr/bin/env python3
      # /// script
      # requires-python = ">=3.10"
      # dependencies = []
      # ///
      """Deterministic technology-coverage tally for skf-verify-stack (coverage.md §6).
      
      The coverage verdicts themselves (Covered / Missing / Replaced) are LLM judgment
      — matching an architecture technology to a generated skill involves aliases and
      prose. This script takes that already-decided coverage matrix and does only the
      part with one correct answer: counting each verdict class and turning the counts
      into `coveragePercentage`.
      
      Two gotchas make the in-prose version drift between runs, so they live here
      instead:
      
        * The denominator excludes Replaced. `live_count = Covered + Missing`;
          technologies flagged Replaced (intentionally being removed) are not a gap and
          must not dilute the percentage.
        * Rounding is pinned to half-up to the nearest integer, so the same matrix
          always yields the same `coveragePercentage` (the shared schema declares
          `coveragePercentage: <0..100 integer>` but not the rounding rule).
      
      CLI usage:
        uv run skf-coverage-tally.py '<JSON>'                  # JSON literal positional
        uv run skf-coverage-tally.py --json-input '<JSON>'     # explicit flag form
        cat input.json | uv run skf-coverage-tally.py --stdin  # piped input
      
      Input schema (one object):
        {
          "rows": [
            {"technology": "react",   "verdict": "Covered"},
            {"technology": "postgres","verdict": "Missing"},
            {"technology": "old-orm", "verdict": "Replaced"}
          ]
        }
      
      Output (stdout, one object):
        {
          "covered_count": <int>,
          "missing_count": <int>,
          "replaced_count": <int>,
          "live_count": <int>,          # Covered + Missing (denominator)
          "total_referenced": <int>,    # all rows after dedup
          "coverage_percentage": <int>  # round-half-up(covered/live*100); 0 when live==0
        }
      
      Exit codes:
        0  — tally emitted successfully
        1  — no input / input could not be parsed as JSON
        2  — input parsed but schema/semantics invalid (error object emitted as JSON)
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import math
      import sys
      
      VALID_VERDICTS = ("Covered", "Missing", "Replaced")
      
      
      def make_error(message):
          return {"error": message, "code": "INVALID_INPUT"}
      
      
      def _validate(inp):
          if inp is None or not isinstance(inp, dict):
              return "Input must be a JSON object"
          rows = inp.get("rows")
          if not isinstance(rows, list):
              return "Missing or invalid required field: rows (must be a list)"
          seen = set()
          for i, row in enumerate(rows):
              if not isinstance(row, dict):
                  return f"rows[{i}] must be an object"
              tech = row.get("technology")
              if not isinstance(tech, str) or not tech.strip():
                  return f"rows[{i}] requires a non-empty string `technology`"
              verdict = row.get("verdict")
              if verdict not in VALID_VERDICTS:
                  return (
                      f"rows[{i}] verdict {verdict!r} is not one of: "
                      f"{', '.join(VALID_VERDICTS)}"
                  )
              norm = tech.strip().lower()
              if norm in seen:
                  return f"duplicate technology {tech!r} — the coverage list must be deduplicated first"
              seen.add(norm)
          return None
      
      
      def tally(inp):
          """Pure tally. Counts each verdict class and computes coverage_percentage."""
          err = _validate(inp)
          if err:
              return make_error(err)
      
          covered = missing = replaced = 0
          for row in inp["rows"]:
              verdict = row["verdict"]
              if verdict == "Covered":
                  covered += 1
              elif verdict == "Missing":
                  missing += 1
              else:  # Replaced
                  replaced += 1
      
          live = covered + missing
          percentage = math.floor(covered / live * 100 + 0.5) if live > 0 else 0
          return {
              "covered_count": covered,
              "missing_count": missing,
              "replaced_count": replaced,
              "live_count": live,
              "total_referenced": covered + missing + replaced,
              "coverage_percentage": percentage,
          }
      
      
      # --- CLI --------------------------------------------------------------------
      
      
      def _build_parser():
          parser = argparse.ArgumentParser(
              prog="skf-coverage-tally",
              description=(
                  "Deterministic technology-coverage tally (coverage.md §6). Counts "
                  "Covered/Missing/Replaced verdicts and computes coveragePercentage "
                  "with Replaced excluded from the denominator and half-up rounding."
              ),
              formatter_class=argparse.RawDescriptionHelpFormatter,
              epilog=(
                  "Example:\n"
                  "  uv run skf-coverage-tally.py "
                  "'{\"rows\":[{\"technology\":\"react\",\"verdict\":\"Covered\"},"
                  "{\"technology\":\"postgres\",\"verdict\":\"Missing\"}]}'"
              ),
          )
          src = parser.add_mutually_exclusive_group()
          src.add_argument(
              "json_input",
              nargs="?",
              help="JSON object as a positional argument (single-quote it on the shell).",
          )
          src.add_argument(
              "--json-input",
              dest="json_input_flag",
              help="JSON object passed via flag (overrides positional).",
          )
          src.add_argument(
              "--stdin",
              action="store_true",
              help="Read the JSON object from stdin.",
          )
          return parser
      
      
      def _resolve_input(args):
          if args.stdin:
              return sys.stdin.read()
          if args.json_input_flag is not None:
              return args.json_input_flag
          if args.json_input is not None:
              return args.json_input
          return ""
      
      
      def main(argv=None):
          parser = _build_parser()
          args = parser.parse_args(argv)
          raw = _resolve_input(args)
          if not raw.strip():
              parser.print_usage(file=sys.stderr)
              print(
                  "error: no input provided (positional arg, --json-input, or --stdin)",
                  file=sys.stderr,
              )
              return 1
      
          try:
              data = json.loads(raw)
          except json.JSONDecodeError as exc:
              print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
              return 1
      
          result = tally(data)
          print(json.dumps(result, indent=2))
          if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
              return 2
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • skf-report-delta.py 9.4 KB
      #!/usr/bin/env python3
      # /// script
      # requires-python = ">=3.10"
      # dependencies = []
      # ///
      """Deterministic previous-vs-current delta for skf-verify-stack (synthesize.md §3).
      
      Reading two feasibility reports and deciding each finding's verdict is judgment;
      classifying how the two runs differ is not. Given the two already-extracted
      finding sets, "improved / regressed / new / unchanged" is a set-diff plus a
      fixed verdict ranking — one correct answer per input. Doing it in prose lets the
      counts drift (mismatched pair keys, inconsistent ranking), so it lives here.
      
      Verdict ranking (higher = healthier):
        * coverage    — Missing(0) < Covered(1). Replaced is intentional removal, not a
          gap; Replaced findings are bucketed separately and never scored improved/
          regressed (mirrors their exclusion from the coverage denominator).
        * integration — Blocked(0) < Risky(1) < Plausible(2) < Verified(3).
        * confidence tier — T2(1) < T1-low(2) < T1(3); a drop is a regression.
      
      Matching keys are normalized so identity is stable across runs:
        * coverage findings match on technology.lower()
        * integration findings match on the unordered pair {libA, libB} (lowercased),
          so "A↔B" in one run equals "B↔A" in the other.
      
      CLI usage:
        uv run skf-report-delta.py '<JSON>'                  # JSON literal positional
        uv run skf-report-delta.py --json-input '<JSON>'     # explicit flag form
        cat input.json | uv run skf-report-delta.py --stdin  # piped input
      
      Input schema (one object):
        {
          "previous": {
            "coverage":    [{"technology": "react", "verdict": "Covered"}, ...],
            "integration": [{"libA": "react", "libB": "express", "verdict": "Verified"}, ...]
          },
          "current":  { ... same shape ... },
          "previousTiers": {"react": "T1", ...},   # optional, skill_name -> tier
          "currentTiers":  {"react": "T2", ...}     # optional
        }
      
      Output (stdout, one object):
        {
          "improved": [labels], "improvedCount": <int>,
          "regressed": [labels], "regressedCount": <int>,
          "unchanged": [labels], "unchangedCount": <int>,
          "new": [labels], "newCount": <int>,
          "dropped": [labels], "droppedCount": <int>,
          "replaced": [labels], "replacedCount": <int>,
          "tierDowngrades": [{"skill": "...", "from": "T1", "to": "T2"}],
          "tierDowngradeCount": <int>
        }
      
      Exit codes:
        0  — delta emitted successfully
        1  — no input / input could not be parsed as JSON
        2  — input parsed but schema/semantics invalid (error object emitted as JSON)
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import sys
      
      COVERAGE_RANK = {"Missing": 0, "Covered": 1}
      COVERAGE_VERDICTS = ("Covered", "Missing", "Replaced")
      INTEGRATION_RANK = {"Blocked": 0, "Risky": 1, "Plausible": 2, "Verified": 3}
      INTEGRATION_VERDICTS = tuple(INTEGRATION_RANK)
      TIER_RANK = {"T2": 1, "T1-LOW": 2, "T1": 3, "T3": 0}
      
      
      def make_error(message):
          return {"error": message, "code": "INVALID_INPUT"}
      
      
      def _validate_side(side, name):
          if not isinstance(side, dict):
              return f"`{name}` must be an object with `coverage`/`integration` lists"
          cov = side.get("coverage", [])
          integ = side.get("integration", [])
          if not isinstance(cov, list) or not isinstance(integ, list):
              return f"`{name}.coverage` and `{name}.integration` must be lists"
          for i, row in enumerate(cov):
              if not isinstance(row, dict) or not isinstance(row.get("technology"), str):
                  return f"`{name}.coverage[{i}]` requires a string `technology`"
              if row.get("verdict") not in COVERAGE_VERDICTS:
                  return (
                      f"`{name}.coverage[{i}]` verdict {row.get('verdict')!r} not one of: "
                      f"{', '.join(COVERAGE_VERDICTS)}"
                  )
          for i, row in enumerate(integ):
              if not isinstance(row, dict) or not isinstance(row.get("libA"), str) or not isinstance(row.get("libB"), str):
                  return f"`{name}.integration[{i}]` requires string `libA` and `libB`"
              if row.get("verdict") not in INTEGRATION_VERDICTS:
                  return (
                      f"`{name}.integration[{i}]` verdict {row.get('verdict')!r} not one of: "
                      f"{', '.join(INTEGRATION_VERDICTS)}"
                  )
          return None
      
      
      def _validate(inp):
          if inp is None or not isinstance(inp, dict):
              return "Input must be a JSON object"
          if "previous" not in inp or "current" not in inp:
              return "Input requires `previous` and `current` objects"
          for name in ("previous", "current"):
              err = _validate_side(inp[name], name)
              if err:
                  return err
          for key in ("previousTiers", "currentTiers"):
              tiers = inp.get(key)
              if tiers is not None and not isinstance(tiers, dict):
                  return f"`{key}` must be an object mapping skill_name -> tier"
          return None
      
      
      def _index(side):
          """Return {normalized_key: (label, domain, verdict)} for one report side."""
          out = {}
          for row in side.get("coverage", []):
              tech = row["technology"].strip()
              out[("cov", tech.lower())] = (tech, "coverage", row["verdict"])
          for row in side.get("integration", []):
              a, b = row["libA"].strip(), row["libB"].strip()
              pair = tuple(sorted([a.lower(), b.lower()]))
              out[("int", pair)] = (f"{a} ↔ {b}", "integration", row["verdict"])
          return out
      
      
      def _rank(domain, verdict):
          return COVERAGE_RANK.get(verdict) if domain == "coverage" else INTEGRATION_RANK.get(verdict)
      
      
      def compute(inp):
          """Pure delta over the two extracted finding sets."""
          err = _validate(inp)
          if err:
              return make_error(err)
      
          prev = _index(inp["previous"])
          curr = _index(inp["current"])
      
          buckets = {k: [] for k in ("improved", "regressed", "unchanged", "new", "dropped", "replaced")}
      
          for key, (label, domain, verdict) in curr.items():
              if key not in prev:
                  # Replaced-on-arrival is informational, not a regression/new gap.
                  if verdict == "Replaced":
                      buckets["replaced"].append(label)
                  else:
                      buckets["new"].append(label)
                  continue
              _, _, prev_verdict = prev[key]
              if verdict == "Replaced" or prev_verdict == "Replaced":
                  buckets["replaced"].append(label)
                  continue
              pr, cr = _rank(domain, prev_verdict), _rank(domain, verdict)
              if cr > pr:
                  buckets["improved"].append(label)
              elif cr < pr:
                  buckets["regressed"].append(label)
              else:
                  buckets["unchanged"].append(label)
      
          for key, (label, _domain, verdict) in prev.items():
              if key not in curr:
                  if verdict == "Replaced":
                      buckets["replaced"].append(label)
                  else:
                      buckets["dropped"].append(label)
      
          tier_downgrades = []
          prev_tiers = inp.get("previousTiers") or {}
          curr_tiers = inp.get("currentTiers") or {}
          for skill in sorted(set(prev_tiers) & set(curr_tiers)):
              pr = TIER_RANK.get(str(prev_tiers[skill]).upper())
              cr = TIER_RANK.get(str(curr_tiers[skill]).upper())
              if pr is None or cr is None:
                  continue
              if cr < pr:
                  tier_downgrades.append({"skill": skill, "from": prev_tiers[skill], "to": curr_tiers[skill]})
      
          result = {}
          for name, items in buckets.items():
              result[name] = sorted(items)
              result[name + "Count"] = len(items)
          result["tierDowngrades"] = tier_downgrades
          result["tierDowngradeCount"] = len(tier_downgrades)
          return result
      
      
      # --- CLI --------------------------------------------------------------------
      
      
      def _build_parser():
          parser = argparse.ArgumentParser(
              prog="skf-report-delta",
              description=(
                  "Deterministic previous-vs-current feasibility delta (synthesize.md "
                  "§3). Consumes the two extracted finding sets and emits improved / "
                  "regressed / unchanged / new / dropped counts plus tier downgrades."
              ),
              formatter_class=argparse.RawDescriptionHelpFormatter,
              epilog=(
                  "Example:\n"
                  "  uv run skf-report-delta.py "
                  "'{\"previous\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Missing\"}]},"
                  "\"current\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Covered\"}]}}'"
              ),
          )
          src = parser.add_mutually_exclusive_group()
          src.add_argument("json_input", nargs="?", help="JSON object as a positional argument.")
          src.add_argument("--json-input", dest="json_input_flag", help="JSON object passed via flag.")
          src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
          return parser
      
      
      def _resolve_input(args):
          if args.stdin:
              return sys.stdin.read()
          if args.json_input_flag is not None:
              return args.json_input_flag
          if args.json_input is not None:
              return args.json_input
          return ""
      
      
      def main(argv=None):
          parser = _build_parser()
          args = parser.parse_args(argv)
          raw = _resolve_input(args)
          if not raw.strip():
              parser.print_usage(file=sys.stderr)
              print(
                  "error: no input provided (positional arg, --json-input, or --stdin)",
                  file=sys.stderr,
              )
              return 1
      
          try:
              data = json.loads(raw)
          except json.JSONDecodeError as exc:
              print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
              return 1
      
          result = compute(data)
          print(json.dumps(result, indent=2))
          if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
              return 2
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • skf-verdict-rollup.py 10.1 KB
      #!/usr/bin/env python3
      # /// script
      # requires-python = ">=3.10"
      # dependencies = []
      # ///
      """Deterministic overall-feasibility verdict rollup for skf-verify-stack (synthesize.md §1).
      
      Deciding each individual finding — is this integration Blocked, is that technology
      Missing, is a requirement Not Addressed — is judgment and happens upstream in
      Steps 02-04, persisted to the report frontmatter/tables. Rolling those already-decided
      counts up into the single overall verdict is *not* judgment: it is a fixed threshold
      ladder with one correct answer per input. Walking a five-tier ordered ladder (with a
      short-circuit, a downgrade rule, and a post-verdict guard) in-prompt lets the headline
      verdict drift between runs, so the token computation lives here. The rationale prose —
      which co-occurring problems to name, how to phrase the recommendation — stays in the
      prompt; this script emits only the token plus the stable condition codes the prompt
      cites when it writes that rationale.
      
      The ladder (exactly mirrors synthesize.md §1; evaluate top-to-bottom, first match wins):
      
        1. Zero-coverage short-circuit  — coveragePercentage == 0  -> NOT_FEASIBLE
           (no live coverage: analysis is vacuous; the remainder of the ladder is skipped).
        2. NOT_FEASIBLE                  — any integration Blocked (pairsBlocked > 0).
        3. CONDITIONALLY_FEASIBLE        — ANY of: a Missing technology (missingCount > 0),
           a Risky integration (pairsRisky > 0), or — only when the requirements pass ran —
           a Not Addressed or Partially Fulfilled requirement.
        4. FEASIBLE                      — none of the above AND zero pairs capped at
           Plausible (pairsPlausible == 0). If any pair sits at Plausible, downgrade to
           CONDITIONALLY_FEASIBLE.
      
        Post-verdict zero-integration-pairs guard (applied after ANY verdict): when all four
        integration counts are 0 AND the user continued past a step-2 zero-state [C] gate,
        the guard fires — a FEASIBLE verdict is overridden to CONDITIONALLY_FEASIBLE, and
        regardless of verdict the prompt appends the "no integration claims found" note.
      
      Note that coveragePercentage and missingCount are independent inputs on purpose: half-up
      rounding means a stack with covered=199, missing=1 rounds to coveragePercentage == 100
      while missingCount is still > 0, so the Missing trigger reads missingCount directly.
      
      CLI usage:
        uv run skf-verdict-rollup.py '<JSON>'                  # JSON literal positional
        uv run skf-verdict-rollup.py --json-input '<JSON>'     # explicit flag form
        cat input.json | uv run skf-verdict-rollup.py --stdin  # piped input
      
      Input schema (one object; counts come straight from the report frontmatter/tables):
        {
          "coveragePercentage": <int 0..100>,      # from coverage.md (coverage-tally)
          "missingCount": <int>,                   # Missing technologies (Replaced excluded)
          "pairsBlocked": <int>,                   # from integrations.md
          "pairsRisky": <int>,
          "pairsPlausible": <int>,                 # includes Check-4-missing caps
          "pairsVerified": <int>,
          "requirementsEvaluated": <bool>,         # optional (default false): requirementsPass == "completed"
          "requirementsNotAddressed": <int>,       # optional (default 0); ignored unless evaluated
          "requirementsPartial": <int>,            # optional (default 0); ignored unless evaluated
          "continuedPastZeroState": <bool>         # optional (default false): user pressed [C] past a step-2 zero-state gate
        }
      
      Output (stdout, one object):
        {
          "overallVerdict": "FEASIBLE" | "CONDITIONALLY_FEASIBLE" | "NOT_FEASIBLE",
          "matchedConditions": [<condition codes, in ladder order>],
          "zeroPairsGuardFired": <bool>
        }
      
      Condition codes (stable; the prompt cites these when synthesizing the rationale):
        zero-coverage · blocked-integration · missing-coverage · risky-integration ·
        requirements-not-addressed · requirements-partial · plausible-cap · zero-integration-pairs
      
      Exit codes:
        0  — verdict emitted successfully
        1  — no input / input could not be parsed as JSON
        2  — input parsed but schema/semantics invalid (error object emitted as JSON)
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import sys
      
      VERDICTS = ("FEASIBLE", "CONDITIONALLY_FEASIBLE", "NOT_FEASIBLE")
      REQUIRED_COUNTS = (
          "missingCount",
          "pairsBlocked",
          "pairsRisky",
          "pairsPlausible",
          "pairsVerified",
      )
      OPTIONAL_COUNTS = ("requirementsNotAddressed", "requirementsPartial")
      
      
      def make_error(message):
          return {"error": message, "code": "INVALID_INPUT"}
      
      
      def _is_nonneg_int(value):
          # bool is a subclass of int; reject it so a stray true/false can't pose as a count.
          return isinstance(value, int) and not isinstance(value, bool) and value >= 0
      
      
      def _validate(inp):
          if inp is None or not isinstance(inp, dict):
              return "Input must be a JSON object"
      
          pct = inp.get("coveragePercentage")
          if not isinstance(pct, int) or isinstance(pct, bool) or not (0 <= pct <= 100):
              return "coveragePercentage must be an integer in 0..100"
      
          for field in REQUIRED_COUNTS:
              if field not in inp:
                  return f"missing required field: {field}"
              if not _is_nonneg_int(inp[field]):
                  return f"{field} must be a non-negative integer"
      
          for field in OPTIONAL_COUNTS:
              if field in inp and inp[field] is not None and not _is_nonneg_int(inp[field]):
                  return f"{field} must be a non-negative integer when present"
      
          for field in ("requirementsEvaluated", "continuedPastZeroState"):
              if field in inp and not isinstance(inp[field], bool):
                  return f"{field} must be a boolean when present"
      
          return None
      
      
      def rollup(inp):
          """Pure verdict rollup over the persisted counts. See module docstring for the ladder."""
          err = _validate(inp)
          if err:
              return make_error(err)
      
          pct = inp["coveragePercentage"]
          missing = inp["missingCount"]
          blocked = inp["pairsBlocked"]
          risky = inp["pairsRisky"]
          plausible = inp["pairsPlausible"]
          verified = inp["pairsVerified"]
          req_eval = bool(inp.get("requirementsEvaluated", False))
          not_addressed = inp.get("requirementsNotAddressed") or 0
          partial = inp.get("requirementsPartial") or 0
          continued = bool(inp.get("continuedPastZeroState", False))
      
          matched: list[str] = []
      
          # 1. Zero-coverage short-circuit — wins over everything else.
          if pct == 0:
              verdict = "NOT_FEASIBLE"
              matched.append("zero-coverage")
          # 2. Any Blocked integration is a fundamental incompatibility.
          elif blocked > 0:
              verdict = "NOT_FEASIBLE"
              matched.append("blocked-integration")
              # Co-occurring problems the rationale should also name (§1).
              if missing > 0:
                  matched.append("missing-coverage")
              if risky > 0:
                  matched.append("risky-integration")
          else:
              # 3. Any gap / risk / unmet requirement -> conditional.
              conditional: list[str] = []
              if missing > 0:
                  conditional.append("missing-coverage")
              if risky > 0:
                  conditional.append("risky-integration")
              if req_eval and not_addressed > 0:
                  conditional.append("requirements-not-addressed")
              if req_eval and partial > 0:
                  conditional.append("requirements-partial")
              if conditional:
                  verdict = "CONDITIONALLY_FEASIBLE"
                  matched.extend(conditional)
              elif plausible > 0:
                  # 4. Clean bar except for Check-4-missing caps -> downgrade.
                  verdict = "CONDITIONALLY_FEASIBLE"
                  matched.append("plausible-cap")
              else:
                  verdict = "FEASIBLE"
      
          # Post-verdict zero-integration-pairs guard.
          zero_pairs = blocked == 0 and risky == 0 and plausible == 0 and verified == 0
          guard_fired = zero_pairs and continued
          if guard_fired:
              if verdict == "FEASIBLE":
                  verdict = "CONDITIONALLY_FEASIBLE"
              matched.append("zero-integration-pairs")
      
          return {
              "overallVerdict": verdict,
              "matchedConditions": matched,
              "zeroPairsGuardFired": guard_fired,
          }
      
      
      # --- CLI --------------------------------------------------------------------
      
      
      def _build_parser():
          parser = argparse.ArgumentParser(
              prog="skf-verdict-rollup",
              description=(
                  "Deterministic overall-feasibility verdict rollup (synthesize.md §1). "
                  "Consumes the persisted coverage / integration / requirements counts and "
                  "emits the FEASIBLE / CONDITIONALLY_FEASIBLE / NOT_FEASIBLE token plus the "
                  "condition codes the prompt cites in its rationale."
              ),
              formatter_class=argparse.RawDescriptionHelpFormatter,
              epilog=(
                  "Example:\n"
                  "  uv run skf-verdict-rollup.py "
                  "'{\"coveragePercentage\":100,\"missingCount\":0,\"pairsBlocked\":0,"
                  "\"pairsRisky\":0,\"pairsPlausible\":0,\"pairsVerified\":3}'"
              ),
          )
          src = parser.add_mutually_exclusive_group()
          src.add_argument("json_input", nargs="?", help="JSON object as a positional argument.")
          src.add_argument("--json-input", dest="json_input_flag", help="JSON object passed via flag.")
          src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
          return parser
      
      
      def _resolve_input(args):
          if args.stdin:
              return sys.stdin.read()
          if args.json_input_flag is not None:
              return args.json_input_flag
          if args.json_input is not None:
              return args.json_input
          return ""
      
      
      def main(argv=None):
          parser = _build_parser()
          args = parser.parse_args(argv)
          raw = _resolve_input(args)
          if not raw.strip():
              parser.print_usage(file=sys.stderr)
              print(
                  "error: no input provided (positional arg, --json-input, or --stdin)",
                  file=sys.stderr,
              )
              return 1
      
          try:
              data = json.loads(raw)
          except json.JSONDecodeError as exc:
              print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
              return 1
      
          result = rollup(data)
          print(json.dumps(result, indent=2))
          if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
              return 2
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • customize.toml 2.3 KB
    # DO NOT EDIT -- overwritten on every update.
    #
    # Workflow customization surface for skf-verify-stack.
    # Team overrides:     _bmad/custom/skf-verify-stack.toml under {project-root}
    # Personal overrides: _bmad/custom/skf-verify-stack.user.toml under {project-root}
    
    [workflow]
    
    # --- Configurable below. Overrides merge per BMad structural rules: ---
    #   scalars: override wins • arrays (persistent_facts, activation_steps_*): append
    #   arrays-of-tables with `code`/`id`: replace matching items, append new ones.
    
    # Steps to run before the standard activation (uv probe, config load).
    # Overrides append. Use for org-wide pre-flight checks (auth, network,
    # compliance) that must precede any verification work.
    
    activation_steps_prepend = []
    
    # Steps to run after activation but before the first stage executes.
    # Overrides append. Use for context loads or banner customization that
    # should run once activation completes successfully.
    
    activation_steps_append = []
    
    # Persistent facts the workflow keeps in mind for the whole run
    # (verification standards, evidence-citation rules, house-style verdict
    # language). Overrides append.
    #
    # Each entry is either:
    #   - a literal sentence, e.g. "Verdict citations must include file:line spans."
    #   - a file reference prefixed with `file:`, e.g.
    #     "file:{project-root}/docs/verify-policy.md" (globs supported; file
    #     contents are loaded and treated as facts).
    
    persistent_facts = [
      "file:{project-root}/**/project-context.md",
    ]
    
    # Instruction executed when the workflow reaches its terminal stage (after the
    # feasibility report + result JSON are written). Empty = no-op. Use to route
    # the verdict onward or trigger a downstream step without forking the skill.
    
    on_complete = ""
    
    # --- Optional asset overrides ---
    #
    # Lift the canonical asset paths so orgs can substitute house-style copies
    # without forking the skill. Empty string = use the bundled default.
    
    report_template_path = ""
    integration_rules_path = ""
    coverage_patterns_path = ""
    
    # Override the destination directory for the feasibility report only. Empty =
    # use {forge_data_folder} from config.yaml. The result-contract JSON
    # (verify-stack-result-*.json) always stays in {forge_data_folder} as a stable
    # path for pipeline consumers, so redirecting this does not relocate it.
    
    output_folder_path = ""
    
  • SKILL.md 8.9 KB
    ---
    name: skf-verify-stack
    description: Pre-code stack feasibility verification against architecture and PRD documents. Use when the user requests to "verify a tech stack" or "verify stack."
    ---
    
    # Verify Stack
    
    ## Overview
    
    Cross-references generated skills against architecture and PRD documents to produce a feasibility report with evidence-backed integration verdicts, coverage analysis, and requirements mapping. Read-only: it reads skills and input documents and writes only the feasibility report (see Workflow Rules).
    
    **Schema contract:** This skill is the producer of the SKF shared feasibility report schema — every report conforms to it.
    
    ## Conventions
    
    - Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
    - `references/` holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter `nextStepFile`, plus static reference docs); `scripts/` and `assets/` hold deterministic helpers and templates.
    - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present).
    - `{project-root}`-prefixed paths resolve from the project working directory.
    - `{skill-name}` resolves to the skill directory's basename.
    
    ## Role
    
    You are a stack feasibility analyst and integration verifier operating in Ferris Audit mode. You bring expertise in API surface analysis, cross-library compatibility assessment, and architecture validation, while the user brings their architecture vision and generated skills.
    
    ## Workflow Rules
    
    These rules apply to every step in this workflow:
    
    - Read-only — never modify skills, architecture docs, or PRD files
    - Every verdict must cite evidence from the generated skills
    - Only load one step file at a time — never preload future steps
    - If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread
    - Always communicate in `{communication_language}`
    - At any interactive prompt, the inputs `cancel`, `exit`, `[X]`, `q`, or `:q` exit cleanly with exit code 6 (`halt_reason: "user-cancelled"`)
    - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
    
    ## Stages
    
    | # | Step | File | Auto-proceed |
    |---|------|------|--------------|
    | 1 | Initialize & Load Inputs | references/init.md | No (confirm) |
    | 2 | Coverage Analysis | references/coverage.md | Yes |
    | 3 | Integration Verification | references/integrations.md | Yes |
    | 4 | Requirements Mapping | references/requirements.md | Yes |
    | 5 | Synthesize Verdict | references/synthesize.md | Yes |
    | 6 | Report | references/report.md | No (confirm) |
    | 7 | Workflow Health Check | references/health-check.md | Yes |
    
    ## Invocation Contract
    
    | Aspect | Detail |
    |--------|--------|
    | **Inputs** | architecture_doc_path [required], prd_path [optional], previous_report_path [optional] |
    | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--architecture-doc <path>` (skip step 1 prompt for the required input); `--prd <path>` (skip step 1 prompt for the optional PRD); `--previous-report <path>` (skip step 1 prompt for delta comparison) |
    | **Gates** | step 1 Input Gate (use args); step 6 Report Menu ([R] review / [X] exit, headless default X). Steps 2-3 also hold elective vacuous-analysis guards (0% coverage, all-Blocked) that only fire in degenerate cases; every guard auto-resolves to Continue in headless. |
    | **Outputs** | `feasibility-report-{projectSlug}-{timestamp}.md` and `feasibility-report-{projectSlug}-latest.md` (copy, not symlink) per the SKF shared feasibility report schema (`_bmad/skf/shared/references/feasibility-report-schema.md`; `src/shared/references/…` in a dev checkout) — with integration verdicts, coverage analysis, recommendations, and evidence sources; plus `verify-stack-result-{timestamp}.json` and `verify-stack-result-latest.json` |
    | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Per-flag args (`--architecture-doc`, `--prd`, `--previous-report`) consumed at the gates that would otherwise prompt. |
    | **Exit codes** | See `references/exit-codes.md` |
    
    ## Result Contract (Headless)
    
    When `{headless_mode}` is true, step 6 emits a single-line JSON envelope on **stdout** before chaining to step 7, and every headless hard halt emits the same envelope shape on **stderr** with `status: "error"`:
    
    ```
    SKF_VERIFY_STACK_RESULT_JSON: {"status":"success|error","report_path":"…|null","report_latest_path":"…|null","overall_verdict":"…|null","coverage_percentage":0,"recommendation_count":0,"exit_code":0,"halt_reason":null}
    ```
    
    `status` is `"success"` on the terminal happy path, `"error"` on any halt. `halt_reason` is one of: `null` (success), `"input-missing"`, `"input-invalid"`, `"skills-folder-missing"`, `"insufficient-skills"`, `"forge-folder-unconfigured"`, `"resolution-failure"`, `"previous-report-collision"`, `"inventory-unreliable"`, `"schema-violation"`, `"write-failed"`, `"user-cancelled"`. `exit_code` matches `references/exit-codes.md` (the `analysis-halted` / exit-8 gates are interactive-only, so they never reach this envelope). `overall_verdict` uses the schema tokens (`FEASIBLE`/`CONDITIONALLY_FEASIBLE`/`NOT_FEASIBLE`).
    
    ## On Activation
    
    1. Load config from `{project-root}/_bmad/skf/config.yaml` and resolve:
       - `project_name`, `user_name`, `communication_language`, `document_output_language`
       - `skills_output_folder`, `forge_data_folder`, `sidecar_path`
    
    2. **Compute run-scoped variables** (same place as config so every stage can reference them without re-derivation):
       - `project_slug` ← slugify `project_name` (lowercase, hyphens only, no unicode, no whitespace)
       - `timestamp` ← UTC `YYYYMMDD-HHmmss` captured at activation time
       - These two combine in init.md §4 into `{outputFile}` per the stage frontmatter template, but the values themselves are fixed for the entire workflow run — every later reference to `{outputFile}` resolves consistently.
    
    3. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
    
    4. **Resolve workflow customization.** Run:
    
       ```bash
       python3 {project-root}/_bmad/scripts/resolve_customization.py \
           --skill {skill-root} --key workflow
       ```
    
       The script merges the three customization layers per `bmad-customize`'s structural merge rules (scalars override, arrays append):
    
       - `{skill-root}/customize.toml` — bundled defaults
       - `_bmad/custom/<skill-name>.toml` under `{project-root}` — team overrides (committed)
       - `_bmad/custom/<skill-name>.user.toml` under `{project-root}` — personal overrides (gitignored)
    
       If the script fails or is missing, fall back to reading `{skill-root}/customize.toml` directly — the bundled defaults are an empty string for each path scalar.
    
       Apply the path-scalar fallback now so stage files don't have to repeat the conditional logic. For each of the four scalars, if the merged value is empty or absent, use the bundled default:
    
       - `{reportTemplatePath}` ← `workflow.report_template_path` if non-empty, else `assets/feasibility-report-template.md`
       - `{integrationRulesPath}` ← `workflow.integration_rules_path` if non-empty, else `references/integration-verification-rules.md`
       - `{coveragePatternsPath}` ← `workflow.coverage_patterns_path` if non-empty, else `references/coverage-patterns.md`
       - `{outputFolderPath}` ← `workflow.output_folder_path` if non-empty, else `{forge_data_folder}`
    
       Stash all four as workflow-context variables. Stage files reference them directly — no conditional at the usage site. Empty-string overrides cleanly fall through to the bundled default.
    
       The same merge resolves `workflow.on_complete` (default empty = no-op); report.md §5 executes it, if non-empty, at the terminal stage.
    
       Also apply the array surfaces so they are not silent no-ops: execute each entry in `workflow.activation_steps_prepend` in order now; treat every entry in `workflow.persistent_facts` as standing context for the whole run (`file:`-prefixed entries load their file/glob contents as facts — the bundled default glob is `{project-root}/**/project-context.md`); then execute each entry in `workflow.activation_steps_append` after activation completes.
    
    5. **Pre-flight write probe.** Verify `{outputFolderPath}` is writable. A read-only mount, full disk, or permissions-denied path otherwise only surfaces at init.md §4 atomic write — by then the user has already gone through the input prompts:
    
       ```bash
       mkdir -p "{outputFolderPath}" && \
         printf 'probe' > "{outputFolderPath}/.skf-write-probe" && \
         rm "{outputFolderPath}/.skf-write-probe"
       ```
    
       On any non-zero exit: HALT (exit code 4, `halt_reason: "write-failed"`). In headless mode, emit the error envelope per **Result Contract (Headless)** with `report_path: null`, `report_latest_path: null`, `overall_verdict: null`.
    
    6. Load, read the full file, and then execute `references/init.md` to begin the workflow.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related