Compare commits

...
Author SHA1 Message Date
Ben Meadors 4386ea26f5 Merge branch 'master' into 129-hardware-support-agent 2026-05-03 06:15:23 -05:00
Ben Meadors 41f53177a1 Use OBS instead of launchpad (#10375) 2026-05-02 09:25:24 -04:00
github-actions[bot]andvidplace7 2d761f6453 Upgrade trunk (#10364)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-05-02 07:23:49 -05:00
renovate[bot] 7cb071c780 Update platform-native digest to cab4b21 (#10372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-01 18:25:08 -04:00
Austin Lane 0240a00d09 MacOS: Re-Add Orcania/Yder 2026-05-01 10:55:32 -04:00
Austin c0fcf807c0 MacOS: Correct pkg-config name openssl for ulfius. (#10369) 2026-05-01 10:42:17 -04:00
55f40ecdfd Add ulfius webserver support to macos native target (#10366)
* Add ulfius webserver support to macos native target

* fix: update PiWebServer docs for macOS and add explicit cstring include

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/3ce82582-23e0-4afe-b22f-b24f81721488

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: add --cflags to openssl@3 pkg-config and fix apt package name

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/1a6c59aa-4393-4134-8cee-61eeee0e9127

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-01 08:56:49 -05:00
Jason P 90744ee0b7 Update PhoneAPI.cpp to reduce chattiness (#10367) 2026-05-01 08:46:53 -05:00
AustinandCopilot 4ee9598107 Docker: Install grpcio-tools from distro (#10358)
Use distro provided Python at build time (instead of the `python` images from dockerhub) and install `grpcio-tools` using the distro provided packages.

This should speed up build times, ESPECIALLY on riscv64 (where prebuilt `grpcio-tools` wheels are not provided on pip).

Co-authored-by: Copilot <copilot@github.com>
2026-04-30 15:22:11 -05:00
Ben Meadors 7066abbb86 Fix MAC_from_string to use input parameter instead of global config for MAC address parsing (#10356)
* Fix MAC_from_string to use input parameter instead of global config for MAC address parsing

* Enhance MAC_from_string validation and error handling

* Add missing include for <cctype> in PortduinoGlue.cpp
2026-04-30 13:52:42 -05:00
Ben Meadors 21cef8c2e5 Add TCP support for Meshtastic MCP interface / tests and update docs (#10355)
* Add TCP support for Meshtastic MCP interface / tests and update docs

* Address TCP endpoint validation and error handling in connection

* TCP connection handling and device listing logic

* Fix docstring formatting in normalize_tcp_endpoint function
2026-04-30 13:51:29 -05:00
github-actions[bot]andthebentern 173ac58ed7 Update protobufs (#10357)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-04-30 10:45:20 -05:00
github-actions[bot]andvidplace7 83adfd417a Upgrade trunk (#10354)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-04-30 06:39:52 -05:00
AustinandCopilot 24d64a0013 Docker: Build for riscv64 (#10345)
Upstream support has been added in Debian and Alpine.
Only build as part of `docker_manifest` (Beta/Alpha/Daily) releases, because these will take a **while** thanks to qemu.

Co-authored-by: Copilot <copilot@github.com>
2026-04-29 21:04:49 -05:00
Ben Meadors 3a87fc82c0 Add documentation for macOS support in Copilot and Agent instructions 2026-04-29 19:54:05 -05:00
Austin 478444eb02 Docker-Alpine: Align version between build/main stages (#10347)
FROM python:3.14-alpine3.23 AS builder
FROM alpine:3.23

the alpine version needs to match in both stages 😅
2026-04-29 20:31:59 -04:00
renovate[bot] ad23c42fcc Update meshtastic/device-ui digest to 4bf593a (#10346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 19:09:21 -05:00
Ben Meadors ad03f6f6d8 Implement Hardware Support Agent Specification and Workflow 2026-03-26 08:55:11 -05:00
81 changed files with 8904 additions and 102 deletions
+29
View File
@@ -0,0 +1,29 @@
# firmware Development Guidelines
Auto-generated from all feature plans. Last updated: 2026-03-25
## Active Technologies
- Python 3.x for workflow tooling, Markdown for generated artifacts, existing C/C++/PlatformIO repository conventions for downstream scaffold targets + Python standard library for inventory/intake tooling, existing Spec Kit artifacts, repository-local Copilot prompt/agent files, PlatformIO environment metadata conventions already in `variants/**/platformio.ini` (129-hardware-support-agent)
## Project Structure
```text
src/
tests/
```
## Commands
cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] pytest [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] ruff check .
## Code Style
Python 3.x for workflow tooling, Markdown for generated artifacts, existing C/C++/PlatformIO repository conventions for downstream scaffold targets: Follow standard conventions
## Recent Changes
- 129-hardware-support-agent: Added Python 3.x for workflow tooling, Markdown for generated artifacts, existing C/C++/PlatformIO repository conventions for downstream scaffold targets + Python standard library for inventory/intake tooling, existing Spec Kit artifacts, repository-local Copilot prompt/agent files, PlatformIO environment metadata conventions already in `variants/**/platformio.ini`
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->
+124
View File
@@ -0,0 +1,124 @@
---
description: Guide maintainers through Meshtastic hardware support context generation, board intake assessment, and optional scaffold generation.
---
# Hardware Support Workflow
Use this workflow when a maintainer wants to add support for a new board variant in the Meshtastic firmware repository.
## Goals
- Reuse repository-backed hardware patterns before drafting new board files.
- Keep all generated output scoped to the requested architecture and board.
- Stop and surface evidence gaps instead of inventing pin mappings or metadata.
## Required Inputs
Collect or confirm these fields before scaffold generation:
- PlatformIO environment name
- hardware model identifier
- display name
- architecture
Recommended additional inputs:
- hardware model slug
- actively supported flag
- support level
- source materials such as schematic, pinout, or datasheet links
- board notes covering revision scope and known uncertainty
## Workflow
### 1. Refresh Repository Context
Run from the repository root:
```bash
python3 bin/generate_hardware_support_context.py
```
Review [docs/hardware-support-context.md](../../docs/hardware-support-context.md) for architecture-specific examples, metadata keys, and inherited-default notes.
### 2. Capture The Intake Request
Create or update a JSON file matching the contract in [specs/129-hardware-support-agent/contracts/board-intake-contract.md](../../specs/129-hardware-support-agent/contracts/board-intake-contract.md).
### 3. Assess Intake Readiness
Run:
```bash
python3 bin/board_intake.py path/to/intake.json
```
What to look for:
- expected artifacts
- required metadata
- matched repository patterns
- evidence gaps
- risk flags
- next actions
- scaffold readiness decision
For CI-style gating, use:
```bash
python3 bin/board_intake.py path/to/intake.json --validate
```
If the assessment is not scaffold-ready, stop and resolve the blocking gaps before continuing.
### 4. Generate Scaffold Output When Ready
Only run this when the intake assessment reports `Scaffold ready: Yes`.
```bash
python3 bin/board_scaffold.py path/to/intake.json --output-dir generated/hardware-support
```
Expected outputs:
- draft `variant.h`
- draft `platformio.ini`
- optional `variant.cpp` for ESP32-family targets
Review all `// TODO: verify — ...` annotations before treating the scaffold as merge-ready.
### 5. Compile-Gate The Target Environment (Required)
The end stage must always validate that the target environment is at least compilable.
Run:
```bash
pio run -e <environment_name>
```
Expected behavior:
- If compile succeeds, include a "compile check passed" note in the review summary.
- If compile fails, treat it as a blocking issue and report the exact failing error.
- Do not mark the workflow complete while compile is failing.
Common first-pass blocker for new scaffolds:
- Missing or placeholder `board = ...` in `platformio.ini` causes `BoardConfig: Board is not defined`.
## Guardrails
- Do not change live firmware runtime code under `src/` as part of this workflow.
- Do not modify existing board definitions under `variants/` automatically.
- Do not guess unresolved radio, display, GPS, power, or input pin mappings.
- Treat multi-revision or multi-option board notes as blocking until the revision scope is explicit.
- Check inherited BSP defaults for `nrf52840`, `rp2040`, `stm32`, and `native` targets before declaring a missing define.
## Validation Expectations
- Run `trunk fmt --force` on touched Python workflow files.
- Re-run `python3 bin/generate_hardware_support_context.py` after changes affecting context output.
- Use fixture-driven smoke tests in `bin/fixtures/` for intake and scaffold workflows.
- Run `pio run -e <environment_name>` as a required final compile gate for the generated board environment.
- Report any skipped validation or remaining TODO annotations in the review notes.
+184
View File
@@ -0,0 +1,184 @@
---
description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation.
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Goal
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
## Operating Constraints
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
## Execution Steps
### 1. Initialize Analysis Context
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths:
- SPEC = FEATURE_DIR/spec.md
- PLAN = FEATURE_DIR/plan.md
- TASKS = FEATURE_DIR/tasks.md
Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command).
For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
### 2. Load Artifacts (Progressive Disclosure)
Load only the minimal necessary context from each artifact:
**From spec.md:**
- Overview/Context
- Functional Requirements
- Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact)
- User Stories
- Edge Cases (if present)
**From plan.md:**
- Architecture/stack choices
- Data Model references
- Phases
- Technical constraints
**From tasks.md:**
- Task IDs
- Descriptions
- Phase grouping
- Parallel markers [P]
- Referenced file paths
**From constitution:**
- Load `.specify/memory/constitution.md` for principle validation
### 3. Build Semantic Models
Create internal representations (do not include raw artifacts in output):
- **Requirements inventory**: For each Functional Requirement (FR-###) and Success Criterion (SC-###), record a stable key. Use the explicit FR-/SC- identifier as the primary key when present, and optionally also derive an imperative-phrase slug for readability (e.g., "User can upload file" → `user-can-upload-file`). Include only Success Criteria items that require buildable work (e.g., load-testing infrastructure, security audit tooling), and exclude post-launch outcome metrics and business KPIs (e.g., "Reduce support tickets by 50%").
- **User story/action inventory**: Discrete user actions with acceptance criteria
- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases)
- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements
### 4. Detection Passes (Token-Efficient Analysis)
Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary.
#### A. Duplication Detection
- Identify near-duplicate requirements
- Mark lower-quality phrasing for consolidation
#### B. Ambiguity Detection
- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria
- Flag unresolved placeholders (TODO, TKTK, ???, `<placeholder>`, etc.)
#### C. Underspecification
- Requirements with verbs but missing object or measurable outcome
- User stories missing acceptance criteria alignment
- Tasks referencing files or components not defined in spec/plan
#### D. Constitution Alignment
- Any requirement or plan element conflicting with a MUST principle
- Missing mandated sections or quality gates from constitution
#### E. Coverage Gaps
- Requirements with zero associated tasks
- Tasks with no mapped requirement/story
- Success Criteria requiring buildable work (performance, security, availability) not reflected in tasks
#### F. Inconsistency
- Terminology drift (same concept named differently across files)
- Data entities referenced in plan but absent in spec (or vice versa)
- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note)
- Conflicting requirements (e.g., one requires Next.js while other specifies Vue)
### 5. Severity Assignment
Use this heuristic to prioritize findings:
- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality
- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion
- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case
- **LOW**: Style/wording improvements, minor redundancy not affecting execution order
### 6. Produce Compact Analysis Report
Output a Markdown report (no file writes) with the following structure:
## Specification Analysis Report
| ID | Category | Severity | Location(s) | Summary | Recommendation |
| --- | ----------- | -------- | ---------------- | ---------------------------- | ------------------------------------ |
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
(Add one row per finding; generate stable IDs prefixed by category initial.)
**Coverage Summary Table:**
| Requirement Key | Has Task? | Task IDs | Notes |
| --------------- | --------- | -------- | ----- |
**Constitution Alignment Issues:** (if any)
**Unmapped Tasks:** (if any)
**Metrics:**
- Total Requirements
- Total Tasks
- Coverage % (requirements with >=1 task)
- Ambiguity Count
- Duplication Count
- Critical Issues Count
### 7. Provide Next Actions
At end of report, output a concise Next Actions block:
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
### 8. Offer Remediation
Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.)
## Operating Principles
### Context Efficiency
- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation
- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis
- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow
- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts
### Analysis Guidelines
- **NEVER modify files** (this is read-only analysis)
- **NEVER hallucinate missing sections** (if absent, report them accurately)
- **Prioritize constitution violations** (these are always CRITICAL)
- **Use examples over exhaustive rules** (cite specific instances, not generic patterns)
- **Report zero issues gracefully** (emit success report with coverage statistics)
## Context
$ARGUMENTS
+295
View File
@@ -0,0 +1,295 @@
---
description: Generate a custom checklist for the current feature based on user requirements.
---
## Checklist Purpose: "Unit Tests for English"
**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain.
**NOT for verification/testing**:
- ❌ NOT "Verify the button clicks correctly"
- ❌ NOT "Test error handling works"
- ❌ NOT "Confirm the API returns 200"
- ❌ NOT checking if code/implementation matches the spec
**FOR requirements quality validation**:
- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness)
- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity)
- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency)
- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage)
- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases)
**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works.
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Execution Steps
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list.
- All file paths must be absolute.
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
- Be generated from the user's phrasing + extracted signals from spec/plan/tasks
- Only ask about information that materially changes checklist content
- Be skipped individually if already unambiguous in `$ARGUMENTS`
- Prefer precision over breadth
Generation algorithm:
1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts").
2. Cluster signals into candidate focus areas (max 4) ranked by relevance.
3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit.
4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria.
5. Formulate questions chosen from these archetypes:
- Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?")
- Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?")
- Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?")
- Audience framing (e.g., "Will this be used by the author only or peers during PR review?")
- Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?")
- Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?")
Question formatting rules:
- If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters
- Limit to A–E options maximum; omit table if a free-form answer is clearer
- Never ask the user to restate what they already said
- Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope."
Defaults when interaction impossible:
- Depth: Standard
- Audience: Reviewer (PR) if code-related; Author otherwise
- Focus: Top 2 relevance clusters
Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more.
3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
- Derive checklist theme (e.g., security, review, deploy, ux)
- Consolidate explicit must-have items mentioned by user
- Map focus selections to category scaffolding
- Infer any missing context from spec/plan/tasks (do NOT hallucinate)
4. **Load feature context**: Read from FEATURE_DIR:
- spec.md: Feature requirements and scope
- plan.md (if exists): Technical details, dependencies
- tasks.md (if exists): Implementation tasks
**Context Loading Strategy**:
- Load only necessary portions relevant to active focus areas (avoid full-file dumping)
- Prefer summarizing long sections into concise scenario/requirement bullets
- Use progressive disclosure: add follow-on retrieval only if gaps detected
- If source docs are large, generate interim summary items instead of embedding raw text
5. **Generate checklist** - Create "Unit Tests for Requirements":
- Create `FEATURE_DIR/checklists/` directory if it doesn't exist
- Generate unique checklist filename:
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
- Format: `[domain].md`
- File handling behavior:
- If file does NOT exist: Create new file and number items starting from CHK001
- If file exists: Append new items to existing file, continuing from the last CHK ID (e.g., if last item is CHK015, start new items at CHK016)
- Never delete or replace existing checklist content - always preserve and append
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
- **Completeness**: Are all necessary requirements present?
- **Clarity**: Are requirements unambiguous and specific?
- **Consistency**: Do requirements align with each other?
- **Measurability**: Can requirements be objectively verified?
- **Coverage**: Are all scenarios/edge cases addressed?
**Category Structure** - Group items by requirement quality dimensions:
- **Requirement Completeness** (Are all necessary requirements documented?)
- **Requirement Clarity** (Are requirements specific and unambiguous?)
- **Requirement Consistency** (Do requirements align without conflicts?)
- **Acceptance Criteria Quality** (Are success criteria measurable?)
- **Scenario Coverage** (Are all flows/cases addressed?)
- **Edge Case Coverage** (Are boundary conditions defined?)
- **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?)
- **Dependencies & Assumptions** (Are they documented and validated?)
- **Ambiguities & Conflicts** (What needs clarification?)
**HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**:
❌ **WRONG** (Testing implementation):
- "Verify landing page displays 3 episode cards"
- "Test hover states work on desktop"
- "Confirm logo click navigates home"
✅ **CORRECT** (Testing requirements quality):
- "Are the exact number and layout of featured episodes specified?" [Completeness]
- "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity]
- "Are hover state requirements consistent across all interactive elements?" [Consistency]
- "Are keyboard navigation requirements defined for all interactive UI?" [Coverage]
- "Is the fallback behavior specified when logo image fails to load?" [Edge Cases]
- "Are loading states defined for asynchronous episode data?" [Completeness]
- "Does the spec define visual hierarchy for competing UI elements?" [Clarity]
**ITEM STRUCTURE**:
Each item should follow this pattern:
- Question format asking about requirement quality
- Focus on what's WRITTEN (or not written) in the spec/plan
- Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.]
- Reference spec section `[Spec §X.Y]` when checking existing requirements
- Use `[Gap]` marker when checking for missing requirements
**EXAMPLES BY QUALITY DIMENSION**:
Completeness:
- "Are error handling requirements defined for all API failure modes? [Gap]"
- "Are accessibility requirements specified for all interactive elements? [Completeness]"
- "Are mobile breakpoint requirements defined for responsive layouts? [Gap]"
Clarity:
- "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]"
- "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]"
- "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]"
Consistency:
- "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]"
- "Are card component requirements consistent between landing and detail pages? [Consistency]"
Coverage:
- "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]"
- "Are concurrent user interaction scenarios addressed? [Coverage, Gap]"
- "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]"
Measurability:
- "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]"
- "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]"
**Scenario Classification & Coverage** (Requirements Quality Focus):
- Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios
- For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?"
- If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]"
- Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]"
**Traceability Requirements**:
- MINIMUM: ≥80% of items MUST include at least one traceability reference
- Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]`
- If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]"
**Surface & Resolve Issues** (Requirements Quality Problems):
Ask questions about the requirements themselves:
- Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]"
- Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]"
- Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]"
- Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]"
- Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]"
**Content Consolidation**:
- Soft cap: If raw candidate items > 40, prioritize by risk/impact
- Merge near-duplicates checking the same requirement aspect
- If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]"
**🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test:
- ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior
- ❌ References to code execution, user actions, system behavior
- ❌ "Displays correctly", "works properly", "functions as expected"
- ❌ "Click", "navigate", "render", "load", "execute"
- ❌ Test cases, test plans, QA procedures
- ❌ Implementation details (frameworks, APIs, algorithms)
**✅ REQUIRED PATTERNS** - These test requirements quality:
- ✅ "Are [requirement type] defined/specified/documented for [scenario]?"
- ✅ "Is [vague term] quantified/clarified with specific criteria?"
- ✅ "Are requirements consistent between [section A] and [section B]?"
- ✅ "Can [requirement] be objectively measured/verified?"
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
- ✅ "Does the spec define [missing aspect]?"
6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
7. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize:
- Focus areas selected
- Depth level
- Actor/timing
- Any explicit user-specified must-have items incorporated
**Important**: Each `/speckit.checklist` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows:
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
- Simple, memorable filenames that indicate checklist purpose
- Easy identification and navigation in the `checklists/` folder
To avoid clutter, use descriptive types and clean up obsolete checklists when done.
## Example Checklist Types & Sample Items
**UX Requirements Quality:** `ux.md`
Sample items (testing the requirements, NOT the implementation):
- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]"
- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]"
- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]"
- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]"
- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]"
- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]"
**API Requirements Quality:** `api.md`
Sample items:
- "Are error response formats specified for all failure scenarios? [Completeness]"
- "Are rate limiting requirements quantified with specific thresholds? [Clarity]"
- "Are authentication requirements consistent across all endpoints? [Consistency]"
- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]"
- "Is versioning strategy documented in requirements? [Gap]"
**Performance Requirements Quality:** `performance.md`
Sample items:
- "Are performance requirements quantified with specific metrics? [Clarity]"
- "Are performance targets defined for all critical user journeys? [Coverage]"
- "Are performance requirements under different load conditions specified? [Completeness]"
- "Can performance requirements be objectively measured? [Measurability]"
- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]"
**Security Requirements Quality:** `security.md`
Sample items:
- "Are authentication requirements specified for all protected resources? [Coverage]"
- "Are data protection requirements defined for sensitive information? [Completeness]"
- "Is the threat model documented and requirements aligned to it? [Traceability]"
- "Are security requirements consistent with compliance obligations? [Consistency]"
- "Are security failure/breach response requirements defined? [Gap, Exception Flow]"
## Anti-Examples: What NOT To Do
**❌ WRONG - These test implementation, not requirements:**
```markdown
- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001]
- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003]
- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010]
- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005]
```
**✅ CORRECT - These test requirements quality:**
```markdown
- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001]
- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003]
- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010]
- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005]
- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap]
- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001]
```
**Key Differences:**
- Wrong: Tests if the system works correctly
- Correct: Tests if the requirements are written correctly
- Wrong: Verification of behavior
- Correct: Validation of requirement quality
- Wrong: "Does it do X?"
- Correct: "Is X clearly specified?"
+181
View File
@@ -0,0 +1,181 @@
---
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
Execution steps:
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields:
- `FEATURE_DIR`
- `FEATURE_SPEC`
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
Functional Scope & Behavior:
- Core user goals & success criteria
- Explicit out-of-scope declarations
- User roles / personas differentiation
Domain & Data Model:
- Entities, attributes, relationships
- Identity & uniqueness rules
- Lifecycle/state transitions
- Data volume / scale assumptions
Interaction & UX Flow:
- Critical user journeys / sequences
- Error/empty/loading states
- Accessibility or localization notes
Non-Functional Quality Attributes:
- Performance (latency, throughput targets)
- Scalability (horizontal/vertical, limits)
- Reliability & availability (uptime, recovery expectations)
- Observability (logging, metrics, tracing signals)
- Security & privacy (authN/Z, data protection, threat assumptions)
- Compliance / regulatory constraints (if any)
Integration & External Dependencies:
- External services/APIs and failure modes
- Data import/export formats
- Protocol/versioning assumptions
Edge Cases & Failure Handling:
- Negative scenarios
- Rate limiting / throttling
- Conflict resolution (e.g., concurrent edits)
Constraints & Tradeoffs:
- Technical constraints (language, storage, hosting)
- Explicit tradeoffs or rejected alternatives
Terminology & Consistency:
- Canonical glossary terms
- Avoided synonyms / deprecated terms
Completion Signals:
- Acceptance criteria testability
- Measurable Definition of Done style indicators
Misc / Placeholders:
- TODO markers / unresolved decisions
- Ambiguous adjectives ("robust", "intuitive") lacking quantification
For each category with Partial or Missing status, add a candidate question opportunity unless:
- Clarification would not materially change implementation or validation strategy
- Information is better deferred to planning phase (note internally)
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
- Maximum of 5 total questions across the whole session.
- Each question must be answerable with EITHER:
- A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR
- A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words").
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
- If more than 5 categories remain unresolved, select the top 5 by (Impact \* Uncertainty) heuristic.
4. Sequential questioning loop (interactive):
- Present EXACTLY ONE question at a time.
- For multiple‑choice questions:
- **Analyze all options** and determine the **most suitable option** based on:
- Best practices for the project type
- Common patterns in similar implementations
- Risk reduction (security, performance, maintainability)
- Alignment with any explicit project goals or constraints visible in the spec
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
- Format as: `**Recommended:** Option [X] - <reasoning>`
- Then render all options as a Markdown table:
| Option | Description |
| ------ | --------------------------------------------------------------------------------------------------- |
| A | <Option A description> |
| B | <Option B description> |
| C | <Option C description> (add D/E as needed up to 5) |
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
- For short‑answer style (no meaningful discrete options):
- Provide your **suggested answer** based on best practices and context.
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
- After the user answers:
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
- Stop asking further questions when:
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
- User signals completion ("done", "good", "no more"), OR
- You reach 5 asked questions.
- Never reveal future queued questions in advance.
- If no valid questions exist at start, immediately report no critical ambiguities.
5. Integration after EACH accepted answer (incremental update approach):
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
- For the first integrated answer in this session:
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
- Then immediately apply the clarification to the most appropriate section(s):
- Functional ambiguity → Update or add a bullet in Functional Requirements.
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
- Non-functional constraint → Add/modify measurable criteria in Success Criteria > Measurable Outcomes (convert vague adjective to metric or explicit target).
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
- Keep each inserted clarification minimal and testable (avoid narrative drift).
6. Validation (performed after EACH write plus final pass):
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
- Total asked (accepted) questions ≤ 5.
- Updated sections contain no lingering vague placeholders the new answer was meant to resolve.
- No contradictory earlier statement remains (scan for now-invalid alternative choices removed).
- Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`.
- Terminology consistency: same canonical term used across all updated sections.
7. Write the updated spec back to `FEATURE_SPEC`.
8. Report completion (after questioning loop ends or early termination):
- Number of questions asked & answered.
- Path to updated spec.
- Sections touched (list names).
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
- Suggested next command.
Behavior rules:
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
- Respect user early termination signals ("stop", "done", "proceed").
- If no questions asked due to full coverage, output a compact coverage summary (all categories Clear) then suggest advancing.
- If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale.
Context for prioritization: $ARGUMENTS
@@ -0,0 +1,84 @@
---
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
handoffs:
- label: Build Specification
agent: speckit.specify
prompt: Implement the feature specification based on the updated constitution. I want to build...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
**Note**: If `.specify/memory/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
Follow this execution flow:
1. Load the existing constitution at `.specify/memory/constitution.md`.
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
2. Collect/derive values for placeholders:
- If user input (conversation) supplies a value, use it.
- Otherwise infer from existing repo context (README, docs, prior constitution versions if embedded).
- For governance dates: `RATIFICATION_DATE` is the original adoption date (if unknown ask or mark TODO), `LAST_AMENDED_DATE` is today if changes are made, otherwise keep previous.
- `CONSTITUTION_VERSION` must increment according to semantic versioning rules:
- MAJOR: Backward incompatible governance/principle removals or redefinitions.
- MINOR: New principle/section added or materially expanded guidance.
- PATCH: Clarifications, wording, typo fixes, non-semantic refinements.
- If version bump type ambiguous, propose reasoning before finalizing.
3. Draft the updated constitution content:
- Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left).
- Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance.
- Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing non‑negotiable rules, explicit rationale if not obvious.
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
4. Consistency propagation checklist (convert prior checklist into active validations):
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
- Version change: old → new
- List of modified principles (old title → new title if renamed)
- Added sections
- Removed sections
- Templates requiring updates (✅ updated / ⚠ pending) with file paths
- Follow-up TODOs if any placeholders intentionally deferred.
6. Validation before final output:
- No remaining unexplained bracket tokens.
- Version line matches report.
- Dates ISO format YYYY-MM-DD.
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite).
8. Output a final summary to the user with:
- New version and bump rationale.
- Any files flagged for manual follow-up.
- Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`).
Formatting & Style Requirements:
- Use Markdown headings exactly as in the template (do not demote/promote levels).
- Wrap long rationale lines to keep readability (<100 chars ideally) but do not hard enforce with awkward breaks.
- Keep a single blank line between sections.
- Avoid trailing whitespace.
If the user supplies partial updates (e.g., only one principle revision), still perform validation and version decision steps.
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file.
+207
View File
@@ -0,0 +1,207 @@
---
description: Execute the implementation plan by processing and executing all tasks defined in tasks.md
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before implementation)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_implement` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Pre-Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Pre-Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
Wait for the result of the hook command before proceeding to the Outline.
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Outline
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Check checklists status** (if FEATURE_DIR/checklists/ exists):
- Scan all checklist files in the checklists/ directory
- For each checklist, count:
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
- Completed items: Lines matching `- [X]` or `- [x]`
- Incomplete items: Lines matching `- [ ]`
- Create a status table:
```text
| Checklist | Total | Completed | Incomplete | Status |
|-----------|-------|-----------|------------|--------|
| ux.md | 12 | 12 | 0 | ✓ PASS |
| test.md | 8 | 5 | 3 | ✗ FAIL |
| security.md | 6 | 6 | 0 | ✓ PASS |
```
- Calculate overall status:
- **PASS**: All checklists have 0 incomplete items
- **FAIL**: One or more checklists have incomplete items
- **If any checklist is incomplete**:
- Display the table with incomplete item counts
- **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)"
- Wait for user response before continuing
- If user says "no" or "wait" or "stop", halt execution
- If user says "yes" or "proceed" or "continue", proceed to step 3
- **If all checklists are complete**:
- Display the table showing all checklists passed
- Automatically proceed to step 3
3. Load and analyze the implementation context:
- **REQUIRED**: Read tasks.md for the complete task list and execution plan
- **REQUIRED**: Read plan.md for tech stack, architecture, and file structure
- **IF EXISTS**: Read data-model.md for entities and relationships
- **IF EXISTS**: Read contracts/ for API specifications and test requirements
- **IF EXISTS**: Read research.md for technical decisions and constraints
- **IF EXISTS**: Read quickstart.md for integration scenarios
4. **Project Setup Verification**:
- **REQUIRED**: Create/verify ignore files based on actual project setup:
**Detection & Creation Logic**:
- Check if the following command succeeds to determine if the repository is a git repo (create/verify .gitignore if so):
```sh
git rev-parse --git-dir 2>/dev/null
```
- Check if Dockerfile\* exists or Docker in plan.md → create/verify .dockerignore
- Check if .eslintrc\* exists → create/verify .eslintignore
- Check if eslint.config.\* exists → ensure the config's `ignores` entries cover required patterns
- Check if .prettierrc\* exists → create/verify .prettierignore
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
- Check if terraform files (\*.tf) exist → create/verify .terraformignore
- Check if .helmignore needed (helm charts present) → create/verify .helmignore
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
**If ignore file missing**: Create with full pattern set for detected technology
**Common Patterns by Technology** (from plan.md tech stack):
- **Node.js/JavaScript/TypeScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*`
- **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/`
- **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/`
- **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/`
- **Go**: `*.exe`, `*.test`, `vendor/`, `*.out`
- **Ruby**: `.bundle/`, `log/`, `tmp/`, `*.gem`, `vendor/bundle/`
- **PHP**: `vendor/`, `*.log`, `*.cache`, `*.env`
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `*.dll`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*`
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
**Tool-Specific Patterns**:
- **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/`
- **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js`
- **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
- **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl`
- **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt`
5. Parse tasks.md structure and extract:
- **Task phases**: Setup, Tests, Core, Integration, Polish
- **Task dependencies**: Sequential vs parallel execution rules
- **Task details**: ID, description, file paths, parallel markers [P]
- **Execution flow**: Order and dependency requirements
6. Execute implementation following the task plan:
- **Phase-by-phase execution**: Complete each phase before moving to the next
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
- **File-based coordination**: Tasks affecting the same files must run sequentially
- **Validation checkpoints**: Verify each phase completion before proceeding
7. Implementation execution rules:
- **Setup first**: Initialize project structure, dependencies, configuration
- **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios
- **Core development**: Implement models, services, CLI commands, endpoints
- **Integration work**: Database connections, middleware, logging, external services
- **Polish and validation**: Unit tests, performance optimization, documentation
8. Progress tracking and error handling:
- Report progress after each completed task
- Halt execution if any non-parallel task fails
- For parallel tasks [P], continue with successful tasks, report failed ones
- Provide clear error messages with context for debugging
- Suggest next steps if implementation cannot proceed
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.
9. Completion validation:
- Verify all required tasks are completed
- Check that implemented features match the original specification
- Validate that tests pass and coverage meets requirements
- Confirm the implementation follows the technical plan
- Report final status with summary of completed work
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_implement` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
+162
View File
@@ -0,0 +1,162 @@
---
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
handoffs:
- label: Create Tasks
agent: speckit.tasks
prompt: Break the plan into tasks
send: true
- label: Create Checklist
agent: speckit.checklist
prompt: Create a checklist for the following domain...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before planning)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_plan` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Pre-Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Pre-Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
Wait for the result of the hook command before proceeding to the Outline.
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Outline
1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load context**: Read FEATURE_SPEC and `.specify/memory/constitution.md`. Load IMPL_PLAN template (already copied).
3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
- Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")
- Fill Constitution Check section from constitution
- Evaluate gates (ERROR if violations unjustified)
- Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION)
- Phase 1: Generate data-model.md, contracts/, quickstart.md
- Phase 1: Update agent context by running the agent script
- Re-evaluate Constitution Check post-design
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
5. **Check for extension hooks**: After reporting, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_plan` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Phases
### Phase 0: Outline & Research
1. **Extract unknowns from Technical Context** above:
- For each NEEDS CLARIFICATION → research task
- For each dependency → best practices task
- For each integration → patterns task
2. **Generate and dispatch research agents**:
```text
For each unknown in Technical Context:
Task: "Research {unknown} for {feature context}"
For each technology choice:
Task: "Find best practices for {tech} in {domain}"
```
3. **Consolidate findings** in `research.md` using format:
- Decision: [what was chosen]
- Rationale: [why chosen]
- Alternatives considered: [what else evaluated]
**Output**: research.md with all NEEDS CLARIFICATION resolved
### Phase 1: Design & Contracts
**Prerequisites:** `research.md` complete
1. **Extract entities from feature spec** → `data-model.md`:
- Entity name, fields, relationships
- Validation rules from requirements
- State transitions if applicable
2. **Define interface contracts** (if project has external interfaces) → `/contracts/`:
- Identify what interfaces the project exposes to users or other systems
- Document the contract format appropriate for the project type
- Examples: public APIs for libraries, command schemas for CLI tools, endpoints for web services, grammars for parsers, UI contracts for applications
- Skip if project is purely internal (build scripts, one-off tools, etc.)
3. **Agent context update**:
- Run `.specify/scripts/bash/update-agent-context.sh copilot`
- These scripts detect which AI agent is in use
- Update the appropriate agent-specific context file
- Add only new technology from current plan
- Preserve manual additions between markers
**Output**: data-model.md, /contracts/\*, quickstart.md, agent-specific file
## Key rules
- Use absolute paths
- ERROR on gate failures or unresolved clarifications
+313
View File
@@ -0,0 +1,313 @@
---
description: Create or update the feature specification from a natural language feature description.
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
- label: Clarify Spec Requirements
agent: speckit.clarify
prompt: Clarify specification requirements
send: true
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before specification)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_specify` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Pre-Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Pre-Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
Wait for the result of the hook command before proceeding to the Outline.
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Outline
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
Given that feature description, do this:
1. **Generate a concise short name** (2-4 words) for the branch:
- Analyze the feature description and extract the most meaningful keywords
- Create a 2-4 word short name that captures the essence of the feature
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
- Keep it concise but descriptive enough to understand the feature at a glance
- Examples:
- "I want to add user authentication" → "user-auth"
- "Implement OAuth2 integration for the API" → "oauth2-api-integration"
- "Create a dashboard for analytics" → "analytics-dashboard"
- "Fix payment processing timeout bug" → "fix-payment-timeout"
2. **Create the feature branch** by running the script with `--short-name` (and `--json`). In sequential mode, do NOT pass `--number` — the script auto-detects the next available number. In timestamp mode, the script generates a `YYYYMMDD-HHMMSS` prefix automatically:
**Branch numbering mode**: Before running the script, check if `.specify/init-options.json` exists and read the `branch_numbering` value.
- If `"timestamp"`, add `--timestamp` (Bash) or `-Timestamp` (PowerShell) to the script invocation
- If `"sequential"` or absent, do not add any extra flag (default behavior)
- Bash example: `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" --json --short-name "user-auth" "Add user authentication"`
- Bash (timestamp): `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" --json --timestamp --short-name "user-auth" "Add user authentication"`
- PowerShell example: `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" -Json -ShortName "user-auth" "Add user authentication"`
- PowerShell (timestamp): `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" -Json -Timestamp -ShortName "user-auth" "Add user authentication"`
**IMPORTANT**:
- Do NOT pass `--number` — the script determines the correct next number automatically
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
- You must only ever run this script once per feature
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
3. Load `.specify/templates/spec-template.md` to understand required sections.
4. Follow this execution flow:
1. Parse user description from Input
If empty: ERROR "No feature description provided"
2. Extract key concepts from description
Identify: actors, actions, data, constraints
3. For unclear aspects:
- Make informed guesses based on context and industry standards
- Only mark with [NEEDS CLARIFICATION: specific question] if:
- The choice significantly impacts feature scope or user experience
- Multiple reasonable interpretations exist with different implications
- No reasonable default exists
- **LIMIT: Maximum 3 [NEEDS CLARIFICATION] markers total**
- Prioritize clarifications by impact: scope > security/privacy > user experience > technical details
4. Fill User Scenarios & Testing section
If no clear user flow: ERROR "Cannot determine user scenarios"
5. Generate Functional Requirements
Each requirement must be testable
Use reasonable defaults for unspecified details (document assumptions in Assumptions section)
6. Define Success Criteria
Create measurable, technology-agnostic outcomes
Include both quantitative metrics (time, performance, volume) and qualitative measures (user satisfaction, task completion)
Each criterion must be verifiable without implementation details
7. Identify Key Entities (if data involved)
8. Return: SUCCESS (spec ready for planning)
5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria:
a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items:
```markdown
# Specification Quality Checklist: [FEATURE NAME]
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: [DATE]
**Feature**: [Link to spec.md]
## Content Quality
- [ ] No implementation details (languages, frameworks, APIs)
- [ ] Focused on user value and business needs
- [ ] Written for non-technical stakeholders
- [ ] All mandatory sections completed
## Requirement Completeness
- [ ] No [NEEDS CLARIFICATION] markers remain
- [ ] Requirements are testable and unambiguous
- [ ] Success criteria are measurable
- [ ] Success criteria are technology-agnostic (no implementation details)
- [ ] All acceptance scenarios are defined
- [ ] Edge cases are identified
- [ ] Scope is clearly bounded
- [ ] Dependencies and assumptions identified
## Feature Readiness
- [ ] All functional requirements have clear acceptance criteria
- [ ] User scenarios cover primary flows
- [ ] Feature meets measurable outcomes defined in Success Criteria
- [ ] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
```
b. **Run Validation Check**: Review the spec against each checklist item:
- For each item, determine if it passes or fails
- Document specific issues found (quote relevant spec sections)
c. **Handle Validation Results**:
- **If all items pass**: Mark checklist complete and proceed to step 7
- **If items fail (excluding [NEEDS CLARIFICATION])**:
1. List the failing items and specific issues
2. Update the spec to address each issue
3. Re-run validation until all items pass (max 3 iterations)
4. If still failing after 3 iterations, document remaining issues in checklist notes and warn user
- **If [NEEDS CLARIFICATION] markers remain**:
1. Extract all [NEEDS CLARIFICATION: ...] markers from the spec
2. **LIMIT CHECK**: If more than 3 markers exist, keep only the 3 most critical (by scope/security/UX impact) and make informed guesses for the rest
3. For each clarification needed (max 3), present options to user in this format:
```markdown
## Question [N]: [Topic]
**Context**: [Quote relevant spec section]
**What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
**Suggested Answers**:
| Option | Answer | Implications |
| ------ | ------------------------- | ------------------------------------- |
| A | [First suggested answer] | [What this means for the feature] |
| B | [Second suggested answer] | [What this means for the feature] |
| C | [Third suggested answer] | [What this means for the feature] |
| Custom | Provide your own answer | [Explain how to provide custom input] |
**Your choice**: _[Wait for user response]_
```
4. **CRITICAL - Table Formatting**: Ensure markdown tables are properly formatted:
- Use consistent spacing with pipes aligned
- Each cell should have spaces around content: `| Content |` not `|Content|`
- Header separator must have at least 3 dashes: `|--------|`
- Test that the table renders correctly in markdown preview
5. Number questions sequentially (Q1, Q2, Q3 - max 3 total)
6. Present all questions together before waiting for responses
7. Wait for user to respond with their choices for all questions (e.g., "Q1: A, Q2: Custom - [details], Q3: B")
8. Update the spec by replacing each [NEEDS CLARIFICATION] marker with the user's selected or provided answer
9. Re-run validation after all clarifications are resolved
d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status
7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`).
8. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_specify` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
## Quick Guidelines
- Focus on **WHAT** users need and **WHY**.
- Avoid HOW to implement (no tech stack, APIs, code structure).
- Written for business stakeholders, not developers.
- DO NOT create any checklists that are embedded in the spec. That will be a separate command.
### Section Requirements
- **Mandatory sections**: Must be completed for every feature
- **Optional sections**: Include only when relevant to the feature
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
### For AI Generation
When creating this spec from a user prompt:
1. **Make informed guesses**: Use context, industry standards, and common patterns to fill gaps
2. **Document assumptions**: Record reasonable defaults in the Assumptions section
3. **Limit clarifications**: Maximum 3 [NEEDS CLARIFICATION] markers - use only for critical decisions that:
- Significantly impact feature scope or user experience
- Have multiple reasonable interpretations with different implications
- Lack any reasonable default
4. **Prioritize clarifications**: scope > security/privacy > user experience > technical details
5. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
6. **Common areas needing clarification** (only if no reasonable default exists):
- Feature scope and boundaries (include/exclude specific use cases)
- User types and permissions (if multiple conflicting interpretations possible)
- Security/compliance requirements (when legally/financially significant)
**Examples of reasonable defaults** (don't ask about these):
- Data retention: Industry-standard practices for the domain
- Performance targets: Standard web/mobile app expectations unless specified
- Error handling: User-friendly messages with appropriate fallbacks
- Authentication method: Standard session-based or OAuth2 for web apps
- Integration patterns: Use project-appropriate patterns (REST/GraphQL for web services, function calls for libraries, CLI args for tools, etc.)
### Success Criteria Guidelines
Success criteria must be:
1. **Measurable**: Include specific metrics (time, percentage, count, rate)
2. **Technology-agnostic**: No mention of frameworks, languages, databases, or tools
3. **User-focused**: Describe outcomes from user/business perspective, not system internals
4. **Verifiable**: Can be tested/validated without knowing implementation details
**Good examples**:
- "Users can complete checkout in under 3 minutes"
- "System supports 10,000 concurrent users"
- "95% of searches return results in under 1 second"
- "Task completion rate improves by 40%"
**Bad examples** (implementation-focused):
- "API response time is under 200ms" (too technical, use "Users see results instantly")
- "Database can handle 1000 TPS" (implementation detail, use user-facing metric)
- "React components render efficiently" (framework-specific)
- "Redis cache hit rate above 80%" (technology-specific)
+209
View File
@@ -0,0 +1,209 @@
---
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
handoffs:
- label: Analyze For Consistency
agent: speckit.analyze
prompt: Run a project analysis for consistency
send: true
- label: Implement Project
agent: speckit.implement
prompt: Start the implementation in phases
send: true
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before tasks generation)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_tasks` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Pre-Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Pre-Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
Wait for the result of the hook command before proceeding to the Outline.
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Outline
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load design documents**: Read from FEATURE_DIR:
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
- **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md (test scenarios)
- Note: Not all projects have all documents. Generate tasks based on what's available.
3. **Execute task generation workflow**:
- Load plan.md and extract tech stack, libraries, project structure
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
- If data-model.md exists: Extract entities and map to user stories
- If contracts/ exists: Map interface contracts to user stories
- If research.md exists: Extract decisions for setup tasks
- Generate tasks organized by user story (see Task Generation Rules below)
- Generate dependency graph showing user story completion order
- Create parallel execution examples per user story
- Validate task completeness (each user story has all needed tasks, independently testable)
4. **Generate tasks.md**: Use `.specify/templates/tasks-template.md` as structure, fill with:
- Correct feature name from plan.md
- Phase 1: Setup tasks (project initialization)
- Phase 2: Foundational tasks (blocking prerequisites for all user stories)
- Phase 3+: One phase per user story (in priority order from spec.md)
- Each phase includes: story goal, independent test criteria, tests (if requested), implementation tasks
- Final Phase: Polish & cross-cutting concerns
- All tasks must follow the strict checklist format (see Task Generation Rules below)
- Clear file paths for each task
- Dependencies section showing story completion order
- Parallel execution examples per story
- Implementation strategy section (MVP first, incremental delivery)
5. **Report**: Output path to generated tasks.md and summary:
- Total task count
- Task count per user story
- Parallel opportunities identified
- Independent test criteria for each story
- Suggested MVP scope (typically just User Story 1)
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
6. **Check for extension hooks**: After tasks.md is generated, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_tasks` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
Context for task generation: $ARGUMENTS
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
## Task Generation Rules
**CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing.
**Tests are OPTIONAL**: Only generate test tasks if explicitly requested in the feature specification or if user requests TDD approach.
### Checklist Format (REQUIRED)
Every task MUST strictly follow this format:
```text
- [ ] [TaskID] [P?] [Story?] Description with file path
```
**Format Components**:
1. **Checkbox**: ALWAYS start with `- [ ]` (markdown checkbox)
2. **Task ID**: Sequential number (T001, T002, T003...) in execution order
3. **[P] marker**: Include ONLY if task is parallelizable (different files, no dependencies on incomplete tasks)
4. **[Story] label**: REQUIRED for user story phase tasks only
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
- Setup phase: NO story label
- Foundational phase: NO story label
- User Story phases: MUST have story label
- Polish phase: NO story label
5. **Description**: Clear action with exact file path
**Examples**:
- ✅ CORRECT: `- [ ] T001 Create project structure per implementation plan`
- ✅ CORRECT: `- [ ] T005 [P] Implement authentication middleware in src/middleware/auth.py`
- ✅ CORRECT: `- [ ] T012 [P] [US1] Create User model in src/models/user.py`
- ✅ CORRECT: `- [ ] T014 [US1] Implement UserService in src/services/user_service.py`
- ❌ WRONG: `- [ ] Create User model` (missing ID and Story label)
- ❌ WRONG: `T001 [US1] Create model` (missing checkbox)
- ❌ WRONG: `- [ ] [US1] Create User model` (missing Task ID)
- ❌ WRONG: `- [ ] T001 [US1] Create model` (missing file path)
### Task Organization
1. **From User Stories (spec.md)** - PRIMARY ORGANIZATION:
- Each user story (P1, P2, P3...) gets its own phase
- Map all related components to their story:
- Models needed for that story
- Services needed for that story
- Interfaces/UI needed for that story
- If tests requested: Tests specific to that story
- Mark story dependencies (most stories should be independent)
2. **From Contracts**:
- Map each interface contract → to the user story it serves
- If tests requested: Each interface contract → contract test task [P] before implementation in that story's phase
3. **From Data Model**:
- Map each entity to the user story(ies) that need it
- If entity serves multiple stories: Put in earliest story or Setup phase
- Relationships → service layer tasks in appropriate story phase
4. **From Setup/Infrastructure**:
- Shared infrastructure → Setup phase (Phase 1)
- Foundational/blocking tasks → Foundational phase (Phase 2)
- Story-specific setup → within that story's phase
### Phase Structure
- **Phase 1**: Setup (project initialization)
- **Phase 2**: Foundational (blocking prerequisites - MUST complete before user stories)
- **Phase 3+**: User Stories in priority order (P1, P2, P3...)
- Within each story: Tests (if requested) → Models → Services → Endpoints → Integration
- Each phase should be a complete, independently testable increment
- **Final Phase**: Polish & Cross-Cutting Concerns
@@ -0,0 +1,30 @@
---
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts.
tools: ["github/github-mcp-server/issue_write"]
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
1. From the executed script, extract the path to **tasks**.
1. Get the Git remote by running:
```bash
git config --get remote.origin.url
```
> [!CAUTION]
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
> [!CAUTION]
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL
+5 -1
View File
@@ -13,6 +13,7 @@ Meshtastic is an open-source LoRa mesh networking project for long-range, low-po
- **RP2040/RP2350** - Raspberry Pi Pico variants
- **STM32WL** - STM32 with integrated LoRa
- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.)
- **macOS native** - Headless `meshtasticd` on Apple Silicon / x86_64; see `variants/native/portduino/platformio.ini` for Homebrew prereqs + CH341 LoRa setup
### Supported Radio Chips
@@ -369,7 +370,7 @@ To reduce avoidable agent mistakes, assume these tools are available (or install
- **Required CLI basics**: `bash`, `git`, `find`, `grep`, `sed`, `awk`, `xargs`
- **Strongly recommended**: `rg` (ripgrep) for fast file/text search, `jq` for JSON processing
- **Build/test tools**: `python3`, `pip`, virtualenv (`python3 -m venv`), `platformio` (`pio`)
- **Containerized native testing**: `docker` (especially important on macOS / non-Linux hosts)
- **Containerized native testing**: `docker` (fallback for non-Linux hosts; macOS can also build natively via `pio run -e native-macos`)
Fallback expectations for agents:
@@ -388,6 +389,7 @@ Build commands:
pio run -e tbeam # Build specific target
pio run -e tbeam -t upload # Build and upload
pio run -e native # Build native/Linux version
pio run -e native-macos # Build headless macOS meshtasticd (Homebrew prereqs in variants/native/portduino/platformio.ini)
```
### Build Manifest
@@ -573,6 +575,8 @@ Grouped by purpose. Full argument shapes in `mcp-server/README.md`; a few high-v
`confirm=True` is a tool-level gate on top of whatever permission prompt your MCP host shows. **Don't bypass it** by asking the host to auto-approve — it exists specifically because MCP hosts sometimes remember "always allow this tool" and that's dangerous for `factory_reset`, `erase_and_flash`, `uhubctl_power(action='off')`, and `uhubctl_cycle`.
**TCP / native-host nodes.** Setting `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` makes `list_devices` surface a `meshtasticd` daemon (e.g. the `native-macos` build) as a synthetic `tcp://host:port` entry, and `connect()` routes through `meshtastic.tcp_interface.TCPInterface` instead of `SerialInterface`. Every read/write/admin tool that flows through `connect()` works against the daemon transparently. USB-only tools (`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`, `serial_open`, `esptool_*`, `nrfutil_*`, `picotool_*`) raise a clear `ConnectionError` when handed a `tcp://` port; `pio_flash` against a `native*` env raises a `FlashError` (no upload step — use `build` and run the binary directly). The pytest harness still assumes USB-attached devices per role; TCP-aware fixtures are deferred. See `mcp-server/README.md` § "TCP / native-host nodes".
### Hardware test suite (`mcp-server/run-tests.sh`)
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
@@ -0,0 +1,3 @@
---
agent: hardware-support
---
@@ -0,0 +1,3 @@
---
agent: speckit.analyze
---
@@ -0,0 +1,3 @@
---
agent: speckit.checklist
---
@@ -0,0 +1,3 @@
---
agent: speckit.clarify
---
@@ -0,0 +1,3 @@
---
agent: speckit.constitution
---
@@ -0,0 +1,3 @@
---
agent: speckit.implement
---
+3
View File
@@ -0,0 +1,3 @@
---
agent: speckit.plan
---
@@ -0,0 +1,3 @@
---
agent: speckit.specify
---
+3
View File
@@ -0,0 +1,3 @@
---
agent: speckit.tasks
---
@@ -0,0 +1,3 @@
---
agent: speckit.taskstoissues
---
+8 -3
View File
@@ -32,10 +32,15 @@ jobs:
shell: bash
working-directory: meshtasticd
run: |
# Build-tools (notably platformio) come from the Meshtastic project
# on the OpenSUSE Build Service:
# https://build.opensuse.org/project/show/network:Meshtastic:build-tools
echo 'deb http://download.opensuse.org/repositories/network:/Meshtastic:/build-tools/xUbuntu_24.04/ /' \
| sudo tee /etc/apt/sources.list.d/network:Meshtastic:build-tools.list
curl -fsSL https://download.opensuse.org/repositories/network:Meshtastic:build-tools/xUbuntu_24.04/Release.key \
| gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/network_Meshtastic_build-tools.gpg >/dev/null
sudo apt-get update -y --fix-missing
sudo apt-get install -y software-properties-common build-essential devscripts equivs
sudo add-apt-repository ppa:meshtastic/build-tools -y
sudo apt-get update -y --fix-missing
sudo apt-get install -y build-essential devscripts equivs
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
- name: Import GPG key
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
shell: bash
run: |
brew update
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config ulfius
- name: Get release version string
run: |
+3 -1
View File
@@ -73,7 +73,9 @@ jobs:
- name: Sanitize platform string
id: sanitize_platform
# Replace slashes with underscores
run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
env:
plat: ${{ inputs.platform }}
run: echo "cleaned_platform=${plat}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
- name: Docker login
if: ${{ inputs.push }}
+22
View File
@@ -43,6 +43,15 @@ jobs:
push: true
secrets: inherit
docker-debian-riscv64:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/riscv64
runs-on: ubuntu-24.04-arm
push: true
secrets: inherit
docker-alpine-amd64:
uses: ./.github/workflows/docker_build.yml
with:
@@ -70,16 +79,27 @@ jobs:
push: true
secrets: inherit
docker-alpine-riscv64:
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/riscv64
runs-on: ubuntu-24.04-arm
push: true
secrets: inherit
docker-manifest:
needs:
# Debian
- docker-debian-amd64
- docker-debian-arm64
- docker-debian-armv7
- docker-debian-riscv64
# Alpine
- docker-alpine-amd64
- docker-alpine-arm64
- docker-alpine-armv7
- docker-alpine-riscv64
runs-on: ubuntu-24.04
steps:
- name: Checkout code
@@ -162,6 +182,7 @@ jobs:
meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-debian-riscv64.outputs.digest }}
- name: Docker meta (Alpine)
id: meta_alpine
@@ -182,3 +203,4 @@ jobs:
meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-alpine-armv7.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-alpine-riscv64.outputs.digest }}
+11
View File
@@ -0,0 +1,11 @@
{
"ai": "copilot",
"ai_commands_dir": null,
"ai_skills": false,
"branch_numbering": "sequential",
"here": true,
"offline": false,
"preset": null,
"script": "sh",
"speckit_version": "0.4.2"
}
+136
View File
@@ -0,0 +1,136 @@
<!--
Sync Impact Report
Version change: template -> 1.0.0
Modified principles:
- [PRINCIPLE_1_NAME] -> I. Safety-Critical Mesh Behavior
- [PRINCIPLE_2_NAME] -> II. Variant-Scoped Hardware Truth
- [PRINCIPLE_3_NAME] -> III. Verification by Targeted Evidence
- [PRINCIPLE_4_NAME] -> IV. Resource and Power Discipline
- [PRINCIPLE_5_NAME] -> V. Minimal, Reviewable Change Sets
Added sections:
- Engineering Constraints
- Delivery Workflow
Removed sections:
- None
Templates requiring updates:
- ✅ .specify/templates/plan-template.md
- ✅ .specify/templates/spec-template.md
- ✅ .specify/templates/tasks-template.md
- ⚠ pending .specify/templates/checklist-template.md (not required for current constitution alignment)
- ⚠ pending .specify/templates/agent-file-template.md (generic scaffold, no constitution-specific drift found)
Follow-up TODOs:
- None
-->
# Meshtastic Firmware Constitution
## Core Principles
### I. Safety-Critical Mesh Behavior
All feature and bug-fix work MUST preserve safe, predictable behavior on live mesh networks.
Changes that affect routing, airtime, broadcast intervals, MQTT bridging, channel handling,
or packet processing MUST document the operational impact on shared bandwidth, public-channel
abuse protections, and interoperability. Any relaxation of rate limits, encryption behavior,
or default-channel safeguards MUST be treated as a breaking governance change unless explicitly
approved and justified.
Rationale: Meshtastic devices operate in constrained radio environments where seemingly small
behavior changes can degrade network reliability, privacy, and fairness for other nodes.
### II. Variant-Scoped Hardware Truth
Board definitions, platform conditionals, and peripheral flags MUST reflect verified hardware
truth and MUST remain scoped to the exact supported target. New capabilities in `variant.h`,
`platformio.ini`, `pins_arduino.h`, or related board files MUST be backed by pin mappings,
chip selection, and power assumptions that are consistent with the board design. Cross-board
copying is prohibited unless every reused define is revalidated for the destination variant.
Rationale: This firmware spans many architectures and board revisions; incorrect hardware
declarations create silent regressions that are hard to detect until devices are flashed.
### III. Verification by Targeted Evidence
Every change MUST be validated by the smallest credible evidence that matches its risk.
At minimum, contributors MUST run formatting or static validation for touched files and MUST
run a targeted build, test, or simulation path for the affected platform when feasible.
Changes to shared core logic, protobufs, routing, or configuration defaults SHOULD include a
native test, simulator run, or equivalent cross-target evidence. If validation cannot be run,
the gap MUST be stated explicitly in the plan, tasks, and final review.
Rationale: The repository supports many targets, so quality depends on explicit validation
rather than assumptions that one successful build implies system-wide safety.
### IV. Resource and Power Discipline
Implementations MUST respect embedded constraints for memory, flash, CPU, battery, and radio
duty cycle. New dependencies, background tasks, logging, polling, display work, and peripheral
power use MUST be justified against the target hardware footprint. Defaults MUST prefer safe
operation on constrained devices, and network-facing behavior MUST account for scaling with
node count where existing project patterns provide that mechanism.
Rationale: Meshtastic firmware runs on low-power devices where unnecessary work directly harms
battery life, responsiveness, thermal behavior, and mesh capacity.
### V. Minimal, Reviewable Change Sets
Changes MUST solve the root problem with the smallest coherent diff that fits the existing
architecture and coding patterns. Unrelated refactors, opportunistic renames, and speculative
abstractions are prohibited in the same change unless they are required to make the fix safe.
Public behavior, configuration semantics, and generated artifacts MUST remain stable unless the
specification and plan explicitly call out the intended change.
Rationale: Small, scoped changes are easier to review across board variants and reduce the
risk of hidden regressions in a large multi-platform firmware repository.
## Engineering Constraints
The authoritative implementation context for this repository is `.github/copilot-instructions.md`.
Plans and tasks MUST align with the existing PlatformIO-based build system, generated protobuf
workflow, architecture-specific source layout, and hardware-variant structure already used in
the repository.
Feature work MUST honor these constraints:
- Code MUST follow existing logging, naming, threading, and configuration patterns.
- Default values and user-facing configuration changes MUST use existing `Default` helpers and
public-channel safeguards where applicable.
- Protobuf changes MUST include regeneration steps and identify downstream effects in generated
code and dependent modules.
- Build and validation steps MUST prefer repository-standard commands such as targeted `pio run`,
`pio test -e native`, simulator tooling, and `trunk fmt` where applicable.
- Platform-specific logic MUST be isolated to the narrowest valid architecture or variant scope.
## Delivery Workflow
Spec-driven work in this repository MUST produce artifacts that make operational risk visible
before code is written.
- Specifications MUST describe affected user or device behavior, impacted platforms, and edge
cases for unavailable peripherals, misconfigured variants, and constrained-network scenarios.
- Plans MUST include a Constitution Check that names the exact validation evidence, target
environments, and any justified deviations from the constitution.
- Tasks MUST be organized so that foundational hardware, protocol, or configuration work is
completed before feature-specific behavior that depends on it.
- Review and implementation notes MUST call out any skipped validation, generated-file updates,
migration considerations, or behavior changes requiring maintainer scrutiny.
## Governance
This constitution supersedes ad hoc workflow preferences for spec-driven work in this repository.
All plans, tasks, reviews, and implementation summaries MUST verify compliance with these
principles.
- Amendments MUST be documented in this file and reflected in dependent templates before new
work proceeds under the changed rule set.
- Versioning policy for this constitution follows semantic versioning.
MAJOR versions indicate removed or materially redefined principles.
MINOR versions indicate new principles, sections, or materially expanded guidance.
PATCH versions indicate clarifications, wording improvements, or non-semantic refinements.
- Compliance review is mandatory for every feature plan and code review. Any constitutional
violation MUST be listed in the plan's Complexity Tracking section or equivalent justification.
- Repository guidance files remain authoritative for implementation detail. Where conflict is
discovered, this constitution governs process and quality gates, while `.github/copilot-instructions.md`
governs repository-specific coding practice until the conflict is resolved by amendment.
**Version**: 1.0.0 | **Ratified**: 2026-03-25 | **Last Amended**: 2026-03-25
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env bash
# Consolidated prerequisite checking script
#
# This script provides unified prerequisite checking for Spec-Driven Development workflow.
# It replaces the functionality previously spread across multiple scripts.
#
# Usage: ./check-prerequisites.sh [OPTIONS]
#
# OPTIONS:
# --json Output in JSON format
# --require-tasks Require tasks.md to exist (for implementation phase)
# --include-tasks Include tasks.md in AVAILABLE_DOCS list
# --paths-only Only output path variables (no validation)
# --help, -h Show help message
#
# OUTPUTS:
# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
set -e
# Parse command line arguments
JSON_MODE=false
REQUIRE_TASKS=false
INCLUDE_TASKS=false
PATHS_ONLY=false
for arg in "$@"; do
case "$arg" in
--json)
JSON_MODE=true
;;
--require-tasks)
REQUIRE_TASKS=true
;;
--include-tasks)
INCLUDE_TASKS=true
;;
--paths-only)
PATHS_ONLY=true
;;
--help | -h)
cat <<'EOF'
Usage: check-prerequisites.sh [OPTIONS]
Consolidated prerequisite checking for Spec-Driven Development workflow.
OPTIONS:
--json Output in JSON format
--require-tasks Require tasks.md to exist (for implementation phase)
--include-tasks Include tasks.md in AVAILABLE_DOCS list
--paths-only Only output path variables (no prerequisite validation)
--help, -h Show this help message
EXAMPLES:
# Check task prerequisites (plan.md required)
./check-prerequisites.sh --json
# Check implementation prerequisites (plan.md + tasks.md required)
./check-prerequisites.sh --json --require-tasks --include-tasks
# Get feature paths only (no validation)
./check-prerequisites.sh --paths-only
EOF
exit 0
;;
*)
echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2
exit 1
;;
esac
done
# Source common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get feature paths and validate branch
_paths_output=$(get_feature_paths) || {
echo "ERROR: Failed to resolve feature paths" >&2
exit 1
}
eval "$_paths_output"
unset _paths_output
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
if $PATHS_ONLY; then
if $JSON_MODE; then
# Minimal JSON paths payload (no validation performed)
if has_jq; then
jq -cn \
--arg repo_root "$REPO_ROOT" \
--arg branch "$CURRENT_BRANCH" \
--arg feature_dir "$FEATURE_DIR" \
--arg feature_spec "$FEATURE_SPEC" \
--arg impl_plan "$IMPL_PLAN" \
--arg tasks "$TASKS" \
'{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'
else
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
"$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"
fi
else
echo "REPO_ROOT: $REPO_ROOT"
echo "BRANCH: $CURRENT_BRANCH"
echo "FEATURE_DIR: $FEATURE_DIR"
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"
echo "TASKS: $TASKS"
fi
exit 0
fi
# Validate required directories and files
if [[ ! -d $FEATURE_DIR ]]; then
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
echo "Run /speckit.specify first to create the feature structure." >&2
exit 1
fi
if [[ ! -f $IMPL_PLAN ]]; then
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
echo "Run /speckit.plan first to create the implementation plan." >&2
exit 1
fi
# Check for tasks.md if required
if $REQUIRE_TASKS && [[ ! -f $TASKS ]]; then
echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
echo "Run /speckit.tasks first to create the task list." >&2
exit 1
fi
# Build list of available documents
docs=()
# Always check these optional docs
[[ -f $RESEARCH ]] && docs+=("research.md")
[[ -f $DATA_MODEL ]] && docs+=("data-model.md")
# Check contracts directory (only if it exists and has files)
if [[ -d $CONTRACTS_DIR ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
docs+=("contracts/")
fi
[[ -f $QUICKSTART ]] && docs+=("quickstart.md")
# Include tasks.md if requested and it exists
if $INCLUDE_TASKS && [[ -f $TASKS ]]; then
docs+=("tasks.md")
fi
# Output results
if $JSON_MODE; then
# Build JSON array of documents
if has_jq; then
if [[ ${#docs[@]} -eq 0 ]]; then
json_docs="[]"
else
json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
fi
jq -cn \
--arg feature_dir "$FEATURE_DIR" \
--argjson docs "$json_docs" \
'{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'
else
if [[ ${#docs[@]} -eq 0 ]]; then
json_docs="[]"
else
json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
json_docs="[${json_docs%,}]"
fi
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"
fi
else
# Text output
echo "FEATURE_DIR:$FEATURE_DIR"
echo "AVAILABLE_DOCS:"
# Show status of each potential document
check_file "$RESEARCH" "research.md"
check_file "$DATA_MODEL" "data-model.md"
check_dir "$CONTRACTS_DIR" "contracts/"
check_file "$QUICKSTART" "quickstart.md"
if $INCLUDE_TASKS; then
check_file "$TASKS" "tasks.md"
fi
fi
+329
View File
@@ -0,0 +1,329 @@
#!/usr/bin/env bash
# Common functions and variables for all scripts
# Find repository root by searching upward for .specify directory
# This is the primary marker for spec-kit projects
find_specify_root() {
local dir="${1:-$(pwd)}"
# Normalize to absolute path to prevent infinite loop with relative paths
# Use -- to handle paths starting with - (e.g., -P, -L)
dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1
local prev_dir=""
while true; do
if [ -d "$dir/.specify" ]; then
echo "$dir"
return 0
fi
# Stop if we've reached filesystem root or dirname stops changing
if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then
break
fi
prev_dir="$dir"
dir="$(dirname "$dir")"
done
return 1
}
# Get repository root, prioritizing .specify directory over git
# This prevents using a parent git repo when spec-kit is initialized in a subdirectory
get_repo_root() {
# First, look for .specify directory (spec-kit's own marker)
local specify_root
if specify_root=$(find_specify_root); then
echo "$specify_root"
return
fi
# Fallback to git if no .specify found
if git rev-parse --show-toplevel >/dev/null 2>&1; then
git rev-parse --show-toplevel
return
fi
# Final fallback to script location for non-git repos
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
(cd "$script_dir/../../.." && pwd)
}
# Get current branch, with fallback for non-git repositories
get_current_branch() {
# First check if SPECIFY_FEATURE environment variable is set
if [[ -n ${SPECIFY_FEATURE-} ]]; then
echo "$SPECIFY_FEATURE"
return
fi
# Then check git if available at the spec-kit root (not parent)
local repo_root=$(get_repo_root)
if has_git; then
git -C "$repo_root" rev-parse --abbrev-ref HEAD
return
fi
# For non-git repos, try to find the latest feature directory
local specs_dir="$repo_root/specs"
if [[ -d $specs_dir ]]; then
local latest_feature=""
local highest=0
local latest_timestamp=""
for dir in "$specs_dir"/*; do
if [[ -d $dir ]]; then
local dirname=$(basename "$dir")
if [[ $dirname =~ ^([0-9]{8}-[0-9]{6})- ]]; then
# Timestamp-based branch: compare lexicographically
local ts="${BASH_REMATCH[1]}"
if [[ $ts > $latest_timestamp ]]; then
latest_timestamp="$ts"
latest_feature=$dirname
fi
elif [[ $dirname =~ ^([0-9]{3})- ]]; then
local number=${BASH_REMATCH[1]}
number=$((10#$number))
if [[ $number -gt $highest ]]; then
highest=$number
# Only update if no timestamp branch found yet
if [[ -z $latest_timestamp ]]; then
latest_feature=$dirname
fi
fi
fi
fi
done
if [[ -n $latest_feature ]]; then
echo "$latest_feature"
return
fi
fi
echo "main" # Final fallback
}
# Check if we have git available at the spec-kit root level
# Returns true only if git is installed and the repo root is inside a git work tree
# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
has_git() {
# First check if git command is available (before calling get_repo_root which may use git)
command -v git >/dev/null 2>&1 || return 1
local repo_root=$(get_repo_root)
# Check if .git exists (directory or file for worktrees/submodules)
[ -e "$repo_root/.git" ] || return 1
# Verify it's actually a valid git work tree
git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1
}
check_feature_branch() {
local branch="$1"
local has_git_repo="$2"
# For non-git repos, we can't enforce branch naming but still provide output
if [[ $has_git_repo != "true" ]]; then
echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
return 0
fi
if [[ ! $branch =~ ^[0-9]{3}- ]] && [[ ! $branch =~ ^[0-9]{8}-[0-9]{6}- ]]; then
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2
return 1
fi
return 0
}
get_feature_dir() { echo "$1/specs/$2"; }
# Find feature directory by numeric prefix instead of exact branch match
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
find_feature_dir_by_prefix() {
local repo_root="$1"
local branch_name="$2"
local specs_dir="$repo_root/specs"
# Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches)
local prefix=""
if [[ $branch_name =~ ^([0-9]{8}-[0-9]{6})- ]]; then
prefix="${BASH_REMATCH[1]}"
elif [[ $branch_name =~ ^([0-9]{3})- ]]; then
prefix="${BASH_REMATCH[1]}"
else
# If branch doesn't have a recognized prefix, fall back to exact match
echo "$specs_dir/$branch_name"
return
fi
# Search for directories in specs/ that start with this prefix
local matches=()
if [[ -d $specs_dir ]]; then
for dir in "$specs_dir"/"$prefix"-*; do
if [[ -d $dir ]]; then
matches+=("$(basename "$dir")")
fi
done
fi
# Handle results
if [[ ${#matches[@]} -eq 0 ]]; then
# No match found - return the branch name path (will fail later with clear error)
echo "$specs_dir/$branch_name"
elif [[ ${#matches[@]} -eq 1 ]]; then
# Exactly one match - perfect!
echo "$specs_dir/${matches[0]}"
else
# Multiple matches - this shouldn't happen with proper naming convention
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
echo "Please ensure only one spec directory exists per prefix." >&2
return 1
fi
}
get_feature_paths() {
local repo_root=$(get_repo_root)
local current_branch=$(get_current_branch)
local has_git_repo="false"
if has_git; then
has_git_repo="true"
fi
# Use prefix-based lookup to support multiple branches per spec
local feature_dir
if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
echo "ERROR: Failed to resolve feature directory" >&2
return 1
fi
# Use printf '%q' to safely quote values, preventing shell injection
# via crafted branch names or paths containing special characters
printf 'REPO_ROOT=%q\n' "$repo_root"
printf 'CURRENT_BRANCH=%q\n' "$current_branch"
printf 'HAS_GIT=%q\n' "$has_git_repo"
printf 'FEATURE_DIR=%q\n' "$feature_dir"
printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
printf 'TASKS=%q\n' "$feature_dir/tasks.md"
printf 'RESEARCH=%q\n' "$feature_dir/research.md"
printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
}
# Check if jq is available for safe JSON construction
has_jq() {
command -v jq >/dev/null 2>&1
}
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259).
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/\\r}"
s="${s//$'\b'/\\b}"
s="${s//$'\f'/\\f}"
# Escape any remaining U+0001-U+001F control characters as \uXXXX.
# (U+0000/NUL cannot appear in bash strings and is excluded.)
# LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes,
# so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact.
local LC_ALL=C
local i char code
for ((i = 0; i < ${#s}; i++)); do
char="${s:i:1}"
printf -v code '%d' "'$char" 2>/dev/null || code=256
if ((code >= 1 && code <= 31)); then
printf '\\u%04x' "$code"
else
printf '%s' "$char"
fi
done
}
check_file() { [[ -f $1 ]] && echo " ✓ $2" || echo " ✗ $2"; }
check_dir() { [[ -d $1 && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
# 3. .specify/extensions/<ext-id>/templates/
# 4. .specify/templates/ (core)
resolve_template() {
local template_name="$1"
local repo_root="$2"
local base="$repo_root/.specify/templates"
# Priority 1: Project overrides
local override="$base/overrides/${template_name}.md"
[ -f "$override" ] && echo "$override" && return 0
# Priority 2: Installed presets (sorted by priority from .registry)
local presets_dir="$repo_root/.specify/presets"
if [ -d "$presets_dir" ]; then
local registry_file="$presets_dir/.registry"
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
# Read preset IDs sorted by priority (lower number = higher precedence).
# The python3 call is wrapped in an if-condition so that set -e does not
# abort the function when python3 exits non-zero (e.g. invalid JSON).
local sorted_presets=""
if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
import json, sys, os
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null); then
if [ -n "$sorted_presets" ]; then
# python3 succeeded and returned preset IDs — search in priority order
while IFS= read -r preset_id; do
local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done <<<"$sorted_presets"
fi
# python3 succeeded but registry has no presets — nothing to search
else
# python3 failed (missing, or registry parse error) — fall back to unordered directory scan
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi
else
# Fallback: alphabetical directory order (no python3 available)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi
fi
# Priority 3: Extension-provided templates
local ext_dir="$repo_root/.specify/extensions"
if [ -d "$ext_dir" ]; then
for ext in "$ext_dir"/*/; do
[ -d "$ext" ] || continue
# Skip hidden directories (e.g. .backup, .cache)
case "$(basename "$ext")" in .*) continue ;; esac
local candidate="$ext/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi
# Priority 4: Core templates
local core="$base/${template_name}.md"
[ -f "$core" ] && echo "$core" && return 0
# Template not found in any location.
# Return 1 so callers can distinguish "not found" from "found".
# Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true
return 1
}
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env bash
set -e
JSON_MODE=false
SHORT_NAME=""
BRANCH_NUMBER=""
USE_TIMESTAMP=false
ARGS=()
i=1
while [ $i -le $# ]; do
arg="${!i}"
case "$arg" in
--json)
JSON_MODE=true
;;
--short-name)
if [ $((i + 1)) -gt $# ]; then
echo 'Error: --short-name requires a value' >&2
exit 1
fi
i=$((i + 1))
next_arg="${!i}"
# Check if the next argument is another option (starts with --)
if [[ $next_arg == --* ]]; then
echo 'Error: --short-name requires a value' >&2
exit 1
fi
SHORT_NAME="$next_arg"
;;
--number)
if [ $((i + 1)) -gt $# ]; then
echo 'Error: --number requires a value' >&2
exit 1
fi
i=$((i + 1))
next_arg="${!i}"
if [[ $next_arg == --* ]]; then
echo 'Error: --number requires a value' >&2
exit 1
fi
BRANCH_NUMBER="$next_arg"
;;
--timestamp)
USE_TIMESTAMP=true
;;
--help | -h)
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
echo ""
echo "Options:"
echo " --json Output in JSON format"
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
echo " --number N Specify branch number manually (overrides auto-detection)"
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
echo " --help, -h Show this help message"
echo ""
echo "Examples:"
echo " $0 'Add user authentication system' --short-name 'user-auth'"
echo " $0 'Implement OAuth2 integration for API' --number 5"
echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
exit 0
;;
*)
ARGS+=("$arg")
;;
esac
i=$((i + 1))
done
FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
exit 1
fi
# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
exit 1
fi
# Function to get highest number from specs directory
get_highest_from_specs() {
local specs_dir="$1"
local highest=0
if [ -d "$specs_dir" ]; then
for dir in "$specs_dir"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
# Only match sequential prefixes (###-*), skip timestamp dirs
if echo "$dirname" | grep -q '^[0-9]\{3\}-'; then
number=$(echo "$dirname" | grep -o '^[0-9]\{3\}')
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
fi
done
fi
echo "$highest"
}
# Function to get highest number from git branches
get_highest_from_branches() {
local highest=0
# Get all branches (local and remote)
branches=$(git branch -a 2>/dev/null || echo "")
if [ -n "$branches" ]; then
while IFS= read -r branch; do
# Clean branch name: remove leading markers and remote prefixes
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
# Extract feature number if branch matches pattern ###-*
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
fi
done <<<"$branches"
fi
echo "$highest"
}
# Function to check existing branches (local and remote) and return next available number
check_existing_branches() {
local specs_dir="$1"
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
git fetch --all --prune >/dev/null 2>&1 || true
# Get highest number from ALL branches (not just matching short name)
local highest_branch=$(get_highest_from_branches)
# Get highest number from ALL specs (not just matching short name)
local highest_spec=$(get_highest_from_specs "$specs_dir")
# Take the maximum of both
local max_num=$highest_branch
if [ "$highest_spec" -gt "$max_num" ]; then
max_num=$highest_spec
fi
# Return next number
echo $((max_num + 1))
}
# Function to clean and format a branch name
clean_branch_name() {
local name="$1"
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
}
# Resolve repository root using common.sh functions which prioritize .specify over git
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
REPO_ROOT=$(get_repo_root)
# Check if git is available at this repo root (not a parent)
if has_git; then
HAS_GIT=true
else
HAS_GIT=false
fi
cd "$REPO_ROOT"
SPECS_DIR="$REPO_ROOT/specs"
mkdir -p "$SPECS_DIR"
# Function to generate branch name with stop word filtering and length filtering
generate_branch_name() {
local description="$1"
# Common stop words to filter out
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
# Convert to lowercase and split into words
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
local meaningful_words=()
for word in $clean_name; do
# Skip empty words
[ -z "$word" ] && continue
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
if ! echo "$word" | grep -qiE "$stop_words"; then
if [ ${#word} -ge 3 ]; then
meaningful_words+=("$word")
elif echo "$description" | grep -q "\b${word^^}\b"; then
# Keep short words if they appear as uppercase in original (likely acronyms)
meaningful_words+=("$word")
fi
fi
done
# If we have meaningful words, use first 3-4 of them
if [ ${#meaningful_words[@]} -gt 0 ]; then
local max_words=3
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
local result=""
local count=0
for word in "${meaningful_words[@]}"; do
if [ $count -ge $max_words ]; then break; fi
if [ -n "$result" ]; then result="$result-"; fi
result="$result$word"
count=$((count + 1))
done
echo "$result"
else
# Fallback to original logic if no meaningful words found
local cleaned=$(clean_branch_name "$description")
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
fi
}
# Generate branch name
if [ -n "$SHORT_NAME" ]; then
# Use provided short name, just clean it up
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
else
# Generate from description with smart filtering
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
fi
# Warn if --number and --timestamp are both specified
if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
echo >&2 "[specify] Warning: --number is ignored when --timestamp is used"
BRANCH_NUMBER=""
fi
# Determine branch prefix
if [ "$USE_TIMESTAMP" = true ]; then
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
else
# Determine branch number
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$HAS_GIT" = true ]; then
# Check existing branches on remotes
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
else
# Fall back to local directory check
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
BRANCH_NUMBER=$((HIGHEST + 1))
fi
fi
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
fi
# GitHub enforces a 244-byte limit on branch names
# Validate and truncate if necessary
MAX_BRANCH_LENGTH=244
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
# Calculate how much we need to trim from suffix
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
PREFIX_LENGTH=$((${#FEATURE_NUM} + 1))
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))
# Truncate suffix at word boundary if possible
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
# Remove trailing hyphen if truncation created one
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
echo >&2 "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
echo >&2 "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
echo >&2 "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
fi
if [ "$HAS_GIT" = true ]; then
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
# Check if branch already exists
if git branch --list "$BRANCH_NAME" | grep -q .; then
if [ "$USE_TIMESTAMP" = true ]; then
echo >&2 "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
else
echo >&2 "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
fi
exit 1
else
echo >&2 "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
exit 1
fi
fi
else
echo >&2 "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
fi
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
mkdir -p "$FEATURE_DIR"
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
SPEC_FILE="$FEATURE_DIR/spec.md"
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
cp "$TEMPLATE" "$SPEC_FILE"
else
echo "Warning: Spec template not found; created empty spec file" >&2
touch "$SPEC_FILE"
fi
# Inform the user how to persist the feature variable in their own shell
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
if $JSON_MODE; then
if command -v jq >/dev/null 2>&1; then
jq -cn \
--arg branch_name "$BRANCH_NAME" \
--arg spec_file "$SPEC_FILE" \
--arg feature_num "$FEATURE_NUM" \
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
else
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
fi
else
echo "BRANCH_NAME: $BRANCH_NAME"
echo "SPEC_FILE: $SPEC_FILE"
echo "FEATURE_NUM: $FEATURE_NUM"
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
fi
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -e
# Parse command line arguments
JSON_MODE=false
ARGS=()
for arg in "$@"; do
case "$arg" in
--json)
JSON_MODE=true
;;
--help | -h)
echo "Usage: $0 [--json]"
echo " --json Output results in JSON format"
echo " --help Show this help message"
exit 0
;;
*)
ARGS+=("$arg")
;;
esac
done
# Get script directory and load common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions
_paths_output=$(get_feature_paths) || {
echo "ERROR: Failed to resolve feature paths" >&2
exit 1
}
eval "$_paths_output"
unset _paths_output
# Check if we're on a proper feature branch (only for git repos)
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
# Ensure the feature directory exists
mkdir -p "$FEATURE_DIR"
# Copy plan template if it exists
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true
if [[ -n $TEMPLATE ]] && [[ -f $TEMPLATE ]]; then
cp "$TEMPLATE" "$IMPL_PLAN"
echo "Copied plan template to $IMPL_PLAN"
else
echo "Warning: Plan template not found"
# Create a basic plan file if template doesn't exist
touch "$IMPL_PLAN"
fi
# Output results
if $JSON_MODE; then
if has_jq; then
jq -cn \
--arg feature_spec "$FEATURE_SPEC" \
--arg impl_plan "$IMPL_PLAN" \
--arg specs_dir "$FEATURE_DIR" \
--arg branch "$CURRENT_BRANCH" \
--arg has_git "$HAS_GIT" \
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'
else
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"
fi
else
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"
echo "SPECS_DIR: $FEATURE_DIR"
echo "BRANCH: $CURRENT_BRANCH"
echo "HAS_GIT: $HAS_GIT"
fi
+840
View File
@@ -0,0 +1,840 @@
#!/usr/bin/env bash
# Update agent context files with information from plan.md
#
# This script maintains AI agent context files by parsing feature specifications
# and updating agent-specific configuration files with project information.
#
# MAIN FUNCTIONS:
# 1. Environment Validation
# - Verifies git repository structure and branch information
# - Checks for required plan.md files and templates
# - Validates file permissions and accessibility
#
# 2. Plan Data Extraction
# - Parses plan.md files to extract project metadata
# - Identifies language/version, frameworks, databases, and project types
# - Handles missing or incomplete specification data gracefully
#
# 3. Agent File Management
# - Creates new agent context files from templates when needed
# - Updates existing agent files with new project information
# - Preserves manual additions and custom configurations
# - Supports multiple AI agent formats and directory structures
#
# 4. Content Generation
# - Generates language-specific build/test commands
# - Creates appropriate project directory structures
# - Updates technology stacks and recent changes sections
# - Maintains consistent formatting and timestamps
#
# 5. Multi-Agent Support
# - Handles agent-specific file paths and naming conventions
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Antigravity or Generic
# - Can update single agents or all existing agent files
# - Creates default Claude file if no agent files exist
#
# Usage: ./update-agent-context.sh [agent_type]
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic
# Leave empty to update all existing agent files
set -e
# Enable strict error handling
set -u
set -o pipefail
#==============================================================================
# Configuration and Global Variables
#==============================================================================
# Get script directory and load common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions
_paths_output=$(get_feature_paths) || {
echo "ERROR: Failed to resolve feature paths" >&2
exit 1
}
eval "$_paths_output"
unset _paths_output
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
AGENT_TYPE="${1-}"
# Agent-specific file paths
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
QWEN_FILE="$REPO_ROOT/QWEN.md"
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
JUNIE_FILE="$REPO_ROOT/.junie/AGENTS.md"
KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
QODER_FILE="$REPO_ROOT/QODER.md"
# Amp, Kiro CLI, IBM Bob, and Pi all share AGENTS.md — use AGENTS_FILE to avoid
# updating the same file multiple times.
AMP_FILE="$AGENTS_FILE"
SHAI_FILE="$REPO_ROOT/SHAI.md"
TABNINE_FILE="$REPO_ROOT/TABNINE.md"
KIRO_FILE="$AGENTS_FILE"
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
BOB_FILE="$AGENTS_FILE"
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
KIMI_FILE="$REPO_ROOT/KIMI.md"
TRAE_FILE="$REPO_ROOT/.trae/rules/AGENTS.md"
IFLOW_FILE="$REPO_ROOT/IFLOW.md"
# Template file
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
# Global variables for parsed plan data
NEW_LANG=""
NEW_FRAMEWORK=""
NEW_DB=""
NEW_PROJECT_TYPE=""
#==============================================================================
# Utility Functions
#==============================================================================
log_info() {
echo "INFO: $1"
}
log_success() {
echo "✓ $1"
}
log_error() {
echo "ERROR: $1" >&2
}
log_warning() {
echo "WARNING: $1" >&2
}
# Cleanup function for temporary files
cleanup() {
local exit_code=$?
# Disarm traps to prevent re-entrant loop
trap - EXIT INT TERM
rm -f /tmp/agent_update_*_$$
rm -f /tmp/manual_additions_$$
exit $exit_code
}
# Set up cleanup trap
trap cleanup EXIT INT TERM
#==============================================================================
# Validation Functions
#==============================================================================
validate_environment() {
# Check if we have a current branch/feature (git or non-git)
if [[ -z $CURRENT_BRANCH ]]; then
log_error "Unable to determine current feature"
if [[ $HAS_GIT == "true" ]]; then
log_info "Make sure you're on a feature branch"
else
log_info "Set SPECIFY_FEATURE environment variable or create a feature first"
fi
exit 1
fi
# Check if plan.md exists
if [[ ! -f $NEW_PLAN ]]; then
log_error "No plan.md found at $NEW_PLAN"
log_info "Make sure you're working on a feature with a corresponding spec directory"
if [[ $HAS_GIT != "true" ]]; then
log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first"
fi
exit 1
fi
# Check if template exists (needed for new files)
if [[ ! -f $TEMPLATE_FILE ]]; then
log_warning "Template file not found at $TEMPLATE_FILE"
log_warning "Creating new agent files will fail"
fi
}
#==============================================================================
# Plan Parsing Functions
#==============================================================================
extract_plan_field() {
local field_pattern="$1"
local plan_file="$2"
grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null |
head -1 |
sed "s|^\*\*${field_pattern}\*\*: ||" |
sed 's/^[ \t]*//;s/[ \t]*$//' |
grep -v "NEEDS CLARIFICATION" |
grep -v "^N/A$" || echo ""
}
parse_plan_data() {
local plan_file="$1"
if [[ ! -f $plan_file ]]; then
log_error "Plan file not found: $plan_file"
return 1
fi
if [[ ! -r $plan_file ]]; then
log_error "Plan file is not readable: $plan_file"
return 1
fi
log_info "Parsing plan data from $plan_file"
NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file")
NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file")
NEW_DB=$(extract_plan_field "Storage" "$plan_file")
NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file")
# Log what we found
if [[ -n $NEW_LANG ]]; then
log_info "Found language: $NEW_LANG"
else
log_warning "No language information found in plan"
fi
if [[ -n $NEW_FRAMEWORK ]]; then
log_info "Found framework: $NEW_FRAMEWORK"
fi
if [[ -n $NEW_DB ]] && [[ $NEW_DB != "N/A" ]]; then
log_info "Found database: $NEW_DB"
fi
if [[ -n $NEW_PROJECT_TYPE ]]; then
log_info "Found project type: $NEW_PROJECT_TYPE"
fi
}
format_technology_stack() {
local lang="$1"
local framework="$2"
local parts=()
# Add non-empty parts
[[ -n $lang && $lang != "NEEDS CLARIFICATION" ]] && parts+=("$lang")
[[ -n $framework && $framework != "NEEDS CLARIFICATION" && $framework != "N/A" ]] && parts+=("$framework")
# Join with proper formatting
if [[ ${#parts[@]} -eq 0 ]]; then
echo ""
elif [[ ${#parts[@]} -eq 1 ]]; then
echo "${parts[0]}"
else
# Join multiple parts with " + "
local result="${parts[0]}"
for ((i = 1; i < ${#parts[@]}; i++)); do
result="$result + ${parts[i]}"
done
echo "$result"
fi
}
#==============================================================================
# Template and Content Generation Functions
#==============================================================================
get_project_structure() {
local project_type="$1"
if [[ $project_type == *"web"* ]]; then
echo 'backend/\nfrontend/\ntests/'
else
echo 'src/\ntests/'
fi
}
get_commands_for_language() {
local lang="$1"
case "$lang" in
*"Python"*)
echo "cd src && pytest && ruff check ."
;;
*"Rust"*)
echo "cargo test && cargo clippy"
;;
*"JavaScript"* | *"TypeScript"*)
echo 'npm test \&\& npm run lint'
;;
*)
echo "# Add commands for $lang"
;;
esac
}
get_language_conventions() {
local lang="$1"
echo "$lang: Follow standard conventions"
}
create_new_agent_file() {
local target_file="$1"
local temp_file="$2"
local project_name="$3"
local current_date="$4"
if [[ ! -f $TEMPLATE_FILE ]]; then
log_error "Template not found at $TEMPLATE_FILE"
return 1
fi
if [[ ! -r $TEMPLATE_FILE ]]; then
log_error "Template file is not readable: $TEMPLATE_FILE"
return 1
fi
log_info "Creating new agent context file from template..."
if ! cp "$TEMPLATE_FILE" "$temp_file"; then
log_error "Failed to copy template file"
return 1
fi
# Replace template placeholders
local project_structure
project_structure=$(get_project_structure "$NEW_PROJECT_TYPE")
local commands
commands=$(get_commands_for_language "$NEW_LANG")
local language_conventions
language_conventions=$(get_language_conventions "$NEW_LANG")
# Perform substitutions with error checking using safer approach
# Escape special characters for sed by using a different delimiter or escaping
local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g')
local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g')
local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g')
# Build technology stack and recent change strings conditionally
local tech_stack
if [[ -n $escaped_lang && -n $escaped_framework ]]; then
tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)"
elif [[ -n $escaped_lang ]]; then
tech_stack="- $escaped_lang ($escaped_branch)"
elif [[ -n $escaped_framework ]]; then
tech_stack="- $escaped_framework ($escaped_branch)"
else
tech_stack="- ($escaped_branch)"
fi
local recent_change
if [[ -n $escaped_lang && -n $escaped_framework ]]; then
recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework"
elif [[ -n $escaped_lang ]]; then
recent_change="- $escaped_branch: Added $escaped_lang"
elif [[ -n $escaped_framework ]]; then
recent_change="- $escaped_branch: Added $escaped_framework"
else
recent_change="- $escaped_branch: Added"
fi
local substitutions=(
"s|\[PROJECT NAME\]|$project_name|"
"s|\[DATE\]|$current_date|"
"s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|"
"s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g"
"s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|"
"s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|"
"s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|"
)
for substitution in "${substitutions[@]}"; do
if ! sed -i.bak -e "$substitution" "$temp_file"; then
log_error "Failed to perform substitution: $substitution"
rm -f "$temp_file" "$temp_file.bak"
return 1
fi
done
# Convert \n sequences to actual newlines
newline=$(printf '\n')
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
# Clean up backup files
rm -f "$temp_file.bak" "$temp_file.bak2"
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
if [[ $target_file == *.mdc ]]; then
local frontmatter_file
frontmatter_file=$(mktemp) || return 1
printf '%s\n' "---" "description: Project Development Guidelines" 'globs: ["**/*"]' "alwaysApply: true" "---" "" >"$frontmatter_file"
cat "$temp_file" >>"$frontmatter_file"
mv "$frontmatter_file" "$temp_file"
fi
return 0
}
update_existing_agent_file() {
local target_file="$1"
local current_date="$2"
log_info "Updating existing agent context file..."
# Use a single temporary file for atomic update
local temp_file
temp_file=$(mktemp) || {
log_error "Failed to create temporary file"
return 1
}
# Process the file in one pass
local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK")
local new_tech_entries=()
local new_change_entry=""
# Prepare new technology entries
if [[ -n $tech_stack ]] && ! grep -q "$tech_stack" "$target_file"; then
new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)")
fi
if [[ -n $NEW_DB ]] && [[ $NEW_DB != "N/A" ]] && [[ $NEW_DB != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then
new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)")
fi
# Prepare new change entry
if [[ -n $tech_stack ]]; then
new_change_entry="- $CURRENT_BRANCH: Added $tech_stack"
elif [[ -n $NEW_DB ]] && [[ $NEW_DB != "N/A" ]] && [[ $NEW_DB != "NEEDS CLARIFICATION" ]]; then
new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"
fi
# Check if sections exist in the file
local has_active_technologies=0
local has_recent_changes=0
if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then
has_active_technologies=1
fi
if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then
has_recent_changes=1
fi
# Process file line by line
local in_tech_section=false
local in_changes_section=false
local tech_entries_added=false
local changes_entries_added=false
local existing_changes_count=0
local file_ended=false
while IFS= read -r line || [[ -n $line ]]; do
# Handle Active Technologies section
if [[ $line == "## Active Technologies" ]]; then
echo "$line" >>"$temp_file"
in_tech_section=true
continue
elif [[ $in_tech_section == true ]] && [[ $line =~ ^##[[:space:]] ]]; then
# Add new tech entries before closing the section
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >>"$temp_file"
tech_entries_added=true
fi
echo "$line" >>"$temp_file"
in_tech_section=false
continue
elif [[ $in_tech_section == true ]] && [[ -z $line ]]; then
# Add new tech entries before empty line in tech section
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >>"$temp_file"
tech_entries_added=true
fi
echo "$line" >>"$temp_file"
continue
fi
# Handle Recent Changes section
if [[ $line == "## Recent Changes" ]]; then
echo "$line" >>"$temp_file"
# Add new change entry right after the heading
if [[ -n $new_change_entry ]]; then
echo "$new_change_entry" >>"$temp_file"
fi
in_changes_section=true
changes_entries_added=true
continue
elif [[ $in_changes_section == true ]] && [[ $line =~ ^##[[:space:]] ]]; then
echo "$line" >>"$temp_file"
in_changes_section=false
continue
elif [[ $in_changes_section == true ]] && [[ $line == "- "* ]]; then
# Keep only first 2 existing changes
if [[ $existing_changes_count -lt 2 ]]; then
echo "$line" >>"$temp_file"
((existing_changes_count++))
fi
continue
fi
# Update timestamp
if [[ $line =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >>"$temp_file"
else
echo "$line" >>"$temp_file"
fi
done <"$target_file"
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >>"$temp_file"
tech_entries_added=true
fi
# If sections don't exist, add them at the end of the file
if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
echo "" >>"$temp_file"
echo "## Active Technologies" >>"$temp_file"
printf '%s\n' "${new_tech_entries[@]}" >>"$temp_file"
tech_entries_added=true
fi
if [[ $has_recent_changes -eq 0 ]] && [[ -n $new_change_entry ]]; then
echo "" >>"$temp_file"
echo "## Recent Changes" >>"$temp_file"
echo "$new_change_entry" >>"$temp_file"
changes_entries_added=true
fi
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
if [[ $target_file == *.mdc ]]; then
if ! head -1 "$temp_file" | grep -q '^---'; then
local frontmatter_file
frontmatter_file=$(mktemp) || {
rm -f "$temp_file"
return 1
}
printf '%s\n' "---" "description: Project Development Guidelines" 'globs: ["**/*"]' "alwaysApply: true" "---" "" >"$frontmatter_file"
cat "$temp_file" >>"$frontmatter_file"
mv "$frontmatter_file" "$temp_file"
fi
fi
# Move temp file to target atomically
if ! mv "$temp_file" "$target_file"; then
log_error "Failed to update target file"
rm -f "$temp_file"
return 1
fi
return 0
}
#==============================================================================
# Main Agent File Update Function
#==============================================================================
update_agent_file() {
local target_file="$1"
local agent_name="$2"
if [[ -z $target_file ]] || [[ -z $agent_name ]]; then
log_error "update_agent_file requires target_file and agent_name parameters"
return 1
fi
log_info "Updating $agent_name context file: $target_file"
local project_name
project_name=$(basename "$REPO_ROOT")
local current_date
current_date=$(date +%Y-%m-%d)
# Create directory if it doesn't exist
local target_dir
target_dir=$(dirname "$target_file")
if [[ ! -d $target_dir ]]; then
if ! mkdir -p "$target_dir"; then
log_error "Failed to create directory: $target_dir"
return 1
fi
fi
if [[ ! -f $target_file ]]; then
# Create new file from template
local temp_file
temp_file=$(mktemp) || {
log_error "Failed to create temporary file"
return 1
}
if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then
if mv "$temp_file" "$target_file"; then
log_success "Created new $agent_name context file"
else
log_error "Failed to move temporary file to $target_file"
rm -f "$temp_file"
return 1
fi
else
log_error "Failed to create new agent file"
rm -f "$temp_file"
return 1
fi
else
# Update existing file
if [[ ! -r $target_file ]]; then
log_error "Cannot read existing file: $target_file"
return 1
fi
if [[ ! -w $target_file ]]; then
log_error "Cannot write to existing file: $target_file"
return 1
fi
if update_existing_agent_file "$target_file" "$current_date"; then
log_success "Updated existing $agent_name context file"
else
log_error "Failed to update existing agent file"
return 1
fi
fi
return 0
}
#==============================================================================
# Agent Selection and Processing
#==============================================================================
update_specific_agent() {
local agent_type="$1"
case "$agent_type" in
claude)
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
;;
gemini)
update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1
;;
copilot)
update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1
;;
cursor-agent)
update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1
;;
qwen)
update_agent_file "$QWEN_FILE" "Qwen Code" || return 1
;;
opencode)
update_agent_file "$AGENTS_FILE" "opencode" || return 1
;;
codex)
update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1
;;
windsurf)
update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1
;;
junie)
update_agent_file "$JUNIE_FILE" "Junie" || return 1
;;
kilocode)
update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1
;;
auggie)
update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1
;;
roo)
update_agent_file "$ROO_FILE" "Roo Code" || return 1
;;
codebuddy)
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1
;;
qodercli)
update_agent_file "$QODER_FILE" "Qoder CLI" || return 1
;;
amp)
update_agent_file "$AMP_FILE" "Amp" || return 1
;;
shai)
update_agent_file "$SHAI_FILE" "SHAI" || return 1
;;
tabnine)
update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1
;;
kiro-cli)
update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1
;;
agy)
update_agent_file "$AGY_FILE" "Antigravity" || return 1
;;
bob)
update_agent_file "$BOB_FILE" "IBM Bob" || return 1
;;
vibe)
update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1
;;
kimi)
update_agent_file "$KIMI_FILE" "Kimi Code" || return 1
;;
trae)
update_agent_file "$TRAE_FILE" "Trae" || return 1
;;
pi)
update_agent_file "$AGENTS_FILE" "Pi Coding Agent" || return 1
;;
iflow)
update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1
;;
generic)
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
;;
*)
log_error "Unknown agent type '$agent_type'"
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic"
exit 1
;;
esac
}
# Helper: skip non-existent files and files already updated (dedup by
# realpath so that variables pointing to the same file — e.g. AMP_FILE,
# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once).
# Uses a linear array instead of associative array for bash 3.2 compatibility.
# Note: defined at top level because bash 3.2 does not support true
# nested/local functions. _updated_paths, _found_agent, and _all_ok are
# initialised exclusively inside update_all_existing_agents so that
# sourcing this script has no side effects on the caller's environment.
_update_if_new() {
local file="$1" name="$2"
[[ -f $file ]] || return 0
local real_path
real_path=$(realpath "$file" 2>/dev/null || echo "$file")
local p
if [[ ${#_updated_paths[@]} -gt 0 ]]; then
for p in "${_updated_paths[@]}"; do
[[ $p == "$real_path" ]] && return 0
done
fi
# Record the file as seen before attempting the update so that:
# (a) aliases pointing to the same path are not retried on failure
# (b) _found_agent reflects file existence, not update success
_updated_paths+=("$real_path")
_found_agent=true
update_agent_file "$file" "$name"
}
update_all_existing_agents() {
_found_agent=false
_updated_paths=()
local _all_ok=true
_update_if_new "$CLAUDE_FILE" "Claude Code" || _all_ok=false
_update_if_new "$GEMINI_FILE" "Gemini CLI" || _all_ok=false
_update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false
_update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false
_update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false
_update_if_new "$AGENTS_FILE" "Codex/opencode" || _all_ok=false
_update_if_new "$AMP_FILE" "Amp" || _all_ok=false
_update_if_new "$KIRO_FILE" "Kiro CLI" || _all_ok=false
_update_if_new "$BOB_FILE" "IBM Bob" || _all_ok=false
_update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false
_update_if_new "$JUNIE_FILE" "Junie" || _all_ok=false
_update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false
_update_if_new "$AUGGIE_FILE" "Auggie CLI" || _all_ok=false
_update_if_new "$ROO_FILE" "Roo Code" || _all_ok=false
_update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI" || _all_ok=false
_update_if_new "$SHAI_FILE" "SHAI" || _all_ok=false
_update_if_new "$TABNINE_FILE" "Tabnine CLI" || _all_ok=false
_update_if_new "$QODER_FILE" "Qoder CLI" || _all_ok=false
_update_if_new "$AGY_FILE" "Antigravity" || _all_ok=false
_update_if_new "$VIBE_FILE" "Mistral Vibe" || _all_ok=false
_update_if_new "$KIMI_FILE" "Kimi Code" || _all_ok=false
_update_if_new "$TRAE_FILE" "Trae" || _all_ok=false
_update_if_new "$IFLOW_FILE" "iFlow CLI" || _all_ok=false
# If no agent files exist, create a default Claude file
if [[ $_found_agent == false ]]; then
log_info "No existing agent files found, creating default Claude file..."
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
fi
[[ $_all_ok == true ]]
}
print_summary() {
echo
log_info "Summary of changes:"
if [[ -n $NEW_LANG ]]; then
echo " - Added language: $NEW_LANG"
fi
if [[ -n $NEW_FRAMEWORK ]]; then
echo " - Added framework: $NEW_FRAMEWORK"
fi
if [[ -n $NEW_DB ]] && [[ $NEW_DB != "N/A" ]]; then
echo " - Added database: $NEW_DB"
fi
echo
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]"
}
#==============================================================================
# Main Execution
#==============================================================================
main() {
# Validate environment before proceeding
validate_environment
log_info "=== Updating agent context files for feature $CURRENT_BRANCH ==="
# Parse the plan file to extract project information
if ! parse_plan_data "$NEW_PLAN"; then
log_error "Failed to parse plan data"
exit 1
fi
# Process based on agent type argument
local success=true
if [[ -z $AGENT_TYPE ]]; then
# No specific agent provided - update all existing agent files
log_info "No agent specified, updating all existing agent files..."
if ! update_all_existing_agents; then
success=false
fi
else
# Specific agent provided - update only that agent
log_info "Updating specific agent: $AGENT_TYPE"
if ! update_specific_agent "$AGENT_TYPE"; then
success=false
fi
fi
# Print summary
print_summary
if [[ $success == true ]]; then
log_success "Agent context update completed successfully"
exit 0
else
log_error "Agent context update completed with errors"
exit 1
fi
}
# Execute main function if script is run directly
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
main "$@"
fi
+28
View File
@@ -0,0 +1,28 @@
# [PROJECT NAME] Development Guidelines
Auto-generated from all feature plans. Last updated: [DATE]
## Active Technologies
[EXTRACTED FROM ALL PLAN.MD FILES]
## Project Structure
```text
[ACTUAL STRUCTURE FROM PLANS]
```
## Commands
[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]
## Code Style
[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]
## Recent Changes
[LAST 3 FEATURES AND WHAT THEY ADDED]
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->
+40
View File
@@ -0,0 +1,40 @@
# [CHECKLIST TYPE] Checklist: [FEATURE NAME]
**Purpose**: [Brief description of what this checklist covers]
**Created**: [DATE]
**Feature**: [Link to spec.md or relevant documentation]
**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.
<!--
============================================================================
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
The /speckit.checklist command MUST replace these with actual items based on:
- User's specific checklist request
- Feature requirements from spec.md
- Technical context from plan.md
- Implementation details from tasks.md
DO NOT keep these sample items in the generated checklist file.
============================================================================
-->
## [Category 1]
- [ ] CHK001 First checklist item with clear action
- [ ] CHK002 Second checklist item
- [ ] CHK003 Third checklist item
## [Category 2]
- [ ] CHK004 Another category item
- [ ] CHK005 Item with specific criteria
- [ ] CHK006 Final item in this category
## Notes
- Check items off as completed: `[x]`
- Add comments or findings inline
- Link to relevant resources or documentation
- Items are numbered sequentially for easy reference
@@ -0,0 +1,73 @@
# [PROJECT_NAME] Constitution
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
## Core Principles
### [PRINCIPLE_1_NAME]
<!-- Example: I. Library-First -->
[PRINCIPLE_1_DESCRIPTION]
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
### [PRINCIPLE_2_NAME]
<!-- Example: II. CLI Interface -->
[PRINCIPLE_2_DESCRIPTION]
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
### [PRINCIPLE_3_NAME]
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
[PRINCIPLE_3_DESCRIPTION]
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
### [PRINCIPLE_4_NAME]
<!-- Example: IV. Integration Testing -->
[PRINCIPLE_4_DESCRIPTION]
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
### [PRINCIPLE_5_NAME]
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
[PRINCIPLE_5_DESCRIPTION]
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
## [SECTION_2_NAME]
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
[SECTION_2_CONTENT]
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
## [SECTION_3_NAME]
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
[SECTION_3_CONTENT]
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
## Governance
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
[GOVERNANCE_RULES]
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
+109
View File
@@ -0,0 +1,109 @@
# Implementation Plan: [FEATURE]
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
## Summary
[Extract from feature spec: primary requirement + technical approach from research]
## Technical Context
<!--
ACTION REQUIRED: Replace the content in this section with the technical details
for the project. The structure here is presented in advisory capacity to guide
the iteration process.
-->
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
## Constitution Check
_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
- Safety-critical mesh impact is identified for any routing, airtime, MQTT, channel, or packet-path change.
- Variant and platform scope are explicit, and all hardware flags or pin mappings are verified against the target board.
- Validation evidence is defined, including the exact `pio`, native test, simulator, formatting, or static checks to run.
- Resource, power, memory, and dependency impact are assessed for the affected targets.
- Any constitutional violation or validation gap is documented with justification in Complexity Tracking.
## Project Structure
### Documentation (this feature)
```text
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```
### Source Code (repository root)
<!--
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with
real paths (e.g., apps/admin, packages/something). The delivered plan must
not include Option labels.
-->
```text
# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
src/
├── models/
├── services/
├── cli/
└── lib/
tests/
├── contract/
├── integration/
└── unit/
# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
backend/
├── src/
│ ├── models/
│ ├── services/
│ └── api/
└── tests/
frontend/
├── src/
│ ├── components/
│ ├── pages/
│ └── services/
└── tests/
# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
api/
└── [same as backend above]
ios/ or android/
└── [platform-specific structure: feature modules, UI flows, platform tests]
```
**Structure Decision**: [Document the selected structure and reference the real
directories captured above]
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
| -------------------------- | ------------------ | ------------------------------------ |
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
+140
View File
@@ -0,0 +1,140 @@
# Feature Specification: [FEATURE NAME]
**Feature Branch**: `[###-feature-name]`
**Created**: [DATE]
**Status**: Draft
**Input**: User description: "$ARGUMENTS"
## User Scenarios & Testing _(mandatory)_
<!--
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
you should still have a viable MVP (Minimum Viable Product) that delivers value.
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
Think of each story as a standalone slice of functionality that can be:
- Developed independently
- Tested independently
- Deployed independently
- Demonstrated to users independently
-->
### User Story 1 - [Brief Title] (Priority: P1)
[Describe this user journey in plain language]
**Why this priority**: [Explain the value and why it has this priority level]
**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
**Acceptance Scenarios**:
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
2. **Given** [initial state], **When** [action], **Then** [expected outcome]
---
### User Story 2 - [Brief Title] (Priority: P2)
[Describe this user journey in plain language]
**Why this priority**: [Explain the value and why it has this priority level]
**Independent Test**: [Describe how this can be tested independently]
**Acceptance Scenarios**:
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
---
### User Story 3 - [Brief Title] (Priority: P3)
[Describe this user journey in plain language]
**Why this priority**: [Explain the value and why it has this priority level]
**Independent Test**: [Describe how this can be tested independently]
**Acceptance Scenarios**:
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
---
[Add more user stories as needed, each with an assigned priority]
For firmware and hardware-facing work, each story MUST identify affected platforms or variants
and describe how the behavior is validated on its own.
### Edge Cases
<!--
ACTION REQUIRED: The content in this section represents placeholders.
Fill them out with the right edge cases.
-->
- What happens when [boundary condition]?
- How does system handle [error scenario]?
- What happens when the target hardware capability is absent, misdeclared, or only present on some variants?
- How does the system behave when radio, power, timing, or memory constraints are tighter than expected?
## Requirements _(mandatory)_
<!--
ACTION REQUIRED: The content in this section represents placeholders.
Fill them out with the right functional requirements.
-->
### Functional Requirements
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
- **FR-005**: System MUST [behavior, e.g., "log all security events"]
_Example of marking unclear requirements:_
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
Where relevant, requirements MUST also state:
- affected architectures, boards, or modules
- whether behavior changes public defaults, protocol compatibility, or generated artifacts
- any required validation evidence for high-risk mesh, hardware, or power behavior
### Key Entities _(include if feature involves data)_
- **[Entity 1]**: [What it represents, key attributes without implementation]
- **[Entity 2]**: [What it represents, relationships to other entities]
## Success Criteria _(mandatory)_
<!--
ACTION REQUIRED: Define measurable success criteria.
These must be technology-agnostic and measurable.
-->
### Measurable Outcomes
- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
## Assumptions
<!--
ACTION REQUIRED: The content in this section represents placeholders.
Fill them out with the right assumptions based on reasonable defaults
chosen when the feature description did not specify certain details.
-->
- [Assumption about target users, e.g., "Users have stable internet connectivity"]
- [Assumption about scope boundaries, e.g., "Mobile support is out of scope for v1"]
- [Assumption about data/environment, e.g., "Existing authentication system will be reused"]
- [Dependency on existing system/service, e.g., "Requires access to the existing user profile API"]
- [Assumption about available hardware capabilities, board revisions, or build targets]
+259
View File
@@ -0,0 +1,259 @@
---
description: "Task list template for feature implementation"
---
# Tasks: [FEATURE NAME]
**Input**: Design documents from `/specs/[###-feature-name]/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
**Tests**: Validation tasks are REQUIRED whenever the constitution or feature risk profile demands evidence. Include targeted builds, native tests, simulator runs, protobuf regeneration checks, formatting, or other repository-standard validation appropriate to the change.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
## Path Conventions
- **Single project**: `src/`, `tests/` at repository root
- **Web app**: `backend/src/`, `frontend/src/`
- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
- Paths shown below assume single project - adjust based on plan.md structure
<!--
============================================================================
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
The /speckit.tasks command MUST replace these with actual tasks based on:
- User stories from spec.md (with their priorities P1, P2, P3...)
- Feature requirements from plan.md
- Entities from data-model.md
- Endpoints from contracts/
Tasks MUST be organized by user story so each story can be:
- Implemented independently
- Tested independently
- Delivered as an MVP increment
DO NOT keep these sample tasks in the generated tasks.md file.
============================================================================
-->
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Project initialization and basic structure
- [ ] T001 Create project structure per implementation plan
- [ ] T002 Initialize [language] project with [framework] dependencies
- [ ] T003 [P] Configure linting and formatting tools
- [ ] T00X Identify affected targets, variants, and validation commands
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
Examples of foundational tasks (adjust based on your project):
- [ ] T004 Setup database schema and migrations framework
- [ ] T005 [P] Implement authentication/authorization framework
- [ ] T006 [P] Setup API routing and middleware structure
- [ ] T007 Create base models/entities that all stories depend on
- [ ] T008 Configure error handling and logging infrastructure
- [ ] T009 Setup environment configuration management
- [ ] T00X Verify board-specific flags, pins, generated assets, or protocol prerequisites needed by all stories
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
---
## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP
**Goal**: [Brief description of what this story delivers]
**Independent Test**: [How to verify this story works on its own]
### Validation for User Story 1 ⚠️
> **NOTE: Add the smallest credible validation for the story's risk level before declaring it complete**
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test\_[name].py
- [ ] T01X [P] [US1] Run targeted build or simulation for affected platform(s)
### Implementation for User Story 1
- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
- [ ] T016 [US1] Add validation and error handling
- [ ] T017 [US1] Add logging for user story 1 operations
- [ ] T01Y [US1] Confirm variant-scoped behavior and configuration defaults remain correct
**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
---
## Phase 4: User Story 2 - [Title] (Priority: P2)
**Goal**: [Brief description of what this story delivers]
**Independent Test**: [How to verify this story works on its own]
### Validation for User Story 2 ⚠️
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test\_[name].py
- [ ] T02X [P] [US2] Run targeted build or simulation for affected platform(s)
### Implementation for User Story 2
- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
- [ ] T021 [US2] Implement [Service] in src/services/[service].py
- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
- [ ] T02Y [US2] Confirm resource, power, and compatibility impacts stay within plan constraints
**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
---
## Phase 5: User Story 3 - [Title] (Priority: P3)
**Goal**: [Brief description of what this story delivers]
**Independent Test**: [How to verify this story works on its own]
### Validation for User Story 3 ⚠️
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test\_[name].py
- [ ] T03X [P] [US3] Run targeted build or simulation for affected platform(s)
### Implementation for User Story 3
- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
- [ ] T027 [US3] Implement [Service] in src/services/[service].py
- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
- [ ] T03Y [US3] Confirm backward compatibility or explicitly document intentional behavior changes
**Checkpoint**: All user stories should now be independently functional
---
[Add more user story phases as needed, following the same pattern]
---
## Phase N: Polish & Cross-Cutting Concerns
**Purpose**: Improvements that affect multiple user stories
- [ ] TXXX [P] Documentation updates in docs/
- [ ] TXXX Code cleanup and refactoring
- [ ] TXXX Performance optimization across all stories
- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
- [ ] TXXX Security hardening
- [ ] TXXX Run quickstart.md validation
- [ ] TXXX Summarize skipped validations, migration notes, and generated-file updates for review
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies - can start immediately
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
- **User Stories (Phase 3+)**: All depend on Foundational phase completion
- User stories can then proceed in parallel (if staffed)
- Or sequentially in priority order (P1 → P2 → P3)
- **Polish (Final Phase)**: Depends on all desired user stories being complete
### User Story Dependencies
- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
### Within Each User Story
- Tests (if included) MUST be written and FAIL before implementation
- Models before services
- Services before endpoints
- Core implementation before integration
- Story complete before moving to next priority
### Parallel Opportunities
- All Setup tasks marked [P] can run in parallel
- All Foundational tasks marked [P] can run in parallel (within Phase 2)
- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
- All tests for a user story marked [P] can run in parallel
- Models within a story marked [P] can run in parallel
- Different user stories can be worked on in parallel by different team members
---
## Parallel Example: User Story 1
```bash
# Launch all tests for User Story 1 together (if tests requested):
Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
Task: "Integration test for [user journey] in tests/integration/test_[name].py"
# Launch all models for User Story 1 together:
Task: "Create [Entity1] model in src/models/[entity1].py"
Task: "Create [Entity2] model in src/models/[entity2].py"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup
2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
3. Complete Phase 3: User Story 1
4. **STOP and VALIDATE**: Test User Story 1 independently
5. Deploy/demo if ready
### Incremental Delivery
1. Complete Setup + Foundational → Foundation ready
2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
3. Add User Story 2 → Test independently → Deploy/Demo
4. Add User Story 3 → Test independently → Deploy/Demo
5. Each story adds value without breaking previous stories
### Parallel Team Strategy
With multiple developers:
1. Team completes Setup + Foundational together
2. Once Foundational is done:
- Developer A: User Story 1
- Developer B: User Story 2
- Developer C: User Story 3
3. Stories complete and integrate independently
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Each user story should be independently completable and testable
- Verify tests fail before implementing
- Commit after each task or logical group
- Stop at any checkpoint to validate story independently
- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
+3 -3
View File
@@ -4,11 +4,11 @@ cli:
plugins:
sources:
- id: trunk
ref: v1.7.6
ref: v1.8.0
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.525
- checkov@3.2.526
- renovate@43.150.0
- prettier@3.8.3
- trufflehog@3.95.2
@@ -36,7 +36,7 @@ lint:
- bin/**
runtimes:
enabled:
- python@3.10.8
- python@3.14.4
- go@1.21.0
- node@22.16.0
actions:
+11
View File
@@ -10,5 +10,16 @@
},
"[powershell]": {
"editor.defaultFormatter": "ms-vscode.powershell"
},
"chat.promptFilesRecommendations": {
"speckit.constitution": true,
"speckit.specify": true,
"speckit.plan": true,
"speckit.tasks": true,
"speckit.implement": true
},
"chat.tools.terminal.autoApprove": {
".specify/scripts/bash/": true,
".specify/scripts/powershell/": true
}
}
+27 -24
View File
@@ -10,17 +10,18 @@ This file (`AGENTS.md`) is a short pointer + quick reference for agents that don
## Quick command reference
| Action | Command |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
| Run firmware unit tests (native) | `pio test -e native` |
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
| Format before commit | `trunk fmt` |
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
| Action | Command |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
| Run firmware unit tests (native) | `pio test -e native` |
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
| Format before commit | `trunk fmt` |
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
## MCP server (device + test automation)
@@ -121,19 +122,21 @@ Sequence these; don't parallelize on the same port.
- **Device fully wedged (no DFU)?** `mcp__meshtastic__uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles it via USB hub PPPS. Needs `uhubctl` installed (`brew install uhubctl` / `apt install uhubctl`); on Linux without udev rules, permission errors fail fast, so use `sudo uhubctl` yourself or configure udev access.
- **Port busy?** `lsof <port>` to find the holder. Usually a stale `pio device monitor` or zombie `meshtastic_mcp` process. Kill it.
- **Multiple MCP servers running?** `ps aux | grep meshtastic_mcp` — zombies hold ports. Kill all but the one your host spawned.
- **macOS: `LIBUSB_ERROR_BUSY` on a CH341 LoRa adapter?** A third-party WCH `CH34xVCPDriver` is claiming interface 0. Find the bundle ID with `ioreg -p IOUSB -l -w 0 | grep -B2 -A30 0x5512`, then `sudo kmutil unload -b <bundleID>`. Apple's bundled CH34x kext targets the CH340 UART (PID 0x7523), not the SPI bridge — it's never the culprit.
## Environment variables (test harness)
| Var | Purpose |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
| Var | Purpose |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
| `MESHTASTIC_MCP_TCP_HOST` | `host` or `host:port` of a `meshtasticd` daemon (e.g. the `native-macos` build). Surfaces it in `list_devices` as `tcp://host:port` so `connect()`-based tools target it transparently. Default port 4403. |
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
+3 -1
View File
@@ -3,15 +3,17 @@
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
FROM python:3.14-slim-trixie AS builder
FROM debian:trixie AS builder
ARG PIO_ENV=native
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC
# Install Dependencies
ENV PIP_ROOT_USER_ACTION=ignore
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN apt-get update && apt-get install --no-install-recommends -y \
curl wget g++ zip git ca-certificates pkg-config \
python3-pip python3-grpc-tools \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
+10 -3
View File
@@ -3,12 +3,19 @@
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
FROM python:3.14-alpine3.22 AS builder
# Ensure the Alpine version is updated in both stages of the container!
FROM alpine:3.23 AS builder
ARG PIO_ENV=native
ENV PIP_ROOT_USER_ACTION=ignore
# Enable Alpine community repository (for 'py3-grpcio-tools')
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v$(cut -d. -f1,2 /etc/alpine-release)/community" >> /etc/apk/repositories
# Install Dependencies
ENV PIP_ROOT_USER_ACTION=ignore
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN apk --no-cache add \
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
py3-pip py3-grpcio-tools \
libgpiod-dev yaml-cpp-dev bluez-dev \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
@@ -60,4 +67,4 @@ EXPOSE 4403
CMD [ "sh", "-cx", "meshtasticd --fsdir=/var/lib/meshtasticd" ]
HEALTHCHECK NONE
HEALTHCHECK NONE
+628
View File
@@ -0,0 +1,628 @@
#!/usr/bin/env python3
"""Board intake assessment for new Meshtastic hardware support.
Validates a board intake request against the repository hardware context,
identifies evidence gaps, and produces a structured readiness report.
Usage:
python3 bin/board_intake.py <intake.json>
python3 bin/board_intake.py <intake.json> --output report.md
python3 bin/board_intake.py <intake.json> --validate # gaps check only, exit 1 if not scaffold_ready
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CONTEXT_PATH = ROOT / "docs" / "hardware-support-context.md"
# Metadata keys every new PlatformIO environment should declare.
REQUIRED_METADATA_KEYS = [
"custom_meshtastic_hw_model",
"custom_meshtastic_hw_model_slug",
"custom_meshtastic_architecture",
"custom_meshtastic_actively_supported",
"custom_meshtastic_support_level",
"custom_meshtastic_display_name",
]
RECOMMENDED_METADATA_KEYS = [
"custom_meshtastic_images",
"custom_meshtastic_tags",
"custom_meshtastic_requires_dfu",
"custom_meshtastic_partition_scheme",
]
# Pin groups that should be backed by evidence before scaffolding.
EVIDENCE_CATEGORIES = ["radio", "display", "input", "GPS", "power"]
# Architecture families that rely on BSP defaults for many pin defines.
BSP_DEFAULT_FAMILIES = {"nrf52840", "rp2040", "stm32", "native"}
# Known valid architectures from the repository.
KNOWN_ARCHITECTURES = {
"esp32",
"esp32-s3",
"esp32-c3",
"esp32-c6",
"esp32s2",
"nrf52840",
"rp2040",
"rp2350",
"stm32",
"native",
}
# ---------------------------------------------------------------------------
# T004 – BoardIntakeRequest dataclass
# ---------------------------------------------------------------------------
@dataclass
class BoardIntakeRequest:
"""Maintainer-supplied description of a proposed new board."""
# Required
environment_name: str
hardware_model: str
display_name: str
architecture: str
# Recommended
hardware_model_slug: str = ""
actively_supported: bool | None = None
support_level: str = ""
source_materials: list[str] = field(default_factory=list)
board_notes: str = ""
@classmethod
def from_dict(cls, data: dict) -> "BoardIntakeRequest":
return cls(
environment_name=data.get("environment_name", ""),
hardware_model=str(data.get("hardware_model", "")),
display_name=data.get("display_name", ""),
architecture=data.get("architecture", ""),
hardware_model_slug=data.get("hardware_model_slug", ""),
actively_supported=data.get("actively_supported"),
support_level=str(data.get("support_level", "")),
source_materials=list(data.get("source_materials", [])),
board_notes=data.get("board_notes", ""),
)
@classmethod
def from_json(cls, path: Path) -> "BoardIntakeRequest":
data = json.loads(path.read_text(encoding="utf-8"))
return cls.from_dict(data)
# ---------------------------------------------------------------------------
# T005 – EvidenceGap dataclass
# ---------------------------------------------------------------------------
@dataclass
class EvidenceGap:
"""A specific missing, conflicting, or ambiguous hardware fact."""
category: str # metadata | radio | display | input | GPS | power | storage | connectivity | revision-scope
description: str
affected_artifact: str
required_evidence: str
blocking: bool
# ---------------------------------------------------------------------------
# T006 – IntakeAssessment dataclass
# ---------------------------------------------------------------------------
@dataclass
class IntakeAssessment:
"""Structured result of evaluating a BoardIntakeRequest."""
request: BoardIntakeRequest
expected_artifacts: list[str]
required_metadata: list[str]
matched_patterns: list[dict]
evidence_gaps: list[EvidenceGap]
risk_flags: list[str]
next_actions: list[str]
scaffold_ready: bool
# ---------------------------------------------------------------------------
# T007 – load_hardware_context
# ---------------------------------------------------------------------------
def load_hardware_context(path: Path = DEFAULT_CONTEXT_PATH) -> dict:
"""Parse the generated hardware-support-context.md into a usable dict.
Returns:
{
"architecture_names": list[str], # families present in the inventory
"metadata_keys": list[str], # custom_meshtastic_* keys observed
"environments": dict[str, dict], # env_name -> {display, hw_model, hw_slug, variant_dir, arch}
}
"""
if not path.exists():
raise FileNotFoundError(
f"Hardware context not found at {path}. "
"Run: python3 bin/generate_hardware_support_context.py"
)
text = path.read_text(encoding="utf-8")
# Extract metadata keys from the "## Repository Metadata Inputs" section.
metadata_keys: list[str] = re.findall(r"`(custom_meshtastic_[^`]+)`", text)
metadata_keys = list(dict.fromkeys(metadata_keys)) # deduplicate, preserve order
# Extract architecture families from the "## Architecture and Environment Inventory" section.
arch_names: list[str] = re.findall(r"^### ([a-z0-9\-]+)\s*$", text, re.MULTILINE)
# Filter out sub-headings that are architecture names (exclude e.g. "nrf52840" inside examples)
# The inventory section has short arch names; filter to known set plus any that look like archs.
arch_names = [a for a in dict.fromkeys(arch_names) if not a[0].isupper()]
# Extract environments from inventory table rows.
environments: dict[str, dict] = {}
current_arch = ""
for line in text.splitlines():
arch_match = re.match(r"^### ([a-z0-9\-]+)\s*$", line)
if arch_match:
current_arch = arch_match.group(1)
continue
# Table row: | env | display | hw_model | hw_slug | variant_dir | categories |
row = re.match(
r"^\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|",
line,
)
if (
row
and not row.group(1).startswith("Environment")
and not row.group(1).startswith("---")
):
env_name = row.group(1).strip()
if env_name:
environments[env_name] = {
"display_name": row.group(2).strip(),
"hw_model": row.group(3).strip(),
"hw_slug": row.group(4).strip(),
"variant_dir": row.group(5).strip(),
"architecture": current_arch,
}
return {
"architecture_names": arch_names,
"metadata_keys": metadata_keys,
"environments": environments,
}
# ---------------------------------------------------------------------------
# T018 – validate_intake
# ---------------------------------------------------------------------------
def validate_intake(request: BoardIntakeRequest, context: dict) -> list[str]:
"""Check required fields and detect conflicts with existing environments/models.
Returns a list of validation error strings (empty = valid).
"""
errors: list[str] = []
if not request.environment_name:
errors.append("environment_name is required.")
if not request.hardware_model:
errors.append("hardware_model is required.")
if not request.display_name:
errors.append("display_name is required.")
if not request.architecture:
errors.append("architecture is required.")
if request.architecture and request.architecture not in KNOWN_ARCHITECTURES:
errors.append(
f"architecture '{request.architecture}' is not a known repository architecture. "
f"Known: {', '.join(sorted(KNOWN_ARCHITECTURES))}"
)
# Conflict: environment name already exists
if request.environment_name and request.environment_name in context.get(
"environments", {}
):
errors.append(
f"environment_name '{request.environment_name}' already exists in the repository. "
"Choose a unique name or confirm this is an intentional update."
)
# Conflict: hardware model already assigned to a different environment
if request.hardware_model:
for env_name, env_data in context.get("environments", {}).items():
if (
env_data.get("hw_model") == request.hardware_model
and env_name != request.environment_name
):
errors.append(
f"hardware_model '{request.hardware_model}' is already assigned to "
f"environment '{env_name}' ({env_data.get('display_name', '')!r}). "
"Verify this is a new model number or confirm the shared-model intent."
)
break # report once
return errors
# ---------------------------------------------------------------------------
# T019 – find_matched_patterns
# ---------------------------------------------------------------------------
def find_matched_patterns(request: BoardIntakeRequest, context: dict) -> list[dict]:
"""Return up to 3 existing environments closest to the request by architecture."""
envs = context.get("environments", {})
arch = request.architecture
# Prefer exact architecture match, then partial (e.g., "esp32" matches "esp32-s3").
exact: list[dict] = []
partial: list[dict] = []
for env_name, env_data in envs.items():
entry = {**env_data, "environment": env_name}
env_arch = env_data.get("architecture", "")
if env_arch == arch:
exact.append(entry)
elif arch and (env_arch.startswith(arch) or arch.startswith(env_arch)):
partial.append(entry)
candidates = exact + partial
# Prefer boards that have a display_name and hw_model (more complete entries).
candidates.sort(key=lambda e: (not e.get("display_name"), not e.get("hw_model")))
return candidates[:3]
# ---------------------------------------------------------------------------
# T020 – build_evidence_gaps
# ---------------------------------------------------------------------------
def build_evidence_gaps(request: BoardIntakeRequest) -> list[EvidenceGap]:
"""Identify missing pin-group evidence and metadata gaps."""
gaps: list[EvidenceGap] = []
has_sources = bool(request.source_materials)
if not has_sources:
# Every pin category is unresolvable without sources.
for cat in EVIDENCE_CATEGORIES:
gaps.append(
EvidenceGap(
category=cat,
description=(
f"No source materials supplied. {cat.capitalize()} pin mappings cannot be "
"verified without a schematic, pinout diagram, or vendor datasheet."
),
affected_artifact="variant.h",
required_evidence="Schematic, pinout image, or vendor board page",
blocking=True,
)
)
else:
# Sources exist but may still be incomplete; flag as non-blocking advisory.
for cat in EVIDENCE_CATEGORIES:
gaps.append(
EvidenceGap(
category=cat,
description=(
f"Source materials are present but {cat} pin assignments have not been "
"extracted and cross-checked against repository macro conventions."
),
affected_artifact="variant.h",
required_evidence=f"Explicit {cat} pin listing matched to repository #define names",
blocking=False,
)
)
# Metadata gaps
if not request.hardware_model_slug:
gaps.append(
EvidenceGap(
category="metadata",
description="hardware_model_slug is not set. The repository requires an UPPER_SNAKE_CASE slug for PlatformIO metadata.",
affected_artifact="platformio.ini (custom_meshtastic_hw_model_slug)",
required_evidence="Agreed slug from the project maintainers",
blocking=True,
)
)
if request.actively_supported is None:
gaps.append(
EvidenceGap(
category="metadata",
description="actively_supported is not specified. This controls CI matrix inclusion.",
affected_artifact="platformio.ini (custom_meshtastic_actively_supported)",
required_evidence="Maintainer decision on support status",
blocking=False,
)
)
if not request.support_level:
gaps.append(
EvidenceGap(
category="metadata",
description="support_level is not specified (expected: 1 = active, 2 = supported, 3 = extra).",
affected_artifact="platformio.ini (custom_meshtastic_support_level)",
required_evidence="Maintainer decision on support tier",
blocking=False,
)
)
# Revision-scope: multiple display/radio options without disambiguation.
# Match whole-word choice language rather than raw substrings so normal text
# like "radio" does not trigger the ambiguity gate.
note_text = request.board_notes.lower()
revision_scope_patterns = (
r"\brevision\b",
r"\bvariant\b",
r"\bvariants\b",
r"\boption\b",
r"\boptions\b",
r"\balternative\b",
r"\balternatives\b",
r"\bmulti\b",
r"\btwo\b",
r"\beither\b",
r"\bor\b",
)
if note_text and any(re.search(pattern, note_text) for pattern in revision_scope_patterns):
gaps.append(
EvidenceGap(
category="revision-scope",
description=(
"Board notes mention multiple variants, revisions, or options. "
"The intake must be scoped to a single hardware revision before scaffolding can proceed."
),
affected_artifact="variant.h, platformio.ini",
required_evidence="Explicit decision on which revision this intake covers",
blocking=True,
)
)
return gaps
# ---------------------------------------------------------------------------
# T021 – assess_intake
# ---------------------------------------------------------------------------
def assess_intake(request: BoardIntakeRequest, context: dict) -> IntakeAssessment:
"""Produce a full structured assessment from intake request and context."""
validation_errors = validate_intake(request, context)
matched = find_matched_patterns(request, context)
gaps = build_evidence_gaps(request)
expected_artifacts = [
f"variants/{request.architecture or '<architecture>'}/<variant-dir>/variant.h",
f"variants/{request.architecture or '<architecture>'}/<variant-dir>/platformio.ini (env:{request.environment_name or '<env>'})",
]
if request.architecture in {"esp32", "esp32-s3", "esp32-c3", "esp32-c6"}:
expected_artifacts.append(
"(optional) variants/.../variant.cpp — only if board requires custom init hooks"
)
expected_artifacts += [
"PlatformIO metadata: all required custom_meshtastic_* keys (see required_metadata below)",
"(optional) board image under branding/ or images/ if custom_meshtastic_images is set",
]
required_metadata = list(REQUIRED_METADATA_KEYS)
if request.architecture in BSP_DEFAULT_FAMILIES:
required_metadata.append(
f"(BSP note) {request.architecture} boards may inherit some pin defines from BSP headers — "
"check the Inherited Defaults section of docs/hardware-support-context.md before assuming a missing define is an error."
)
risk_flags: list[str] = []
for err in validation_errors:
risk_flags.append(f"Validation error: {err}")
if request.architecture in BSP_DEFAULT_FAMILIES:
risk_flags.append(
f"Architecture '{request.architecture}' uses BSP defaults for some pin defines. "
"Verify which macros are inherited before declaring them explicitly in variant.h."
)
if not matched:
risk_flags.append(
"No closely matched existing board found for this architecture. "
"Manual review of variant structure is required."
)
next_actions: list[str] = []
if validation_errors:
next_actions.append("Resolve validation errors before proceeding.")
blocking_gaps = [g for g in gaps if g.blocking]
non_blocking_gaps = [g for g in gaps if not g.blocking]
for gap in blocking_gaps:
next_actions.append(
f"Provide {gap.required_evidence} for {gap.category} ({gap.affected_artifact})."
)
if non_blocking_gaps:
next_actions.append(
f"Review {len(non_blocking_gaps)} non-blocking gap(s) before merging scaffold output."
)
if not next_actions:
next_actions.append(
"All required evidence is present. Proceed to scaffold generation."
)
scaffold_ready = len(validation_errors) == 0 and all(not g.blocking for g in gaps)
return IntakeAssessment(
request=request,
expected_artifacts=expected_artifacts,
required_metadata=required_metadata,
matched_patterns=matched,
evidence_gaps=gaps,
risk_flags=risk_flags,
next_actions=next_actions,
scaffold_ready=scaffold_ready,
)
# ---------------------------------------------------------------------------
# T022 – render_assessment_markdown
# ---------------------------------------------------------------------------
def render_assessment_markdown(assessment: IntakeAssessment) -> str:
req = assessment.request
lines: list[str] = []
lines.append(f"# Board Intake Assessment: `{req.environment_name or '(unnamed)'}`")
lines.append("")
lines.append(
f"**Scaffold ready**: {'✅ Yes' if assessment.scaffold_ready else '❌ No — see blocking gaps below'}"
)
lines.append("")
lines.append("## Request Summary")
lines.append("")
lines.append(f"- **Environment name**: `{req.environment_name}`")
lines.append(f"- **Hardware model**: `{req.hardware_model}`")
lines.append(
f"- **Hardware model slug**: `{req.hardware_model_slug or '(not set)'}`"
)
lines.append(f"- **Display name**: {req.display_name}")
lines.append(f"- **Architecture**: `{req.architecture}`")
if req.actively_supported is not None:
lines.append(f"- **Actively supported**: {req.actively_supported}")
if req.support_level:
lines.append(f"- **Support level**: {req.support_level}")
if req.source_materials:
lines.append("- **Source materials**:")
for src in req.source_materials:
lines.append(f" - {src}")
if req.board_notes:
lines.append(f"- **Board notes**: {req.board_notes}")
lines.append("")
lines.append("## Expected Artifacts")
lines.append("")
for artifact in assessment.expected_artifacts:
lines.append(f"- {artifact}")
lines.append("")
lines.append("## Required Metadata")
lines.append("")
for key in assessment.required_metadata:
lines.append(
f"- `{key}`" if key.startswith("custom_meshtastic") else f"- {key}"
)
lines.append("")
lines.append("## Matched Repository Patterns")
lines.append("")
if assessment.matched_patterns:
for pattern in assessment.matched_patterns:
name = pattern.get("display_name") or pattern.get("environment", "")
env = pattern.get("environment", "")
arch = pattern.get("architecture", "")
vdir = pattern.get("variant_dir", "")
lines.append(f"- **{name}** (`{env}`, {arch}) — `{vdir}`")
else:
lines.append("- No closely matched patterns found for this architecture.")
lines.append("")
blocking = [g for g in assessment.evidence_gaps if g.blocking]
non_blocking = [g for g in assessment.evidence_gaps if not g.blocking]
lines.append("## Evidence Gaps")
lines.append("")
if blocking:
lines.append("### Blocking")
lines.append("")
for gap in blocking:
lines.append(f"- **[{gap.category}]** {gap.description}")
lines.append(f" - Affected: `{gap.affected_artifact}`")
lines.append(f" - Required evidence: {gap.required_evidence}")
else:
lines.append("*No blocking evidence gaps.*")
lines.append("")
if non_blocking:
lines.append("### Non-blocking (review before merge)")
lines.append("")
for gap in non_blocking:
lines.append(f"- **[{gap.category}]** {gap.description}")
lines.append(f" - Affected: `{gap.affected_artifact}`")
lines.append(f" - Required evidence: {gap.required_evidence}")
lines.append("")
if assessment.risk_flags:
lines.append("## Risk Flags")
lines.append("")
for flag in assessment.risk_flags:
lines.append(f"- {flag}")
lines.append("")
lines.append("## Next Actions")
lines.append("")
for i, action in enumerate(assessment.next_actions, 1):
lines.append(f"{i}. {action}")
lines.append("")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# T023 – CLI entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Evaluate a board intake request against the repository hardware context."
)
parser.add_argument("intake", help="Path to the intake JSON file")
parser.add_argument(
"--output",
default="-",
help="Output path for the assessment markdown (default: stdout)",
)
parser.add_argument(
"--validate",
action="store_true",
help="Exit with code 1 if scaffold_ready is false (useful for CI gate checks)",
)
parser.add_argument(
"--context",
default=str(DEFAULT_CONTEXT_PATH),
help="Path to docs/hardware-support-context.md (default: auto-detected)",
)
args = parser.parse_args()
intake_path = Path(args.intake)
if not intake_path.exists():
print(f"Error: intake file not found: {intake_path}", file=sys.stderr)
sys.exit(2)
request = BoardIntakeRequest.from_json(intake_path)
context = load_hardware_context(Path(args.context))
assessment = assess_intake(request, context)
report = render_assessment_markdown(assessment)
if args.output == "-":
print(report)
else:
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(report, encoding="utf-8")
print(f"Assessment written to {output_path}")
if args.validate and not assessment.scaffold_ready:
sys.exit(1)
if __name__ == "__main__":
main()
+392
View File
@@ -0,0 +1,392 @@
#!/usr/bin/env python3
"""Scaffold board-support files from a validated intake assessment."""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from board_intake import (
BoardIntakeRequest,
EvidenceGap,
IntakeAssessment,
assess_intake,
load_hardware_context,
render_assessment_markdown,
)
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT_ROOT = ROOT / "generated" / "hardware-support"
ARCH_BASE_ENV = {
"esp32": "esp32_base",
"esp32-s3": "esp32s3_base",
"esp32-c3": "esp32c3_base",
"esp32-c6": "esp32c6_base",
"esp32s2": "esp32s2_base",
"nrf52840": "nrf52_base",
"rp2040": "rp2040_base",
"rp2350": "rp2350_base",
"stm32": "stm32_base",
"native": "native_base",
}
ARCH_VARIANT_ROOT = {
"esp32": "esp32",
"esp32-s3": "esp32s3",
"esp32-c3": "esp32c3",
"esp32-c6": "esp32c6",
"esp32s2": "esp32s2",
"nrf52840": "nrf52840",
"rp2040": "rp2040",
"rp2350": "rp2350",
"stm32": "stm32",
"native": "native",
}
PLACEHOLDER_DEFINES = {
"status": [
"#define LED_PIN // TODO: verify status LED pin if present",
],
"input": [
"#define BUTTON_PIN // TODO: verify user button pin",
],
"display": [
"#define HAS_SCREEN 1",
"#define USE_SSD1306 // TODO: verify display controller",
"#define I2C_SCL // TODO: verify display I2C clock pin",
"#define I2C_SDA // TODO: verify display I2C data pin",
],
"GPS": [
"#define GPS_RX_PIN // TODO: verify GPS RX pin or remove if GPS absent",
"#define GPS_TX_PIN // TODO: verify GPS TX pin or remove if GPS absent",
],
"power": [
"#define BATTERY_PIN // TODO: verify battery sense pin",
"#define ADC_MULTIPLIER // TODO: verify voltage divider ratio",
],
"radio": [
"#define USE_SX1262 // TODO: verify radio chip selection from schematic",
"#define SX126X_CS // TODO: verify radio chip-select pin",
"#define SX126X_BUSY // TODO: verify radio busy pin",
"#define SX126X_DIO1 // TODO: verify radio IRQ pin",
"#define SX126X_RESET // TODO: verify radio reset pin",
"#define LORA_SCK // TODO: verify radio SPI clock pin",
"#define LORA_MISO // TODO: verify radio SPI MISO pin",
"#define LORA_MOSI // TODO: verify radio SPI MOSI pin",
],
}
DEFINE_PATTERN = re.compile(r"^#define\s+([A-Za-z0-9_]+)(?:\s+(.*?))?\s*(?://.*)?$")
CATEGORY_MACROS = {
"status": ["LED_POWER", "LED_PIN", "LED_STATE_ON", "VEXT_ENABLE"],
"input": ["BUTTON_PIN", "BUTTON_NEED_PULLUP", "ROTARY_A", "ROTARY_B"],
"display": [
"HAS_SCREEN",
"USE_SSD1306",
"USE_SH1106",
"USE_TFTDISPLAY",
"USE_ST7789",
"I2C_SCL",
"I2C_SDA",
"I2C_SCL1",
"I2C_SDA1",
"TFT_HEIGHT",
"TFT_WIDTH",
"TFT_OFFSET_X",
"TFT_OFFSET_Y",
"SCREEN_ROTATE",
"SCREEN_TRANSITION_FRAMERATE",
],
"GPS": [
"HAS_GPS",
"GPS_RX_PIN",
"GPS_TX_PIN",
"GPS_BAUDRATE",
"PIN_GPS_PPS",
"GPS_THREAD_INTERVAL",
],
"power": [
"ADC_CTRL",
"ADC_CTRL_ENABLED",
"BATTERY_PIN",
"ADC_CHANNEL",
"ADC_ATTENUATION",
"ADC_MULTIPLIER",
"USE_POWERSAVE",
"SLEEP_TIME",
],
"radio": [
"USE_SX1262",
"USE_SX1268",
"USE_LR1121",
"USE_SX1280",
"LORA_DIO0",
"LORA_RESET",
"LORA_DIO1",
"LORA_DIO2",
"LORA_SCK",
"LORA_MISO",
"LORA_MOSI",
"LORA_CS",
"SX126X_CS",
"SX126X_DIO1",
"SX126X_BUSY",
"SX126X_RESET",
"SX126X_DIO2_AS_RF_SWITCH",
"SX126X_DIO3_TCXO_VOLTAGE",
"LR1121_IRQ_PIN",
"LR1121_NRESET_PIN",
"LR1121_BUSY_PIN",
"LR1121_SPI_NSS_PIN",
"LR1121_SPI_SCK_PIN",
"LR1121_SPI_MOSI_PIN",
"LR1121_SPI_MISO_PIN",
"LR11X0_DIO3_TCXO_VOLTAGE",
"LR11X0_DIO_AS_RF_SWITCH",
],
}
def slugify_variant_dir(req: BoardIntakeRequest) -> str:
return req.environment_name.replace("_", "-").lower()
def target_variant_dir(req: BoardIntakeRequest) -> Path:
arch_root = ARCH_VARIANT_ROOT.get(req.architecture, req.architecture)
return Path("variants") / arch_root / slugify_variant_dir(req)
def pick_pattern(assessment: IntakeAssessment) -> dict | None:
return assessment.matched_patterns[0] if assessment.matched_patterns else None
def source_basis_lines(assessment: IntakeAssessment, context: dict) -> list[str]:
lines = [
f"// Intake environment: {assessment.request.environment_name}",
f"// Intake architecture: {assessment.request.architecture}",
]
pattern = pick_pattern(assessment)
if pattern:
lines.append(
"// Repository pattern basis: "
f"{pattern.get('environment', '')} -> {pattern.get('variant_dir', '')}"
)
lines.append(
f"// Context source: {context.get('context_path', 'docs/hardware-support-context.md')}"
)
return lines
def parse_variant_defines(variant_path: Path) -> dict[str, str]:
defines: dict[str, str] = {}
if not variant_path.exists():
return defines
for raw_line in variant_path.read_text(encoding="utf-8").splitlines():
match = DEFINE_PATTERN.match(raw_line.strip())
if not match:
continue
name = match.group(1)
value = (match.group(2) or "1").strip()
defines[name] = value
return defines
def infer_category_lines(pattern_variant_path: Path, category: str) -> list[str]:
defines = parse_variant_defines(pattern_variant_path)
lines: list[str] = []
for macro_name in CATEGORY_MACROS[category]:
value = defines.get(macro_name)
if value is None:
continue
if value == "1":
lines.append(f"#define {macro_name}")
else:
lines.append(f"#define {macro_name} {value}")
if lines:
return lines
return PLACEHOLDER_DEFINES[category]
# T028
def generate_variant_h(assessment: IntakeAssessment, context: dict) -> str:
req = assessment.request
lines: list[str] = []
lines.extend(source_basis_lines(assessment, context))
lines.append("")
lines.append("#pragma once")
lines.append("")
pattern = pick_pattern(assessment)
pattern_variant_path = None
if pattern and pattern.get("variant_dir"):
pattern_variant_path = (ROOT / str(pattern["variant_dir"]) / "variant.h").resolve()
for category in ("status", "input", "display", "GPS", "power", "radio"):
lines.append(f"// {category}")
if pattern_variant_path is not None:
lines.extend(infer_category_lines(pattern_variant_path, category))
else:
lines.extend(PLACEHOLDER_DEFINES[category])
lines.append("")
lines.append("// Board identity")
macro_name = req.hardware_model_slug or req.environment_name.upper().replace(
"-", "_"
)
lines.append(f"#define {macro_name} 1")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
# T029
def generate_platformio_env(assessment: IntakeAssessment) -> str:
req = assessment.request
extends = ARCH_BASE_ENV.get(req.architecture, f"{req.architecture}_base")
variant_dir = target_variant_dir(req)
build_define = (
req.hardware_model_slug or req.environment_name.upper().replace("-", "_")
).replace(" ", "_")
lines = [
f"[env:{req.environment_name}]",
f"custom_meshtastic_hw_model = {req.hardware_model}",
f"custom_meshtastic_hw_model_slug = {req.hardware_model_slug or 'TODO_SET_HW_MODEL_SLUG'}",
f"custom_meshtastic_architecture = {req.architecture}",
f"custom_meshtastic_actively_supported = {str(req.actively_supported).lower() if req.actively_supported is not None else 'TODO_SET_ACTIVELY_SUPPORTED'}",
f"custom_meshtastic_support_level = {req.support_level or 'TODO_SET_SUPPORT_LEVEL'}",
f"custom_meshtastic_display_name = {req.display_name}",
"custom_meshtastic_images = TODO_SET_IMAGES",
"custom_meshtastic_tags = TODO_SET_TAGS",
"custom_meshtastic_requires_dfu = TODO_SET_REQUIRES_DFU",
"custom_meshtastic_partition_scheme = TODO_SET_PARTITION_SCHEME",
"",
"board = TODO_SET_PLATFORMIO_BOARD",
f"extends = {extends}",
"build_flags =",
f" ${{{extends}.build_flags}}",
f" -D {build_define}",
f" -I {variant_dir.as_posix()}",
]
return "\n".join(lines).rstrip() + "\n"
# T030
def annotate_unresolved(content: str, gaps: list[EvidenceGap], is_ini: bool = False) -> str:
annotations = [
gap
for gap in gaps
if not gap.blocking
and gap.category in {"radio", "display", "input", "GPS", "power", "metadata"}
]
if not annotations:
return content
lines = content.splitlines()
todo_lines = [f"; TODO: verify — {gap.description}" for gap in annotations]
if is_ini:
# For INI files, insert TODOs after the first section header
if lines and lines[0].startswith("["):
return "\n".join(lines[:1] + todo_lines + [""] + lines[1:]) + "\n"
else:
# For .h files, prepend TODOs with C++ comment syntax
todo_lines = [f"// TODO: verify — {gap.description}" for gap in annotations]
return "\n".join(todo_lines + [""] + lines) + "\n"
return content
# T031
def scaffold_board(
assessment: IntakeAssessment, context: dict, output_dir: Path
) -> dict[str, Path]:
req = assessment.request
variant_dir = output_dir / target_variant_dir(req)
variant_dir.mkdir(parents=True, exist_ok=True)
variant_h_path = variant_dir / "variant.h"
platformio_path = variant_dir / "platformio.ini"
variant_h_content = annotate_unresolved(
generate_variant_h(assessment, context), assessment.evidence_gaps, is_ini=False
)
platformio_content = annotate_unresolved(
generate_platformio_env(assessment), assessment.evidence_gaps, is_ini=True
)
variant_h_path.write_text(variant_h_content, encoding="utf-8")
platformio_path.write_text(platformio_content, encoding="utf-8")
generated = {
"variant_h": variant_h_path,
"platformio": platformio_path,
}
if req.architecture in {"esp32", "esp32-s3", "esp32-c3", "esp32-c6"}:
variant_cpp_path = variant_dir / "variant.cpp"
variant_cpp_content = annotate_unresolved(
"\n".join(
[
'#include "variant.h"',
"",
"// Optional board-specific initialization hooks go here.",
"// Leave this file out if the board does not need custom startup behavior.",
]
)
+ "\n",
assessment.evidence_gaps,
)
variant_cpp_path.write_text(variant_cpp_content, encoding="utf-8")
generated["variant_cpp"] = variant_cpp_path
return generated
# T032 + T033
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate board-support scaffold files from an intake JSON file."
)
parser.add_argument("intake", help="Path to intake JSON")
parser.add_argument(
"--output-dir",
default=str(DEFAULT_OUTPUT_ROOT),
help="Directory where scaffold files will be written",
)
args = parser.parse_args()
intake_path = Path(args.intake)
request = BoardIntakeRequest.from_json(intake_path)
context = load_hardware_context()
context["context_path"] = "docs/hardware-support-context.md"
assessment = assess_intake(request, context)
if not assessment.scaffold_ready:
print(render_assessment_markdown(assessment))
sys.exit(1)
generated = scaffold_board(assessment, context, Path(args.output_dir))
print("Generated scaffold files:")
for name, path in generated.items():
print(f"- {name}: {path}")
if __name__ == "__main__":
main()
+14
View File
@@ -0,0 +1,14 @@
{
"environment_name": "new_esp32s3_board",
"hardware_model": "999",
"hardware_model_slug": "NEW_ESP32S3_BOARD",
"display_name": "Acme ESP32-S3 Dev Board v1",
"architecture": "esp32-s3",
"actively_supported": true,
"support_level": "1",
"source_materials": [
"https://example.com/acme-esp32s3-schematic.pdf",
"https://example.com/acme-esp32s3-pinout.png"
],
"board_notes": "Initial bring-up for v1 PCB only. SX1262 radio on SPI2, OLED on I2C bus."
}
+6
View File
@@ -0,0 +1,6 @@
{
"environment_name": "hypothetical_esp32s3_board",
"hardware_model": "9981",
"display_name": "Hypothetical ESP32-S3 Board",
"architecture": "esp32-s3"
}
+13
View File
@@ -0,0 +1,13 @@
{
"environment_name": "new_nrf52_tracker",
"hardware_model": "998",
"hardware_model_slug": "NEW_NRF52_TRACKER",
"display_name": "Acme nRF52840 Tracker v2",
"architecture": "nrf52840",
"actively_supported": null,
"support_level": "2",
"source_materials": [
"https://example.com/acme-tracker-pinout.pdf"
],
"board_notes": "Two revisions exist: v2 (SSD1306 OLED) and v2a (no display). This intake covers v2 only."
}
+592
View File
@@ -0,0 +1,592 @@
#!/usr/bin/env python3
"""Generate a markdown inventory of board-support metadata and common target-definition macros."""
from __future__ import annotations
import argparse
import re
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
VARIANTS_DIR = ROOT / "variants"
OUTPUT_PATH = ROOT / "docs" / "hardware-support-context.md"
PLATFORMIO_METADATA_KEYS = [
"custom_meshtastic_hw_model",
"custom_meshtastic_hw_model_slug",
"custom_meshtastic_architecture",
"custom_meshtastic_actively_supported",
"custom_meshtastic_support_level",
"custom_meshtastic_display_name",
"custom_meshtastic_images",
"custom_meshtastic_tags",
"custom_meshtastic_requires_dfu",
"custom_meshtastic_partition_scheme",
]
MACRO_PATTERNS = {
"Input": [
"BUTTON_PIN",
"BUTTON_PIN_TOUCH",
"ALT_BUTTON_PIN",
"CANCEL_BUTTON_PIN",
"ROTARY_",
"KB_",
"INPUTDRIVER_",
],
"Radio": [
"USE_RF95",
"USE_SX126",
"USE_SX128",
"USE_LLCC68",
"USE_LR11",
"LORA_",
"SX126X_",
"SX128X_",
"LR1121_",
"RF95_",
],
"GPS": [
"HAS_GPS",
"GPS_",
"PIN_GPS_",
"GPS_DEFAULT_NOT_PRESENT",
],
"Display": [
"HAS_SCREEN",
"USE_TFTDISPLAY",
"USE_TFT",
"USE_SSD",
"USE_SH",
"USE_ST",
"TFT_",
"OLED_",
"SCREEN_",
"PIN_EINK_",
"EINK_",
"DISPLAY_",
"LGFX_",
"HAS_TFT",
],
"I2C/SPI": [
"I2C_",
"SPI_",
"PIN_SPI",
"WIRE_",
],
"Power": [
"BATTERY_",
"ADC_",
"PIN_POWER",
"USE_POWERSAVE",
"SLEEP_TIME",
"XPOWERS_",
"HAS_AXP",
"HAS_PPM",
"HAS_BQ",
"EXT_NOTIFY_OUT",
"FAN_CTRL_PIN",
"RF95_FAN_EN",
],
"Connectivity/Other": [
"HAS_WIFI",
"HAS_BLUETOOTH",
"HAS_ETHERNET",
"HAS_NEOPIXEL",
"HAS_I2S",
"HAS_TOUCH",
"HAS_TOUCHSCREEN",
"HAS_NFC",
"NFC_",
"USE_XL9555",
"EXPANDS_",
"PCF",
"RTC",
],
}
def classify_macro(name: str) -> str:
for category, prefixes in MACRO_PATTERNS.items():
for prefix in prefixes:
if name.startswith(prefix):
return category
return "Other"
def parse_platformio_envs(path: Path) -> list[dict[str, object]]:
envs: list[dict[str, object]] = []
current: dict[str, object] | None = None
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith(";"):
continue
if line.startswith("[") and line.endswith("]"):
if current:
envs.append(current)
current = {"section": line[1:-1], "metadata": {}}
continue
if current is None or "=" not in line:
continue
key, value = [part.strip() for part in line.split("=", 1)]
if key in PLATFORMIO_METADATA_KEYS:
current["metadata"][key] = value
if current:
envs.append(current)
return envs
def parse_variant_macros(path: Path) -> tuple[dict[str, str], Counter[str]]:
defines: dict[str, str] = {}
category_counts: Counter[str] = Counter()
pattern = re.compile(r"^#define\s+([A-Za-z0-9_]+)(?:\s+(.*?))?\s*(?://.*)?$")
for raw_line in path.read_text(encoding="utf-8").splitlines():
match = pattern.match(raw_line.strip())
if not match:
continue
name = match.group(1)
value = (match.group(2) or "1").strip()
defines[name] = value
category_counts[classify_macro(name)] += 1
return defines, category_counts
MACRO_DESCRIPTIONS: dict[str, str] = {
# ---------- Input ----------
"BUTTON_PIN": "Primary user button GPIO pin number.",
"ALT_BUTTON_PIN": "Secondary / alternate button pin, used on boards with two buttons.",
"CANCEL_BUTTON_PIN": "Button wired to cancel/back actions (e.g., T-Deck cancel key).",
"BUTTON_NEED_PULLUP": "Set to 1 when the button GPIO requires the internal pull-up to be enabled.",
"BUTTON_PIN_ALT": "Alternative spelling used by some older board families for a second button.",
"KB_BL_PIN": "Keyboard backlight control pin (e.g., T-LoRa Pager keyboard).",
"KB_INT": "Keyboard interrupt input pin; signals a keypress to the MCU.",
"KB_POWERON": "Pin used to power-on or enable the keyboard peripheral.",
"KB_SLAVE_ADDRESS": "I2C address of the keyboard controller IC.",
"INPUTDRIVER_ENCODER_TYPE": "Selects the rotary encoder driver variant (0 = none, 1+ = specific type).",
"INPUTDRIVER_TWO_WAY_ROCKER": "Enables the two-way rocker input driver.",
"INPUTDRIVER_TWO_WAY_ROCKER_RIGHT": "GPIO pin for the rightward rocker direction.",
"INPUTDRIVER_TWO_WAY_ROCKER_LEFT": "GPIO pin for the leftward rocker direction.",
"INPUTDRIVER_TWO_WAY_ROCKER_BTN": "GPIO pin for the rocker click/press direction.",
"ROTARY_A": "First encoder channel pin (clock / A signal).",
"ROTARY_B": "Second encoder channel pin (data / B signal).",
# ---------- Radio ----------
"USE_SX1262": "Select the SX1262 sub-GHz LoRa chip driver.",
"USE_SX1268": "Select the SX1268 sub-GHz LoRa chip driver (higher power variant of SX1262).",
"USE_SX1280": "Select the SX1280 2.4 GHz LoRa chip driver.",
"USE_RF95": "Select the RFM95/SX1276 legacy LoRa chip driver.",
"USE_LLCC68": "Select the LLCC68 low-cost LoRa chip driver.",
"USE_LR1110": "Select the LR1110 wideband radio driver.",
"USE_LR1120": "Select the LR1120 wideband radio driver.",
"USE_LR1121": "Select the LR1121 wideband radio driver.",
"LORA_CS": "LoRa radio SPI chip-select GPIO pin.",
"LORA_RESET": "LoRa radio hardware-reset GPIO pin (active low).",
"LORA_DIO0": "LoRa radio DIO0 interrupt pin (RF95/SX1276 done/RxDone).",
"LORA_DIO1": "LoRa radio DIO1 interrupt pin; on SX126x this is the primary IRQ line.",
"LORA_DIO2": "LoRa radio DIO2 pin; on RF95 used for FSK interrupt; on SX126x tx/rx control.",
"LORA_SCK": "LoRa radio SPI clock pin.",
"LORA_MISO": "LoRa radio SPI MISO pin.",
"LORA_MOSI": "LoRa radio SPI MOSI pin.",
"SX126X_CS": "SX126x chip-select pin (often same as LORA_CS).",
"SX126X_RESET": "SX126x reset pin (often same as LORA_RESET).",
"SX126X_BUSY": "SX126x BUSY pin; must be polled low before issuing SPI commands.",
"SX126X_DIO1": "SX126x DIO1 interrupt output used for all IRQ events.",
"SX126X_DIO3_TCXO_VOLTAGE": "Drives the TCXO regulator via DIO3; set to the supply voltage (e.g., 1.8).",
"SX126X_DIO2_AS_RF_SWITCH": "Set to 1 so the driver controls the TX/RX RF switch via DIO2.",
"LR1121_IRQ_PIN": "LR1121 interrupt request pin.",
"RF95_FAN_EN": "Enables a cooling fan for high-power RF95 installations.",
# ---------- GPS ----------
"HAS_GPS": "Set to 1 if the board has an on-board GPS receiver; 0 to disable GPS entirely.",
"GPS_RX_PIN": "UART RX pin connected to the GPS module TX output.",
"GPS_TX_PIN": "UART TX pin connected to the GPS module RX input.",
"PIN_GPS_EN": "GPIO to power-enable or power-gate the GPS module.",
"PIN_GPS_PPS": "GPS pulse-per-second input pin for timing synchronisation.",
"PIN_GPS_STANDBY": "Places the GPS into standby/low-power mode when driven.",
"PIN_GPS_RESET": "Hardware-reset line to the GPS module.",
"PIN_GPS_REINIT": "Pin used to trigger GPS re-initialisation sequences.",
"GPS_THREAD_INTERVAL": "Millisecond poll interval for the GPS background thread.",
"GPS_BAUDRATE": "UART baud rate for GPS serial communication.",
"GPS_L76K": "Selects the Quectel L76K GPS driver and protocol.",
"GPS_UBLOX": "Selects the u-blox GPS driver and UBX protocol.",
"GPS_EN_ACTIVE": "Logic level (HIGH or LOW) that enables the GPS power pin.",
"GPS_RESET_MODE": "Defines the reset signal polarity or protocol for the GPS chip.",
"GPS_DEFAULT_NOT_PRESENT": "Compiled-in default assuming no GPS; overridden at runtime if detected.",
# ---------- Display ----------
"HAS_SCREEN": "Set to 1 if the board has any display hardware.",
"USE_TFTDISPLAY": "Selects the TFT LCD driver path.",
"USE_SSD1306": "Selects the SSD1306 128×64 OLED driver over I2C.",
"USE_SH1106": "Selects the SH1106 128×64 OLED driver.",
"USE_ST7735": "Selects the ST7735 TFT SPI driver.",
"USE_ST7789": "Selects the ST7789 TFT SPI driver.",
"TFT_WIDTH": "Horizontal pixel resolution of the TFT display.",
"TFT_HEIGHT": "Vertical pixel resolution of the TFT display.",
"TFT_OFFSET_X": "Horizontal pixel offset for display alignment correction.",
"TFT_OFFSET_Y": "Vertical pixel offset for display alignment correction.",
"TFT_BL": "Backlight control PWM or GPIO pin for the TFT panel.",
"SCREEN_ROTATE": "Non-zero value rotates the display 180° for mounted-upside-down screens.",
"SCREEN_TRANSITION_FRAMERATE": "Target framerate for UI animations and transitions.",
"PIN_EINK_CS": "E-ink display SPI chip-select pin.",
"PIN_EINK_BUSY": "E-ink display BUSY output; high when a page update is in progress.",
"PIN_EINK_DC": "E-ink display data/command select pin.",
"PIN_EINK_RES": "E-ink display hardware-reset pin.",
"PIN_EINK_SCLK": "E-ink display SPI clock pin.",
"PIN_EINK_MOSI": "E-ink display SPI MOSI pin.",
# ---------- I2C/SPI ----------
"I2C_SDA": "Primary I2C data line GPIO pin.",
"I2C_SCL": "Primary I2C clock line GPIO pin.",
"PIN_SPI_MISO": "Primary SPI MISO (data from peripheral) GPIO pin.",
"PIN_SPI_MOSI": "Primary SPI MOSI (data to peripheral) GPIO pin.",
"PIN_SPI_SCK": "Primary SPI clock GPIO pin.",
"SPI_INTERFACES_COUNT": "Number of hardware SPI buses available on this board.",
"WIRE_INTERFACES_COUNT": "Number of hardware I2C buses available on this board.",
"PIN_SPI1_MISO": "Secondary SPI bus MISO pin.",
"PIN_SPI1_MOSI": "Secondary SPI bus MOSI pin.",
"PIN_SPI1_SCK": "Secondary SPI bus clock pin.",
"SPI_FREQUENCY": "Default SPI clock frequency in Hz for this board.",
"SPI_READ_FREQUENCY": "Reduced SPI clock rate used for read transactions.",
"SPI_SCK": "SPI clock pin alias used in older board files.",
"SPI_MOSI": "SPI MOSI pin alias used in older board files.",
"SPI_MISO": "SPI MISO pin alias used in older board files.",
# ---------- Power ----------
"BATTERY_PIN": "ADC input GPIO connected to the battery voltage divider.",
"ADC_MULTIPLIER": "Floating-point scale factor to convert raw ADC reading to battery voltage.",
"ADC_CHANNEL": "ADC channel enum or number for the battery sense input.",
"BATTERY_SENSE_RESOLUTION_BITS": "ADC resolution in bits used for battery voltage sampling.",
"ADC_RESOLUTION": "Board-level ADC resolution definition, referenced by other power macros.",
"BATTERY_SENSE_RESOLUTION": "Alias for the effective ADC resolution for battery sense.",
"ADC_CTRL": "GPIO that enables or gates the ADC voltage-divider circuit.",
"ADC_CTRL_ENABLED": "Logic level (HIGH or LOW) that turns on the ADC control switch.",
"ADC_ATTENUATION": "ESP32 ADC input attenuation setting; controls measurable voltage range.",
"BATTERY_SENSE_SAMPLES": "Number of ADC samples to average for a stable battery reading.",
"EXT_NOTIFY_OUT": "GPIO output used to signal an external LED or buzzer for notifications.",
"USE_POWERSAVE": "Enables aggressive power-save mode (deep sleep, reduced poll intervals).",
"SLEEP_TIME": "Default light-sleep duration in milliseconds between wakeups.",
"PIN_POWER_EN": "GPIO to assert to enable a board power rail or load switch.",
"HAS_PPM": "Set to 1 if the board has an IP5306 or similar PPM power path IC.",
# ---------- Connectivity/Other ----------
"HAS_TOUCHSCREEN": "Set to 1 for boards with a capacitive or resistive touch panel.",
"HAS_NEOPIXEL": "Set to 1 if the board has addressable RGB LEDs (WS2812 / NeoPixel).",
"PCF8563_RTC": "I2C address of the PCF8563 real-time clock IC.",
"PCF85063_RTC": "I2C address of the PCF85063 real-time clock IC.",
"HAS_ETHERNET": "Set to 1 for boards with a wired Ethernet interface.",
"HAS_I2S": "Set to 1 if I2S audio output is present.",
"NFC_INT": "Interrupt pin from the NFC controller IC.",
"NFC_CS": "SPI chip-select for the NFC controller.",
"USE_XL9555": "Enables the XL9555 16-bit I2C GPIO expander driver.",
"EXPANDS_DRV_EN": "GPIO expander pin used to enable the haptic driver.",
"EXPANDS_AMP_EN": "GPIO expander pin used to power-on the audio amplifier.",
"EXPANDS_KB_RST": "GPIO expander pin used to reset the keyboard controller.",
"EXPANDS_LORA_EN": "GPIO expander pin used to power-gate the LoRa radio.",
"EXPANDS_GPS_EN": "GPIO expander pin used to power-gate the GPS module.",
"EXPANDS_NFC_EN": "GPIO expander pin used to power-gate the NFC controller.",
# ---------- Other ----------
"LED_POWER": "GPIO for the status LED, defines the LED pin number.",
"LED_STATE_ON": "Logic level (HIGH or LOW) that turns the status LED on.",
"PIN_SERIAL1_RX": "Secondary UART RX pin (used for accessories, GPS on some boards).",
"PIN_SERIAL1_TX": "Secondary UART TX pin.",
"PIN_WIRE_SDA": "Arduino-framework I2C SDA pin alias (nRF52 / RP2040 style).",
"PIN_WIRE_SCL": "Arduino-framework I2C SCL pin alias.",
"PIN_LED1": "First LED GPIO pin in the nRF52 Arduino BSP pin table.",
"VARIANT_MCK": "Crystal oscillator frequency in Hz for nRF52 variant clock configuration.",
"PINS_COUNT": "Total number of GPIO pins defined in the Arduino BSP variant table.",
"NUM_DIGITAL_PINS": "Count of digital-capable pins in the BSP variant table.",
"NUM_ANALOG_INPUTS": "Count of analog-input pins in the BSP variant table.",
"NUM_ANALOG_OUTPUTS": "Count of analog-output (DAC) pins in the BSP variant table.",
"LED_BLUE": "GPIO number of the blue status LED (typical on nRF52 and RP2040 boards).",
"USE_LFXO": "Instructs the nRF52 BSP to use the low-frequency crystal oscillator.",
"BUTTON_NEED_PULLUP": "Enables internal pull-up on the button GPIO (duplicate entry for clarity).",
}
def shorten(value: str, limit: int = 60) -> str:
compact = " ".join(value.split())
return compact if len(compact) <= limit else compact[: limit - 3] + "..."
# Architecture families known to rely heavily on BSP or base-environment defaults rather
# than declaring every field locally. Used in the Inherited Defaults Note section.
_BSP_DEFAULT_FAMILIES: dict[str, list[str]] = {
"nrf52840": [
"VARIANT_MCK — nRF52 BSP clock constant (e.g., 64000000ul); always inherited from BSP unless overridden.",
"USE_LFXO — low-frequency crystal oscillator selection; declared locally only when the board uses LFXO rather than the RC oscillator.",
"PIN_SPI_* / PIN_SPI1_* — SPI bus pin numbers come from the BSP variant table; boards override only when the LoRa radio or display uses non-default SPI routing.",
"WIRE_INTERFACES_COUNT / SPI_INTERFACES_COUNT — bus count comes from BSP; explicitly set only when the board deviates.",
"LED_BLUE / PIN_LED1 / PINS_COUNT / NUM_DIGITAL_PINS — standard BSP pin-table entries inherited from the nRF52 Arduino core.",
],
"rp2040": [
"PIN_SPI_* — primary SPI pins come from the RP2040 Arduino BSP; most boards declare them explicitly, but the defaults align with the Pico pin assignments.",
"NUM_DIGITAL_PINS / NUM_ANALOG_INPUTS — Arduino BSP counts; rarely overridden locally.",
],
"stm32": [
"Radio and pin assignments for STM32WL targets are largely internal to the WL SoC and declared via STM32 HAL/BSP headers; variant.h files are minimal.",
"USE_STM32WLx is typically the only explicit define; all other radio config comes from the BSP.",
],
"native": [
"The native/Portduino target uses runtime configuration rather than compile-time pin defines; variant.h only sets display and GPS stubs.",
],
}
def collect_inventory() -> dict[str, object]:
architectures: defaultdict[str, list[dict[str, object]]] = defaultdict(list)
category_frequency: defaultdict[str, Counter[str]] = defaultdict(Counter)
total_variant_dirs = 0
total_envs = 0
no_variant_h: defaultdict[str, int] = defaultdict(int) # arch -> count of dirs with no variant.h
has_metadata: int = 0 # env count that has at least one custom_meshtastic_* key
for platformio_path in sorted(VARIANTS_DIR.glob("**/platformio.ini")):
variant_dir = platformio_path.parent
total_variant_dirs += 1
variant_path = variant_dir / "variant.h"
board_level_arch_raw = variant_dir.parts[-2] if len(variant_dir.parts) >= 2 else "unknown"
if not variant_path.exists():
no_variant_h[board_level_arch_raw] += 1
continue
envs = parse_platformio_envs(platformio_path)
defines, category_counts = parse_variant_macros(variant_path)
total_envs += len(envs)
board_level_arch = board_level_arch_raw
for env in envs:
section = str(env["section"])
if not section.startswith("env:"):
continue
metadata = dict(env["metadata"])
if metadata:
has_metadata += 1
architecture = str(metadata.get("custom_meshtastic_architecture") or board_level_arch)
if architecture == "esp32s3":
architecture = "esp32-s3"
if architecture == "esp32c3":
architecture = "esp32-c3"
if architecture == "esp32c6":
architecture = "esp32-c6"
entry = {
"environment": section.split(":", 1)[1],
"variant_dir": str(variant_dir.relative_to(ROOT)),
"metadata": metadata,
"defines": defines,
"category_counts": category_counts,
}
architectures[architecture].append(entry)
for name in defines:
category = classify_macro(name)
category_frequency[category][name] += 1
return {
"architectures": architectures,
"category_frequency": category_frequency,
"total_variant_dirs": total_variant_dirs,
"total_envs": total_envs,
"no_variant_h": dict(no_variant_h),
"has_metadata": has_metadata,
}
def render_markdown(inventory: dict[str, object]) -> str:
architectures = inventory["architectures"]
category_frequency = inventory["category_frequency"]
lines: list[str] = []
lines.append("# Hardware Support Context")
lines.append("")
lines.append("This document inventories the board-support inputs and common target-definition fields")
lines.append("currently used in the Meshtastic firmware repository. It is intended as reusable context")
lines.append("for adding new hardware support without re-discovering naming patterns, metadata keys, and")
lines.append("frequently used pin or capability macros from scratch.")
lines.append("")
lines.append("## Scope")
lines.append("")
lines.append(f"- Variant directories scanned: {inventory['total_variant_dirs']}")
lines.append(f"- PlatformIO environments summarized: {inventory['total_envs']}")
lines.append("- Sources: `variants/**/platformio.ini` and `variants/**/variant.h`")
lines.append("- Notes: This inventory reflects explicit per-variant declarations. Some boards also inherit")
lines.append(" defaults from architecture headers or shared base environments, which must still be checked")
lines.append(" before creating new hardware support.")
lines.append("")
lines.append("## Repository Metadata Inputs")
lines.append("")
lines.append("The following `custom_meshtastic_*` metadata keys are already used across board environments:")
lines.append("")
for key in PLATFORMIO_METADATA_KEYS:
lines.append(f"- `{key}`")
lines.append("")
lines.append("## Common Target-Definition Categories")
lines.append("")
for category in ["Input", "Radio", "GPS", "Display", "I2C/SPI", "Power", "Connectivity/Other", "Other"]:
counter = category_frequency.get(category, Counter())
if not counter:
continue
lines.append(f"### {category}")
lines.append("")
lines.append("| Macro | Used in | Description |")
lines.append("| --- | --- | --- |")
for name, count in counter.most_common(15):
description = MACRO_DESCRIPTIONS.get(name, "")
lines.append(f"| `{name}` | {count} variants | {description} |")
lines.append("")
lines.append("## Architecture and Environment Inventory")
lines.append("")
for architecture in sorted(architectures):
entries = sorted(architectures[architecture], key=lambda item: item["environment"])
lines.append(f"### {architecture}")
lines.append("")
lines.append("| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |")
lines.append("| --- | --- | --- | --- | --- | --- |")
for entry in entries:
metadata = entry["metadata"]
category_counts = entry["category_counts"]
categories = []
for category in ["Display", "Radio", "Input", "GPS", "Power", "Connectivity/Other"]:
count = category_counts.get(category, 0)
if count:
categories.append(f"{category}:{count}")
category_summary = ", ".join(categories) if categories else "none"
lines.append(
"| {environment} | {display_name} | {hw_model} | {hw_slug} | {variant_dir} | {categories} |".format(
environment=entry["environment"],
display_name=metadata.get("custom_meshtastic_display_name", ""),
hw_model=metadata.get("custom_meshtastic_hw_model", ""),
hw_slug=metadata.get("custom_meshtastic_hw_model_slug", ""),
variant_dir=entry["variant_dir"],
categories=category_summary,
)
)
lines.append("")
lines.append("## Representative Board Examples")
lines.append("")
for architecture in sorted(architectures):
entries = sorted(architectures[architecture], key=lambda item: item["environment"])
if not entries:
continue
sample = entries[0]
lines.append(f"### {architecture}: `{sample['environment']}`")
lines.append("")
lines.append(f"- Variant directory: `{sample['variant_dir']}`")
metadata = sample["metadata"]
for key in PLATFORMIO_METADATA_KEYS:
value = metadata.get(key)
if value:
lines.append(f"- `{key}`: `{value}`")
defines = sample["defines"]
for category in ["Input", "Radio", "GPS", "Display", "I2C/SPI", "Power", "Connectivity/Other"]:
selected = [name for name in defines if classify_macro(name) == category][:8]
if not selected:
continue
lines.append(f"- {category} examples:")
for name in selected:
lines.append(f" - `{name}` = `{shorten(defines[name])}`")
lines.append("")
lines.append("## Intake Guidance For New Hardware")
lines.append("")
lines.append("When using this context to add a new board, collect these inputs before generating files:")
lines.append("")
lines.append("- PlatformIO environment name")
lines.append("- `custom_meshtastic_hw_model` and `custom_meshtastic_hw_model_slug`")
lines.append("- Display name and architecture")
lines.append("- Whether the board is actively supported and its support level")
lines.append("- Partition scheme, DFU requirement, and image/tag metadata if applicable")
lines.append("- Radio chip family and complete radio pin group")
lines.append("- Input/button/rotary/keyboard pins")
lines.append("- Display interface pins and driver-related macros")
lines.append("- GPS, power-management, I2C, SPI, storage, and auxiliary peripheral definitions")
lines.append("- Any board-specific initialization that requires `variant.cpp` or extra variant hooks")
lines.append("")
lines.append("## Inherited Defaults Note")
lines.append("")
lines.append(
"Some architecture families rely on BSP (Board Support Package) or base-environment defaults "
"rather than declaring every pin or capability macro explicitly in `variant.h`. "
"When adding a new board for one of these families, check the relevant BSP headers before "
"assuming a missing define means a feature is absent."
)
lines.append("")
no_variant_h = inventory.get("no_variant_h", {})
if no_variant_h:
lines.append(
f"Directories scanned that had no `variant.h` "
f"(relying entirely on BSP/base-environment): "
+ ", ".join(f"{arch}: {count}" for arch, count in sorted(no_variant_h.items()))
)
lines.append("")
for family, notes in _BSP_DEFAULT_FAMILIES.items():
lines.append(f"### {family}")
lines.append("")
for note in notes:
lines.append(f"- {note}")
lines.append("")
lines.append("## Cautions")
lines.append("")
lines.append("- Some boards rely on architecture defaults rather than declaring every field locally.")
lines.append("- Some board families expose multiple environments or display variants that share one hardware model.")
lines.append("- Source materials such as schematics still need human verification before new pin mappings are trusted.")
lines.append("- This document is a starting context artifact, not proof that a new board definition is safe to merge.")
lines.append("")
return "\n".join(lines)
def print_validate_summary(inventory: dict[str, object]) -> None:
"""Print a validation summary to stdout without writing the output file."""
total_dirs = inventory["total_variant_dirs"]
total_envs = inventory["total_envs"]
has_metadata = inventory["has_metadata"]
no_variant_h = inventory.get("no_variant_h", {})
no_variant_h_total = sum(no_variant_h.values())
architectures = inventory["architectures"]
arch_count = len(architectures)
print("Hardware Support Context — Validation Summary")
print("=" * 48)
print(f"Variant directories scanned : {total_dirs}")
print(f"Directories with no variant.h (BSP-only) : {no_variant_h_total}")
if no_variant_h:
for arch, count in sorted(no_variant_h.items()):
print(f" {arch}: {count}")
print(f"PlatformIO environments found : {total_envs}")
print(f"Environments with custom_meshtastic_* metadata : {has_metadata}")
print(f"Architecture families represented : {arch_count}")
print("")
print("Architecture families:")
for arch in sorted(architectures):
entries = architectures[arch]
print(f" {arch}: {len(entries)} environment(s)")
print("")
print("BSP-default families with special notes:")
for family in _BSP_DEFAULT_FAMILIES:
marker = "YES" if family in {a.split("-")[0] for a in architectures} else "not in scan"
print(f" {family}: {marker}")
def main() -> None:
parser = argparse.ArgumentParser(description="Generate hardware support context markdown")
parser.add_argument("--output", default=str(OUTPUT_PATH), help="Output markdown path")
parser.add_argument(
"--validate",
action="store_true",
help="Print a validation summary (variant counts, BSP-only dirs, metadata coverage) without writing output",
)
args = parser.parse_args()
inventory = collect_inventory()
if args.validate:
print_validate_summary(inventory)
return
markdown = render_markdown(inventory)
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(markdown + "\n", encoding="utf-8")
if __name__ == "__main__":
main()
+742
View File
@@ -0,0 +1,742 @@
# Hardware Support Context
This document inventories the board-support inputs and common target-definition fields
currently used in the Meshtastic firmware repository. It is intended as reusable context
for adding new hardware support without re-discovering naming patterns, metadata keys, and
frequently used pin or capability macros from scratch.
## Scope
- Variant directories scanned: 166
- PlatformIO environments summarized: 212
- Sources: `variants/**/platformio.ini` and `variants/**/variant.h`
- Notes: This inventory reflects explicit per-variant declarations. Some boards also inherit
defaults from architecture headers or shared base environments, which must still be checked
before creating new hardware support.
## Repository Metadata Inputs
The following `custom_meshtastic_*` metadata keys are already used across board environments:
- `custom_meshtastic_hw_model`
- `custom_meshtastic_hw_model_slug`
- `custom_meshtastic_architecture`
- `custom_meshtastic_actively_supported`
- `custom_meshtastic_support_level`
- `custom_meshtastic_display_name`
- `custom_meshtastic_images`
- `custom_meshtastic_tags`
- `custom_meshtastic_requires_dfu`
- `custom_meshtastic_partition_scheme`
## Common Target-Definition Categories
### Input
| Macro | Used in | Description |
| ---------------------------------- | ------------ | --------------------------------------------------------------------------- |
| `BUTTON_PIN` | 127 variants | Primary user button GPIO pin number. |
| `ALT_BUTTON_PIN` | 12 variants | Secondary / alternate button pin, used on boards with two buttons. |
| `CANCEL_BUTTON_PIN` | 8 variants | Button wired to cancel/back actions (e.g., T-Deck cancel key). |
| `KB_BL_PIN` | 5 variants | Keyboard backlight control pin (e.g., T-LoRa Pager keyboard). |
| `KB_INT` | 4 variants | Keyboard interrupt input pin; signals a keypress to the MCU. |
| `INPUTDRIVER_ENCODER_TYPE` | 3 variants | Selects the rotary encoder driver variant (0 = none, 1+ = specific type). |
| `BUTTON_PIN_ALT` | 2 variants | Alternative spelling used by some older board families for a second button. |
| `INPUTDRIVER_TWO_WAY_ROCKER` | 2 variants | Enables the two-way rocker input driver. |
| `INPUTDRIVER_TWO_WAY_ROCKER_RIGHT` | 2 variants | GPIO pin for the rightward rocker direction. |
| `INPUTDRIVER_TWO_WAY_ROCKER_LEFT` | 2 variants | GPIO pin for the leftward rocker direction. |
| `INPUTDRIVER_TWO_WAY_ROCKER_BTN` | 2 variants | GPIO pin for the rocker click/press direction. |
| `KB_POWERON` | 2 variants | Pin used to power-on or enable the keyboard peripheral. |
| `KB_SLAVE_ADDRESS` | 2 variants | I2C address of the keyboard controller IC. |
| `ROTARY_A` | 2 variants | First encoder channel pin (clock / A signal). |
| `ROTARY_B` | 2 variants | Second encoder channel pin (data / B signal). |
### Radio
| Macro | Used in | Description |
| -------------------------- | ------------ | ----------------------------------------------------------------------------- |
| `SX126X_CS` | 150 variants | SX126x chip-select pin (often same as LORA_CS). |
| `SX126X_RESET` | 150 variants | SX126x reset pin (often same as LORA_RESET). |
| `SX126X_BUSY` | 150 variants | SX126x BUSY pin; must be polled low before issuing SPI commands. |
| `SX126X_DIO1` | 150 variants | SX126x DIO1 interrupt output used for all IRQ events. |
| `USE_SX1262` | 145 variants | Select the SX1262 sub-GHz LoRa chip driver. |
| `SX126X_DIO3_TCXO_VOLTAGE` | 140 variants | Drives the TCXO regulator via DIO3; set to the supply voltage (e.g., 1.8). |
| `LORA_CS` | 137 variants | LoRa radio SPI chip-select GPIO pin. |
| `LORA_RESET` | 135 variants | LoRa radio hardware-reset GPIO pin (active low). |
| `SX126X_DIO2_AS_RF_SWITCH` | 135 variants | Set to 1 so the driver controls the TX/RX RF switch via DIO2. |
| `LORA_DIO1` | 130 variants | LoRa radio DIO1 interrupt pin; on SX126x this is the primary IRQ line. |
| `LORA_SCK` | 125 variants | LoRa radio SPI clock pin. |
| `LORA_MISO` | 125 variants | LoRa radio SPI MISO pin. |
| `LORA_MOSI` | 125 variants | LoRa radio SPI MOSI pin. |
| `LORA_DIO2` | 110 variants | LoRa radio DIO2 pin; on RF95 used for FSK interrupt; on SX126x tx/rx control. |
| `LORA_DIO0` | 98 variants | LoRa radio DIO0 interrupt pin (RF95/SX1276 done/RxDone). |
### GPS
| Macro | Used in | Description |
| ------------------------- | ------------ | ------------------------------------------------------------------------------ |
| `GPS_RX_PIN` | 114 variants | UART RX pin connected to the GPS module TX output. |
| `GPS_TX_PIN` | 111 variants | UART TX pin connected to the GPS module RX input. |
| `HAS_GPS` | 69 variants | Set to 1 if the board has an on-board GPS receiver; 0 to disable GPS entirely. |
| `PIN_GPS_PPS` | 43 variants | GPS pulse-per-second input pin for timing synchronisation. |
| `PIN_GPS_EN` | 30 variants | GPIO to power-enable or power-gate the GPS module. |
| `PIN_GPS_STANDBY` | 29 variants | Places the GPS into standby/low-power mode when driven. |
| `GPS_THREAD_INTERVAL` | 28 variants | Millisecond poll interval for the GPS background thread. |
| `GPS_L76K` | 27 variants | Selects the Quectel L76K GPS driver and protocol. |
| `GPS_BAUDRATE` | 27 variants | UART baud rate for GPS serial communication. |
| `GPS_EN_ACTIVE` | 17 variants | Logic level (HIGH or LOW) that enables the GPS power pin. |
| `PIN_GPS_RESET` | 16 variants | Hardware-reset line to the GPS module. |
| `GPS_DEFAULT_NOT_PRESENT` | 16 variants | Compiled-in default assuming no GPS; overridden at runtime if detected. |
| `GPS_RESET_MODE` | 13 variants | Defines the reset signal polarity or protocol for the GPS chip. |
| `GPS_UBLOX` | 8 variants | Selects the u-blox GPS driver and UBX protocol. |
| `PIN_GPS_REINIT` | 7 variants | Pin used to trigger GPS re-initialisation sequences. |
### Display
| Macro | Used in | Description |
| ----------------------------- | ----------- | ------------------------------------------------------------------------ |
| `PIN_EINK_CS` | 51 variants | E-ink display SPI chip-select pin. |
| `PIN_EINK_BUSY` | 51 variants | E-ink display BUSY output; high when a page update is in progress. |
| `PIN_EINK_DC` | 51 variants | E-ink display data/command select pin. |
| `PIN_EINK_RES` | 51 variants | E-ink display hardware-reset pin. |
| `PIN_EINK_SCLK` | 49 variants | E-ink display SPI clock pin. |
| `PIN_EINK_MOSI` | 49 variants | E-ink display SPI MOSI pin. |
| `HAS_SCREEN` | 34 variants | Set to 1 if the board has any display hardware. |
| `USE_TFTDISPLAY` | 27 variants | Selects the TFT LCD driver path. |
| `TFT_HEIGHT` | 25 variants | Vertical pixel resolution of the TFT display. |
| `TFT_WIDTH` | 25 variants | Horizontal pixel resolution of the TFT display. |
| `SCREEN_TRANSITION_FRAMERATE` | 23 variants | Target framerate for UI animations and transitions. |
| `TFT_OFFSET_X` | 22 variants | Horizontal pixel offset for display alignment correction. |
| `TFT_OFFSET_Y` | 22 variants | Vertical pixel offset for display alignment correction. |
| `SCREEN_ROTATE` | 18 variants | Non-zero value rotates the display 180° for mounted-upside-down screens. |
| `TFT_BL` | 15 variants | Backlight control PWM or GPIO pin for the TFT panel. |
### I2C/SPI
| Macro | Used in | Description |
| ----------------------- | ------------ | ----------------------------------------------------- |
| `I2C_SDA` | 103 variants | Primary I2C data line GPIO pin. |
| `I2C_SCL` | 103 variants | Primary I2C clock line GPIO pin. |
| `PIN_SPI_MISO` | 72 variants | Primary SPI MISO (data from peripheral) GPIO pin. |
| `PIN_SPI_MOSI` | 72 variants | Primary SPI MOSI (data to peripheral) GPIO pin. |
| `PIN_SPI_SCK` | 72 variants | Primary SPI clock GPIO pin. |
| `SPI_INTERFACES_COUNT` | 66 variants | Number of hardware SPI buses available on this board. |
| `WIRE_INTERFACES_COUNT` | 64 variants | Number of hardware I2C buses available on this board. |
| `PIN_SPI1_MISO` | 35 variants | Secondary SPI bus MISO pin. |
| `PIN_SPI1_MOSI` | 35 variants | Secondary SPI bus MOSI pin. |
| `PIN_SPI1_SCK` | 35 variants | Secondary SPI bus clock pin. |
| `SPI_FREQUENCY` | 24 variants | Default SPI clock frequency in Hz for this board. |
| `SPI_READ_FREQUENCY` | 21 variants | Reduced SPI clock rate used for read transactions. |
| `SPI_SCK` | 19 variants | SPI clock pin alias used in older board files. |
| `SPI_MOSI` | 19 variants | SPI MOSI pin alias used in older board files. |
| `SPI_MISO` | 19 variants | SPI MISO pin alias used in older board files. |
### Power
| Macro | Used in | Description |
| ------------------------------- | ------------ | -------------------------------------------------------------------------- |
| `BATTERY_PIN` | 128 variants | ADC input GPIO connected to the battery voltage divider. |
| `ADC_MULTIPLIER` | 122 variants | Floating-point scale factor to convert raw ADC reading to battery voltage. |
| `ADC_CHANNEL` | 71 variants | ADC channel enum or number for the battery sense input. |
| `BATTERY_SENSE_RESOLUTION_BITS` | 64 variants | ADC resolution in bits used for battery voltage sampling. |
| `ADC_RESOLUTION` | 49 variants | Board-level ADC resolution definition, referenced by other power macros. |
| `BATTERY_SENSE_RESOLUTION` | 44 variants | Alias for the effective ADC resolution for battery sense. |
| `ADC_CTRL` | 29 variants | GPIO that enables or gates the ADC voltage-divider circuit. |
| `ADC_CTRL_ENABLED` | 27 variants | Logic level (HIGH or LOW) that turns on the ADC control switch. |
| `EXT_NOTIFY_OUT` | 23 variants | GPIO output used to signal an external LED or buzzer for notifications. |
| `USE_POWERSAVE` | 21 variants | Enables aggressive power-save mode (deep sleep, reduced poll intervals). |
| `SLEEP_TIME` | 21 variants | Default light-sleep duration in milliseconds between wakeups. |
| `ADC_ATTENUATION` | 20 variants | ESP32 ADC input attenuation setting; controls measurable voltage range. |
| `PIN_POWER_EN` | 18 variants | GPIO to assert to enable a board power rail or load switch. |
| `BATTERY_SENSE_SAMPLES` | 11 variants | Number of ADC samples to average for a stable battery reading. |
| `HAS_PPM` | 4 variants | Set to 1 if the board has an IP5306 or similar PPM power path IC. |
### Connectivity/Other
| Macro | Used in | Description |
| ----------------- | ----------- | ------------------------------------------------------------------- |
| `HAS_TOUCHSCREEN` | 19 variants | Set to 1 for boards with a capacitive or resistive touch panel. |
| `HAS_NEOPIXEL` | 14 variants | Set to 1 if the board has addressable RGB LEDs (WS2812 / NeoPixel). |
| `PCF8563_RTC` | 11 variants | I2C address of the PCF8563 real-time clock IC. |
| `HAS_ETHERNET` | 9 variants | Set to 1 for boards with a wired Ethernet interface. |
| `HAS_I2S` | 7 variants | Set to 1 if I2S audio output is present. |
| `PCF85063_RTC` | 3 variants | I2C address of the PCF85063 real-time clock IC. |
| `NFC_INT` | 2 variants | Interrupt pin from the NFC controller IC. |
| `NFC_CS` | 2 variants | SPI chip-select for the NFC controller. |
| `USE_XL9555` | 2 variants | Enables the XL9555 16-bit I2C GPIO expander driver. |
| `EXPANDS_DRV_EN` | 2 variants | GPIO expander pin used to enable the haptic driver. |
| `EXPANDS_AMP_EN` | 2 variants | GPIO expander pin used to power-on the audio amplifier. |
| `EXPANDS_KB_RST` | 2 variants | GPIO expander pin used to reset the keyboard controller. |
| `EXPANDS_LORA_EN` | 2 variants | GPIO expander pin used to power-gate the LoRa radio. |
| `EXPANDS_GPS_EN` | 2 variants | GPIO expander pin used to power-gate the GPS module. |
| `EXPANDS_NFC_EN` | 2 variants | GPIO expander pin used to power-gate the NFC controller. |
### Other
| Macro | Used in | Description |
| -------------------- | ----------- | -------------------------------------------------------------------------- |
| `LED_POWER` | 94 variants | GPIO for the status LED, defines the LED pin number. |
| `LED_STATE_ON` | 88 variants | Logic level (HIGH or LOW) that turns the status LED on. |
| `PIN_SERIAL1_RX` | 67 variants | Secondary UART RX pin (used for accessories, GPS on some boards). |
| `PIN_SERIAL1_TX` | 67 variants | Secondary UART TX pin. |
| `PIN_WIRE_SDA` | 64 variants | Arduino-framework I2C SDA pin alias (nRF52 / RP2040 style). |
| `PIN_WIRE_SCL` | 64 variants | Arduino-framework I2C SCL pin alias. |
| `PIN_LED1` | 63 variants | First LED GPIO pin in the nRF52 Arduino BSP pin table. |
| `VARIANT_MCK` | 63 variants | Crystal oscillator frequency in Hz for nRF52 variant clock configuration. |
| `PINS_COUNT` | 63 variants | Total number of GPIO pins defined in the Arduino BSP variant table. |
| `NUM_DIGITAL_PINS` | 63 variants | Count of digital-capable pins in the BSP variant table. |
| `NUM_ANALOG_INPUTS` | 63 variants | Count of analog-input pins in the BSP variant table. |
| `NUM_ANALOG_OUTPUTS` | 63 variants | Count of analog-output (DAC) pins in the BSP variant table. |
| `LED_BLUE` | 62 variants | GPIO number of the blue status LED (typical on nRF52 and RP2040 boards). |
| `USE_LFXO` | 58 variants | Instructs the nRF52 BSP to use the low-frequency crystal oscillator. |
| `BUTTON_NEED_PULLUP` | 54 variants | Enables internal pull-up on the button GPIO (duplicate entry for clarity). |
## Architecture and Environment Inventory
### diy
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| ------------------------- | ------------ | -------- | ------- | --------------------------------------------- | --------------------------------------------------------- |
| 9m2ibr_aprs_lora_tracker | | | | variants/esp32/diy/9m2ibr_aprs_lora_tracker | Display:1, Radio:24, Input:1, GPS:2, Power:4 |
| esp32c3_super_mini | | | | variants/esp32c3/diy/esp32c3_super_mini | Display:1, Radio:18, Input:1, GPS:2 |
| meshtastic-diy-v1_1 | | | | variants/esp32/diy/v1_1 | Radio:24, Input:1, GPS:1, Power:1 |
| my-esp32s3-diy-eink | | | | variants/esp32s3/diy/my_esp32s3_diy_eink | Display:6, Radio:17, Input:1, GPS:1, Connectivity/Other:1 |
| my-esp32s3-diy-oled | | | | variants/esp32s3/diy/my_esp32s3_diy_oled | Display:1, Radio:17, Input:1, GPS:1, Connectivity/Other:1 |
| nrf52_promicro_diy-inkhud | | | | variants/nrf52840/diy/nrf52_promicro_diy_tcxo | Display:4, Radio:29, Input:1, GPS:4, Power:6 |
| t-energy-s3_e22 | | | | variants/esp32s3/diy/t-energy-s3_e22 | Display:1, Radio:18, Input:1, GPS:3, Power:3 |
### esp32
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| --------------------------- | --------------------------- | -------- | --------------------------- | ------------------------------------------ | ------------------------------------------------------------------ |
| betafpv_2400_tx_micro | | | | variants/esp32/betafpv_2400_tx_micro | Radio:14, Input:1, Connectivity/Other:1 |
| betafpv_900_tx_nano | | | | variants/esp32/betafpv_900_tx_nano | Display:1, Radio:10, Input:1 |
| chatter2 | | | | variants/esp32/chatter2 | Display:12, Radio:16, Input:3, GPS:3, Power:4 |
| hackerboxes-esp32-io | | | | variants/esp32/hackerboxes_esp32_io | Display:1, Radio:16, Input:1, GPS:1 |
| heltec-v1 | Heltec V1 | 11 | HELTEC_V1 | variants/esp32/heltec_v1 | Radio:5, Input:1, GPS:2, Power:3 |
| heltec-v2_0 | Heltec V2.0 | 5 | HELTEC_V2_0 | variants/esp32/heltec_v2 | Radio:5, Input:1, GPS:2, Power:3 |
| heltec-v2_1 | Heltec V2.1 | 10 | HELTEC_V2_1 | variants/esp32/heltec_v2.1 | Radio:5, Input:1, GPS:3, Power:4 |
| heltec-wireless-bridge | | | | variants/esp32/heltec_wireless_bridge | Radio:9, GPS:1 |
| heltec-wsl-v2_1 | | | | variants/esp32/heltec_wsl_v2.1 | Radio:9, Input:1, Power:5 |
| hydra | Hydra | 39 | HYDRA | variants/esp32/diy/hydra | Radio:22, Input:1, GPS:3, Power:4 |
| m5stack-core | M5 Stack | 42 | M5STACK | variants/esp32/m5stack_core | Display:7, Radio:9, Input:1, GPS:2 |
| m5stack-coreink | | | | variants/esp32/m5stack_coreink | Display:7, Radio:34, Input:1, GPS:2, Power:3, Connectivity/Other:1 |
| meshtastic-diy-v1 | DIY V1 | 39 | DIY_V1 | variants/esp32/diy/v1 | Radio:23, Input:1, GPS:3, Power:4 |
| meshtastic-dr-dev | DR-DEV | 41 | DR_DEV | variants/esp32/diy/dr-dev | Display:1, Radio:26, Input:1, GPS:2, Power:3 |
| nano-g1 | Nano G1 | 14 | NANO_G1 | variants/esp32/nano-g1 | Display:1, Radio:13, Input:1, GPS:2, Power:1 |
| nano-g1-explorer | Nano G1 Explorer | 17 | NANO_G1_EXPLORER | variants/esp32/nano-g1-explorer | Display:1, Radio:13, Input:1, GPS:2, Power:5 |
| radiomaster_900_bandit | | | | variants/esp32/radiomaster_900_bandit | Radio:19, Input:1, GPS:1, Connectivity/Other:1 |
| radiomaster_900_bandit_nano | RadioMaster 900 Bandit Nano | 64 | RADIOMASTER_900_BANDIT_NANO | variants/esp32/radiomaster_900_bandit_nano | Display:1, Radio:19 |
| rak11200 | RAK WisBlock 11200 | 13 | RAK11200 | variants/esp32/rak11200 | Radio:18, GPS:2, Power:2 |
| station-g1 | Station G1 | 25 | STATION_G1 | variants/esp32/station-g1 | Display:1, Radio:13, Input:1, GPS:2, Power:5 |
| sugarcube | | | | variants/esp32/tlora_v2_1_16 | Radio:6, Power:4 |
| tbeam | LILYGO T-Beam | 4 | TBEAM | variants/esp32/tbeam | Display:4, Radio:14, Input:1, GPS:3, Power:2, Connectivity/Other:1 |
| tbeam-displayshield | | | | variants/esp32/tbeam | Display:4, Radio:14, Input:1, GPS:3, Power:2, Connectivity/Other:1 |
| tbeam0_7 | LILYGO T-Beam V0.7 | 6 | TBEAM_V0P7 | variants/esp32/tbeam_v07 | Radio:5, Input:1, GPS:3, Power:3 |
| tlora-v1 | LILYGO T-LoRa V1 | 2 | TLORA_V1 | variants/esp32/tlora_v1 | Radio:5, Input:1, Power:1 |
| tlora-v2 | LILYGO T-LoRa V2 | 1 | TLORA_V2 | variants/esp32/tlora_v2 | Radio:5, Input:1, Power:2 |
| tlora-v2-1-1_6 | LILYGO T-LoRa V2.1-1.6 | 3 | TLORA_V2_1_1P6 | variants/esp32/tlora_v2_1_16 | Radio:6, Power:4 |
| tlora-v2-1-1_8 | LILYGO T-LoRa V2.1-1.8 | 15 | TLORA_V2_1_1P8 | variants/esp32/tlora_v2_1_18 | Radio:6, Input:1, Power:3 |
| tlora_v1_3 | | | | variants/esp32/tlora_v1_3 | Radio:5, Input:1, Power:2 |
| trackerd | | | | variants/esp32/trackerd | Display:1, Radio:5, Input:1, GPS:9, Power:4 |
| wiphone | | | | variants/esp32/wiphone | Display:9, Radio:9, GPS:1 |
### esp32-c3
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| -------------------------- | ------------ | -------- | ----------- | ----------------------------------------- | ------------------------------------------------------------------ |
| ai-c3 | | | | variants/esp32c3/ai-c3 | Radio:15, Input:1, GPS:1 |
| hackerboxes-esp32c3-oled | | | | variants/esp32c3/hackerboxes_esp32c3_oled | Display:1, Radio:16, Input:1, GPS:1 |
| heltec-hru-3601 | | | | variants/esp32c3/heltec_hru_3601 | Display:1, Radio:16, Input:1, GPS:1, Power:1, Connectivity/Other:1 |
| heltec-ht62-esp32c3-sx1262 | Heltec HT62 | 53 | HELTEC_HT62 | variants/esp32c3/heltec_esp32c3 | Display:1, Radio:16, Input:1, GPS:1 |
| m5stack-stamp-c3 | | | | variants/esp32c3/m5stack-stamp-c3 | Radio:9, Input:1, GPS:1 |
### esp32-c6
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| --------------- | ---------------- | -------- | ----------- | -------------------------------- | ------------------------------------------------ |
| m5stack-unitc6l | M5Stack Unit C6L | 111 | M5STACK_C6L | variants/esp32c6/m5stack_unitc6l | Display:1, Radio:14, GPS:3, Connectivity/Other:1 |
| tlora-c6 | | | | variants/esp32c6/tlora_c6 | Radio:15 |
### esp32-s3
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| -------------------------------- | ----------------------------- | -------- | ---------------------------- | --------------------------------------------- | -------------------------------------------------------------------- |
| CDEBYTE_EoRa-Hub | | | | variants/esp32s3/CDEBYTE_EoRa-Hub | Display:2, Radio:14, Input:1, Power:6 |
| CDEBYTE_EoRa-S3 | EBYTE EoRa-S3 | 61 | CDEBYTE_EORA_S3 | variants/esp32s3/CDEBYTE_EoRa-S3 | Display:2, Radio:13, Input:1, Power:3 |
| EBYTE_ESP32-S3 | | | | variants/esp32s3/EBYTE_ESP32-S3 | Display:1, Radio:26, Input:1, GPS:5, Power:1 |
| ESP32-S3-Pico | | | | variants/esp32s3/esp32-s3-pico | Display:6, Radio:18, Input:2, GPS:1, Power:4, Connectivity/Other:1 |
| bpi_picow_esp32_s3 | | | | variants/esp32s3/bpi_picow_esp32_s3 | Display:1, Radio:25, Input:1, GPS:1 |
| crowpanel-esp32s3-2-epaper | | | | variants/esp32s3/crowpanel-esp32s3-5-epaper | Display:6, Radio:21, Input:1, GPS:1, Power:1 |
| crowpanel-esp32s3-4-epaper | | | | variants/esp32s3/crowpanel-esp32s3-5-epaper | Display:6, Radio:21, Input:1, GPS:1, Power:1 |
| crowpanel-esp32s3-5-epaper | | | | variants/esp32s3/crowpanel-esp32s3-5-epaper | Display:6, Radio:21, Input:1, GPS:1, Power:1 |
| dreamcatcher-2206 | | | | variants/esp32s3/dreamcatcher | Radio:16, Input:1, Power:1, Connectivity/Other:1 |
| elecrow-adv-24-28-tft | Crowpanel Adv 2.4/2.8 TFT | 97 | CROWPANEL | variants/esp32s3/elecrow_panel | Display:1, Radio:21, GPS:6, Power:2 |
| elecrow-adv-35-tft | Crowpanel Adv 3.5 TFT | 97 | CROWPANEL | variants/esp32s3/elecrow_panel | Display:1, Radio:21, GPS:6, Power:2 |
| elecrow-adv1-43-50-70-tft | Crowpanel Adv 4.3/5.0/7.0 TFT | 97 | CROWPANEL | variants/esp32s3/elecrow_panel | Display:1, Radio:21, GPS:6, Power:2 |
| hackaday-communicator | | | | variants/esp32s3/hackaday-communicator | Display:12, Radio:14, Input:1, GPS:1, Power:2 |
| heltec-v3 | Heltec V3 | 43 | HELTEC_V3 | variants/esp32s3/heltec_v3 | Display:1, Radio:16, Input:1, Power:6 |
| heltec-v4 | Heltec V4 | 110 | HELTEC_V4 | variants/esp32s3/heltec_v4 | Display:1, Radio:21, Input:1, GPS:10, Power:6 |
| heltec-v4-tft | Heltec V4 TFT | 110 | HELTEC_V4 | variants/esp32s3/heltec_v4 | Display:1, Radio:21, Input:1, GPS:10, Power:6 |
| heltec-vision-master-e213 | Heltec Vision Master E213 | 67 | HELTEC_VISION_MASTER_E213 | variants/esp32s3/heltec_vision_master_e213 | Display:6, Radio:15, Input:2, Power:6 |
| heltec-vision-master-e213-inkhud | | | | variants/esp32s3/heltec_vision_master_e213 | Display:6, Radio:15, Input:2, Power:6 |
| heltec-vision-master-e290 | Heltec Vision Master E290 | 68 | HELTEC_VISION_MASTER_E290 | variants/esp32s3/heltec_vision_master_e290 | Display:6, Radio:15, Input:2, Power:6 |
| heltec-vision-master-e290-inkhud | | | | variants/esp32s3/heltec_vision_master_e290 | Display:6, Radio:15, Input:2, Power:6 |
| heltec-vision-master-t190 | Heltec Vision Master T190 | 66 | HELTEC_VISION_MASTER_T190 | variants/esp32s3/heltec_vision_master_t190 | Display:6, Radio:16, Input:2, Power:6 |
| heltec-wireless-paper | Heltec Wireless Paper | 49 | HELTEC_WIRELESS_PAPER | variants/esp32s3/heltec_wireless_paper | Display:6, Radio:15, Input:1, Power:6 |
| heltec-wireless-paper-inkhud | | | | variants/esp32s3/heltec_wireless_paper | Display:6, Radio:15, Input:1, Power:6 |
| heltec-wireless-paper-v1_0 | Heltec Wireless Paper V1.0 | 57 | HELTEC_WIRELESS_PAPER_V1_0 | variants/esp32s3/heltec_wireless_paper_v1 | Display:6, Radio:15, Input:1, Power:6 |
| heltec-wireless-tracker | Heltec Wireless Tracker V1.1 | 48 | HELTEC_WIRELESS_TRACKER | variants/esp32s3/heltec_wireless_tracker | Display:9, Radio:16, Input:1, GPS:7, Power:6 |
| heltec-wireless-tracker-V1-0 | Heltec Wireless Tracker V1.0 | 58 | HELTEC_WIRELESS_TRACKER_V1_0 | variants/esp32s3/heltec_wireless_tracker_V1_0 | Display:9, Radio:16, Input:1, GPS:9, Power:4 |
| heltec-wireless-tracker-v2 | Heltec Wireless Tracker V2 | 113 | HELTEC_WIRELESS_TRACKER_V2 | variants/esp32s3/heltec_wireless_tracker_v2 | Display:10, Radio:19, Input:1, GPS:7, Power:6 |
| heltec-wsl-v3 | Heltec Wireless Stick Lite V3 | 44 | HELTEC_WSL_V3 | variants/esp32s3/heltec_wsl_v3 | Radio:16, Input:1, Power:6 |
| heltec_capsule_sensor_v3 | | | | variants/esp32s3/heltec_capsule_sensor_v3 | Display:1, Radio:16, Input:1, GPS:7, Power:6 |
| heltec_sensor_hub | | | | variants/esp32s3/heltec_sensor_hub | Display:1, Radio:15, Input:1, Power:6, Connectivity/Other:1 |
| icarus | | | | variants/esp32s3/icarus | Display:1, Radio:12, Input:1 |
| link32-s3-v1 | | | | variants/esp32s3/link32_s3_v1 | Display:1, Radio:16, Input:2, Power:4, Connectivity/Other:1 |
| m5stack-cardputer-adv | | | | variants/esp32s3/m5stack_cardputer_adv | Display:5, Radio:17, Input:2, GPS:4, Power:3, Connectivity/Other:2 |
| m5stack-cores3 | | | | variants/esp32s3/m5stack_cores3 | Radio:11, Power:1 |
| mesh-tab-3-2-IPS-capacitive | | | | variants/esp32s3/mesh-tab | Radio:18, Input:1, GPS:2, Power:5, Connectivity/Other:1 |
| mesh-tab-3-2-IPS-resistive | | | | variants/esp32s3/mesh-tab | Radio:18, Input:1, GPS:2, Power:5, Connectivity/Other:1 |
| mesh-tab-3-2-TN-resistive | | | | variants/esp32s3/mesh-tab | Radio:18, Input:1, GPS:2, Power:5, Connectivity/Other:1 |
| mesh-tab-3-5-IPS-capacitive | | | | variants/esp32s3/mesh-tab | Radio:18, Input:1, GPS:2, Power:5, Connectivity/Other:1 |
| mesh-tab-3-5-IPS-resistive | | | | variants/esp32s3/mesh-tab | Radio:18, Input:1, GPS:2, Power:5, Connectivity/Other:1 |
| mesh-tab-3-5-TN-resistive | | | | variants/esp32s3/mesh-tab | Radio:18, Input:1, GPS:2, Power:5, Connectivity/Other:1 |
| mesh-tab-4-0-IPS-capacitive | | | | variants/esp32s3/mesh-tab | Radio:18, Input:1, GPS:2, Power:5, Connectivity/Other:1 |
| mini-epaper-s3 | LILYGO Mini ePaper S3 E-Ink | | MINI_EPAPER_S3 | variants/esp32s3/mini-epaper-s3 | Display:8, Radio:12, Input:5, GPS:1, Power:3, Connectivity/Other:1 |
| mini-epaper-s3-inkhud | | | | variants/esp32s3/mini-epaper-s3 | Display:8, Radio:12, Input:5, GPS:1, Power:3, Connectivity/Other:1 |
| nibble-esp32 | | | | variants/esp32s3/nibble_esp32 | Radio:9, Input:1 |
| nugget-s3-lora | | | | variants/esp32s3/nugget_s3_lora | Display:2, Radio:9, Input:1, Connectivity/Other:1 |
| picomputer-s3 | Pi Computer S3 | 52 | PICOMPUTER_S3 | variants/esp32s3/picomputer-s3 | Display:10, Radio:9, Input:1, Power:3 |
| picomputer-s3-tft | | | | variants/esp32s3/picomputer-s3 | Display:10, Radio:9, Input:1, Power:3 |
| rak3112 | | | | variants/esp32s3/rak3312 | Radio:13, GPS:3, Power:5 |
| rak3312 | RAK3312 | 106 | RAK3312 | variants/esp32s3/rak3312 | Radio:13, GPS:3, Power:5 |
| rak_wismesh_tap_v2 | RAK WisMesh Tap V2 | 116 | WISMESH_TAP_V2 | variants/esp32s3/rak_wismesh_tap_v2 | Radio:13, Input:1, GPS:3, Power:4 |
| rak_wismesh_tap_v2-tft | | | | variants/esp32s3/rak_wismesh_tap_v2 | Radio:13, Input:1, GPS:3, Power:4 |
| seeed-sensecap-indicator | Seeed SenseCAP Indicator | 70 | SENSECAP_INDICATOR | variants/esp32s3/seeed-sensecap-indicator | Display:12, Radio:17, Input:1, GPS:4, Connectivity/Other:1 |
| seeed-sensecap-indicator-tft | | | | variants/esp32s3/seeed-sensecap-indicator | Display:12, Radio:17, Input:1, GPS:4, Connectivity/Other:1 |
| seeed-xiao-s3 | Seeed Xiao ESP32-S3 | 81 | SEEED_XIAO_S3 | variants/esp32s3/seeed_xiao_s3 | Radio:16, Input:1, GPS:6, Power:3 |
| station-g2 | Station G2 | 31 | STATION_G2 | variants/esp32s3/station-g2 | Display:1, Radio:14, Input:1, GPS:2, Power:4 |
| t-beam-1w | LILYGO T-Beam 1W | 122 | TBEAM_1_WATT | variants/esp32s3/t-beam-1w | Display:3, Radio:19, Input:2, GPS:6, Power:5 |
| t-deck | LILYGO T-Deck | 50 | T_DECK | variants/esp32s3/t-deck | Display:11, Radio:17, Input:3, GPS:3, Power:5, Connectivity/Other:2 |
| t-deck-pro | LILYGO T-Deck Pro | 102 | T_DECK_PRO | variants/esp32s3/t-deck-pro | Display:6, Radio:18, Input:2, GPS:7, Power:5, Connectivity/Other:1 |
| t-deck-tft | | | | variants/esp32s3/t-deck | Display:11, Radio:17, Input:3, GPS:3, Power:5, Connectivity/Other:2 |
| t-eth-elite | | | | variants/esp32s3/t-eth-elite | Display:1, Radio:34, Input:1, GPS:5, Connectivity/Other:1 |
| t-watch-s3 | LILYGO T-Watch S3 | 51 | T_WATCH_S3 | variants/esp32s3/t-watch-s3 | Display:12, Radio:17, Input:1, GPS:5, Power:3, Connectivity/Other:3 |
| t5s3_epaper_inkhud | | | | variants/esp32s3/t5s3_epaper | Radio:21, Input:3, GPS:2, Power:5, Connectivity/Other:3 |
| tbeam-s3-core | LILYGO T-Beam Supreme | 12 | LILYGO_TBEAM_S3_CORE | variants/esp32s3/tbeam-s3-core | Display:1, Radio:25, Input:1, GPS:4, Power:1, Connectivity/Other:2 |
| thinknode_m2 | ThinkNode M2 | 90 | THINKNODE_M2 | variants/esp32s3/ELECROW-ThinkNode-M2 | Display:2, Radio:14, Input:2, GPS:1, Power:6 |
| thinknode_m5 | ThinkNode M5 | 107 | THINKNODE_M5 | variants/esp32s3/ELECROW-ThinkNode-M5 | Display:6, Radio:14, Input:2, GPS:8, Power:4, Connectivity/Other:1 |
| tlora-pager | LILYGO T-LoRa Pager | 103 | T_LORA_PAGER | variants/esp32s3/tlora-pager | Display:10, Radio:30, Input:6, GPS:5, Power:5, Connectivity/Other:17 |
| tlora-pager-tft | | | | variants/esp32s3/tlora-pager | Display:10, Radio:30, Input:6, GPS:5, Power:5, Connectivity/Other:17 |
| tlora-t3s3-epaper | LILYGO T-LoRa T3-S3 E-Ink | 16 | TLORA_T3_S3 | variants/esp32s3/tlora_t3s3_epaper | Display:6, Radio:28, Input:1, GPS:3, Power:3 |
| tlora-t3s3-epaper-inkhud | | | | variants/esp32s3/tlora_t3s3_epaper | Display:6, Radio:28, Input:1, GPS:3, Power:3 |
| tlora-t3s3-v1 | LILYGO T-LoRa T3-S3 | 16 | TLORA_T3_S3 | variants/esp32s3/tlora_t3s3_v1 | Display:1, Radio:36, Input:1, Power:3 |
| unphone | unPhone | 59 | UNPHONE | variants/esp32s3/unphone | Display:9, Radio:9, Input:3, GPS:1, Power:2, Connectivity/Other:1 |
| unphone-tft | | | | variants/esp32s3/unphone | Display:9, Radio:9, Input:3, GPS:1, Power:2, Connectivity/Other:1 |
### esp32s2
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| -------------- | ------------ | -------- | ------- | ------------------------------- | -------------------------------------- |
| nugget-s2-lora | | | | variants/esp32s2/nugget_s2_lora | Radio:9, Input:1, Connectivity/Other:1 |
### native
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| ---------------- | ------------ | -------- | ------- | ----------------------------------- | ----------------- |
| buildroot | | | | variants/native/portduino-buildroot | Display:2, GPS:1 |
| coverage | | | | variants/native/portduino | Display:2, GPS:1 |
| native | | | | variants/native/portduino | Display:2, GPS:1 |
| native-fb | | | | variants/native/portduino | Display:2, GPS:1 |
| native-tft | | | | variants/native/portduino | Display:2, GPS:1 |
| native-tft-debug | | | | variants/native/portduino | Display:2, GPS:1 |
### nrf52840
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| -------------------------------- | -------------------------- | -------- | ------------------------- | ---------------------------------------------- | --------------------------------------------------------- |
| ME25LS01-4Y10TD | | | | variants/nrf52840/ME25LS01-4Y10TD | Radio:8, Input:1, GPS:8, Power:4 |
| ME25LS01-4Y10TD_e-ink | | | | variants/nrf52840/ME25LS01-4Y10TD_e-ink | Display:6, Radio:8, Input:1, GPS:8, Power:4 |
| TWC_mesh_v4 | | | | variants/nrf52840/TWC_mesh_v4 | Display:1, Radio:7, GPS:4, Power:5 |
| canaryone | Canary One | 29 | CANARYONE | variants/nrf52840/canaryone | Radio:7, GPS:8, Power:5 |
| feather_diy | | | | variants/nrf52840/feather_diy | Radio:17, Input:1 |
| gat562_mesh_trial_tracker | | | | variants/nrf52840/gat562_mesh_trial_tracker | Display:2, Radio:8, GPS:4, Power:5 |
| heltec-mesh-node-t096 | Heltec Mesh Node 096 | 127 | HELTEC_MESH_NODE_T096 | variants/nrf52840/heltec_mesh_node_t096 | Display:8, Radio:11, GPS:10, Power:9 |
| heltec-mesh-node-t114 | Heltec Mesh Node T114 | 69 | HELTEC_MESH_NODE_T114 | variants/nrf52840/heltec_mesh_node_t114 | Display:7, Radio:8, GPS:7, Power:8, Connectivity/Other:1 |
| heltec-mesh-node-t114-inkhud | | | | variants/nrf52840/heltec_mesh_node_t114-inkhud | Display:6, Radio:8, GPS:7, Power:7, Connectivity/Other:1 |
| heltec-mesh-pocket-10000 | Heltec Mesh Pocket | 94 | HELTEC_MESH_POCKET | variants/nrf52840/heltec_mesh_pocket | Display:6, Radio:8, GPS:1, Power:7 |
| heltec-mesh-pocket-10000-inkhud | Heltec Mesh Pocket | 94 | HELTEC_MESH_POCKET | variants/nrf52840/heltec_mesh_pocket | Display:6, Radio:8, GPS:1, Power:7 |
| heltec-mesh-pocket-5000 | Heltec Mesh Pocket | 94 | HELTEC_MESH_POCKET | variants/nrf52840/heltec_mesh_pocket | Display:6, Radio:8, GPS:1, Power:7 |
| heltec-mesh-pocket-5000-inkhud | Heltec Mesh Pocket | 94 | HELTEC_MESH_POCKET | variants/nrf52840/heltec_mesh_pocket | Display:6, Radio:8, GPS:1, Power:7 |
| heltec-mesh-solar | Heltec MeshSolar | 108 | HELTEC_MESH_SOLAR | variants/nrf52840/heltec_mesh_solar | Radio:8, GPS:6 |
| heltec-mesh-solar-eink | | | | variants/nrf52840/heltec_mesh_solar | Radio:8, GPS:6 |
| heltec-mesh-solar-inkhud | | | | variants/nrf52840/heltec_mesh_solar | Radio:8, GPS:6 |
| heltec-mesh-solar-oled | | | | variants/nrf52840/heltec_mesh_solar | Radio:8, GPS:6 |
| heltec-mesh-solar-tft | | | | variants/nrf52840/heltec_mesh_solar | Radio:8, GPS:6 |
| makerpython_nrf52840_sx1280_eink | | | | variants/nrf52840/MakePython_nRF52840_eink | Display:6, Radio:6, GPS:4, Power:5 |
| makerpython_nrf52840_sx1280_oled | | | | variants/nrf52840/MakePython_nRF52840_oled | Radio:6, GPS:4, Power:5 |
| meshlink | | | | variants/nrf52840/meshlink | Display:6, Radio:7, Input:1, GPS:5, Power:5 |
| meshlink_eink | | | | variants/nrf52840/meshlink | Display:6, Radio:7, Input:1, GPS:5, Power:5 |
| meshtiny | | | | variants/nrf52840/meshtiny | Display:2, Radio:8, Input:5, Power:5 |
| minimesh_lite | | | | variants/nrf52840/dls_Minimesh_Lite | Radio:15, Input:1, GPS:4, Power:6 |
| monteops_hw1 | | | | variants/nrf52840/monteops_hw1 | Radio:8, GPS:3, Power:5, Connectivity/Other:1 |
| ms24sf1 | | | | variants/nrf52840/MS24SF1 | Radio:8, Input:1, GPS:8, Power:4 |
| muzi-base | muzi BASE | 93 | MUZI_BASE | variants/nrf52840/muzi_base | Display:3, Radio:19, Input:1, GPS:4, Power:5 |
| nano-g2-ultra | Nano G2 Ultra | 18 | NANO_G2_ULTRA | variants/nrf52840/nano-g2-ultra | Display:1, Radio:7, GPS:4, Power:6, Connectivity/Other:1 |
| nrf52_promicro_diy_tcxo | NRF52 Pro-micro DIY | 63 | NRF52_PROMICRO_DIY | variants/nrf52840/diy/nrf52_promicro_diy_tcxo | Display:4, Radio:29, Input:1, GPS:4, Power:6 |
| pca10059_diy_eink | | | | variants/nrf52840/Dongle_nRF52840-pca10059-v1 | Display:7, Radio:8, GPS:4, Power:5 |
| r1-neo | muzi R1 Neo | 101 | MUZI_R1_NEO | variants/nrf52840/r1-neo | Display:1, Radio:8, GPS:4, Power:5 |
| rak2560 | RAK WisMesh Repeater | 22 | WISMESH_HUB | variants/nrf52840/rak2560 | Display:6, Radio:8, GPS:2, Power:5 |
| rak3401-1watt | RAK3401 1W | 117 | RAK3401 | variants/nrf52840/rak3401_1watt | Display:6, Radio:12, GPS:3, Power:5 |
| rak4631 | RAK WisBlock 4631 | 9 | RAK4631 | variants/nrf52840/rak4631 | Display:6, Radio:8, GPS:3, Power:7, Connectivity/Other:1 |
| rak4631_dbg | | | | variants/nrf52840/rak4631 | Display:6, Radio:8, GPS:3, Power:7, Connectivity/Other:1 |
| rak4631_eink | | | | variants/nrf52840/rak4631_epaper | Display:6, Radio:8, GPS:4, Power:5 |
| rak4631_eink_onrxtx | | | | variants/nrf52840/rak4631_epaper_onrxtx | Display:6, Radio:8, Power:1 |
| rak4631_eth_gw | | | | variants/nrf52840/rak4631_eth_gw | Display:6, Radio:8, GPS:3, Power:5, Connectivity/Other:1 |
| rak4631_eth_gw_dbg | | | | variants/nrf52840/rak4631_eth_gw | Display:6, Radio:8, GPS:3, Power:5, Connectivity/Other:1 |
| rak4631_nomadstar_meteor_pro | NomadStar Meteor Pro | 96 | NOMADSTAR_METEOR_PRO | variants/nrf52840/rak4631_nomadstar_meteor_pro | Display:6, Radio:8, GPS:3, Power:5, Connectivity/Other:1 |
| rak4631_nomadstar_meteor_pro_dbg | | | | variants/nrf52840/rak4631_nomadstar_meteor_pro | Display:6, Radio:8, GPS:3, Power:5, Connectivity/Other:1 |
| rak_wismeshtag | RAK WisMesh Tag | 105 | WISMESH_TAG | variants/nrf52840/rak_wismeshtag | Display:7, Radio:8, GPS:4, Power:5 |
| rak_wismeshtap | RAK WisMesh Tap | 84 | WISMESH_TAP | variants/nrf52840/rak_wismeshtap | Display:21, Radio:8, GPS:3, Power:7, Connectivity/Other:1 |
| seeed_solar_node | Seeed SenseCAP Solar Node | 95 | SEEED_SOLAR_NODE | variants/nrf52840/seeed_solar_node | Radio:9, Input:2, GPS:8, Power:3 |
| seeed_wio_tracker_L1 | Seeed Wio Tracker L1 | 99 | SEEED_WIO_TRACKER_L1 | variants/nrf52840/seeed_wio_tracker_L1 | Display:2, Radio:9, Input:1, GPS:7, Power:3 |
| seeed_wio_tracker_L1_eink | Seeed Wio Tracker L1 E-Ink | 100 | SEEED_WIO_TRACKER_L1_EINK | variants/nrf52840/seeed_wio_tracker_L1_eink | Display:7, Radio:9, Input:1, GPS:7, Power:3 |
| seeed_wio_tracker_L1_eink-inkhud | | | | variants/nrf52840/seeed_wio_tracker_L1_eink | Display:7, Radio:9, Input:1, GPS:7, Power:3 |
| seeed_xiao_nrf52840_kit | Seeed Xiao NRF52840 Kit | 88 | XIAO_NRF52_KIT | variants/nrf52840/seeed_xiao_nrf52840_kit | Radio:19, Input:2, GPS:9, Power:6 |
| seeed_xiao_nrf52840_kit_i2c | | | | variants/nrf52840/seeed_xiao_nrf52840_kit | Radio:19, Input:2, GPS:9, Power:6 |
| t-echo | LILYGO T-Echo | 7 | T_ECHO | variants/nrf52840/t-echo | Display:7, Radio:8, GPS:7, Power:6, Connectivity/Other:1 |
| t-echo-inkhud | | | | variants/nrf52840/t-echo | Display:7, Radio:8, GPS:7, Power:6, Connectivity/Other:1 |
| t-echo-lite | LILYGO T-Echo Lite | 109 | T_ECHO_LITE | variants/nrf52840/t-echo-lite | Display:6, Radio:9, GPS:10, Power:8 |
| t-echo-plus | | | | variants/nrf52840/t-echo-plus | Display:8, Radio:8, GPS:7, Power:6 |
| thinknode_m1 | ThinkNode M1 | 89 | THINKNODE_M1 | variants/nrf52840/ELECROW-ThinkNode-M1 | Display:7, Radio:8, Input:1, GPS:8, Power:8 |
| thinknode_m1-inkhud | | | | variants/nrf52840/ELECROW-ThinkNode-M1 | Display:7, Radio:8, Input:1, GPS:8, Power:8 |
| thinknode_m3 | Elecrow ThinkNode M3 | 115 | THINKNODE_M3 | variants/nrf52840/ELECROW-ThinkNode-M3 | Radio:1, Input:2, GPS:8, Power:7, Connectivity/Other:1 |
| thinknode_m4 | | | | variants/nrf52840/ELECROW-ThinkNode-M4 | Radio:8, GPS:12, Power:7 |
| thinknode_m6 | ThinkNode M6 | 120 | THINKNODE_M6 | variants/nrf52840/ELECROW-ThinkNode-M6 | Radio:7, GPS:9, Power:9, Connectivity/Other:1 |
| tracker-t1000-e | Seeed SenseCAP T1000-E | 71 | TRACKER_T1000_E | variants/nrf52840/tracker-t1000-e | Display:1, Radio:8, Input:1, GPS:13, Power:5 |
| wio-sdk-wm1110 | | | | variants/nrf52840/wio-sdk-wm1110 | Radio:8, Input:1 |
| wio-t1000-s | | | | variants/nrf52840/wio-t1000-s | Radio:8, Input:1, GPS:12, Power:4 |
| wio-tracker-wm1110 | Seeed Wio WM1110 Tracker | 21 | WIO_WM1110 | variants/nrf52840/wio-tracker-wm1110 | Radio:8, Input:1, GPS:3 |
### rp2040
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| -------------------- | ------------------- | -------- | ----------- | ------------------------------------ | ------------------------------------------------ |
| catsniffer | | | | variants/rp2040/ec_catsniffer | Display:1, Radio:17, GPS:1 |
| challenger_2040_lora | | | | variants/rp2040/challenger_2040_lora | Radio:17, Input:1, Power:1 |
| feather_rp2040_rfm95 | | | | variants/rp2040/feather_rp2040_rfm95 | Radio:17, Input:1, Power:1 |
| nibble-rp2040 | | | | variants/rp2040/nibble_rp2040 | Radio:9, Input:1 |
| pico | Raspberry Pi Pico | 47 | RPI_PICO | variants/rp2040/rpipico | Radio:16, Input:1, Power:4 |
| pico_slowclock | | | | variants/rp2040/rpipico-slowclock | Display:2, Radio:16, Input:1, GPS:5, Power:4 |
| picow | Raspberry Pi Pico W | 47 | RPI_PICO | variants/rp2040/rpipicow | Radio:16, Input:1, Power:4, Connectivity/Other:1 |
| rak11310 | RAK WisBlock 11310 | 26 | RAK11310 | variants/rp2040/rak11310 | Radio:17, Input:1, Power:3, Connectivity/Other:1 |
| rp2040-lora | RP2040 LoRa | 30 | RP2040_LORA | variants/rp2040/rp2040-lora | Radio:18, Input:1, Power:1 |
| senselora_rp2040 | | | | variants/rp2040/senselora_rp2040 | Display:1, Radio:9, Input:1, Power:1 |
### rp2350
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| ----------- | ------------ | -------- | ------- | ------------------------- | ------------------------------------------------ |
| pico2 | | | | variants/rp2350/rpipico2 | Radio:16, Input:1, Power:4 |
| pico2w | | | | variants/rp2350/rpipico2w | Radio:16, Input:1, Power:4, Connectivity/Other:1 |
### stm32
| Environment | Display Name | HW Model | HW Slug | Variant Dir | Common Categories |
| --------------- | ------------ | -------- | ------- | ------------------------------ | ---------------------------------- |
| CDEBYTE_E77-MBL | | | | variants/stm32/CDEBYTE_E77-MBL | Display:1 |
| milesight_gs301 | | | | variants/stm32/milesight_gs301 | Display:1, Radio:1, Input:1 |
| rak3172 | | | | variants/stm32/rak3172 | Display:1 |
| russell | | | | variants/stm32/russell | Display:1, Radio:1, Input:1, GPS:4 |
| wio-e5 | | | | variants/stm32/wio-e5 | Display:1 |
## Representative Board Examples
### diy: `9m2ibr_aprs_lora_tracker`
- Variant directory: `variants/esp32/diy/9m2ibr_aprs_lora_tracker`
- Input examples:
- `BUTTON_PIN` = `15`
- Radio examples:
- `LORA_SCK` = `18`
- `LORA_MISO` = `19`
- `LORA_MOSI` = `23`
- `LORA_CS` = `5`
- `LORA_DIO0` = `26`
- `LORA_RESET` = `27`
- `LORA_DIO1` = `12`
- `LORA_DIO2` = `RADIOLIB_NC`
- GPS examples:
- `GPS_RX_PIN` = `16`
- `GPS_TX_PIN` = `17`
- Display examples:
- `HAS_SCREEN` = `1`
- I2C/SPI examples:
- `I2C_SDA` = `21`
- `I2C_SCL` = `22`
- Power examples:
- `BATTERY_PIN` = `35`
- `ADC_MULTIPLIER` = `2.01`
- `ADC_CHANNEL` = `ADC1_GPIO35_CHANNEL`
- `BATTERY_SENSE_RESOLUTION_BITS` = `ADC_RESOLUTION`
### esp32: `betafpv_2400_tx_micro`
- Variant directory: `variants/esp32/betafpv_2400_tx_micro`
- Input examples:
- `BUTTON_PIN` = `25`
- Radio examples:
- `LORA_SCK` = `18`
- `LORA_MISO` = `19`
- `LORA_MOSI` = `23`
- `LORA_CS` = `5`
- `RF95_FAN_EN` = `17`
- `USE_SX1280` = `1`
- `LORA_RESET` = `14`
- `SX128X_CS` = `5`
- I2C/SPI examples:
- `I2C_SDA` = `22`
- `I2C_SCL` = `32`
- Connectivity/Other examples:
- `HAS_NEOPIXEL` = `1`
### esp32-c3: `ai-c3`
- Variant directory: `variants/esp32c3/ai-c3`
- Input examples:
- `BUTTON_PIN` = `9`
- Radio examples:
- `USE_RF95` = `1`
- `LORA_SCK` = `4`
- `LORA_MISO` = `5`
- `LORA_MOSI` = `6`
- `LORA_CS` = `7`
- `LORA_DIO0` = `10`
- `LORA_DIO1` = `3`
- `LORA_RESET` = `2`
- GPS examples:
- `HAS_GPS` = `0`
- I2C/SPI examples:
- `I2C_SDA` = `SDA`
- `I2C_SCL` = `SCL`
### esp32-c6: `m5stack-unitc6l`
- Variant directory: `variants/esp32c6/m5stack_unitc6l`
- `custom_meshtastic_hw_model`: `111`
- `custom_meshtastic_hw_model_slug`: `M5STACK_C6L`
- `custom_meshtastic_architecture`: `esp32-c6`
- `custom_meshtastic_actively_supported`: `true`
- `custom_meshtastic_support_level`: `1`
- `custom_meshtastic_display_name`: `M5Stack Unit C6L`
- `custom_meshtastic_images`: `m5_c6l.svg`
- `custom_meshtastic_tags`: `M5Stack`
- Radio examples:
- `USE_SX1262` = `1`
- `LORA_MISO` = `22`
- `LORA_SCK` = `20`
- `LORA_MOSI` = `21`
- `LORA_CS` = `23`
- `LORA_RESET` = `RADIOLIB_NC`
- `LORA_DIO1` = `7`
- `LORA_BUSY` = `19`
- GPS examples:
- `HAS_GPS` = `1`
- `GPS_RX_PIN` = `4`
- `GPS_TX_PIN` = `5`
- Display examples:
- `SCREEN_TRANSITION_FRAMERATE` = `10`
- I2C/SPI examples:
- `I2C_SDA` = `10`
- `I2C_SCL` = `8`
- Connectivity/Other examples:
- `HAS_NEOPIXEL` = `1`
### esp32-s3: `CDEBYTE_EoRa-Hub`
- Variant directory: `variants/esp32s3/CDEBYTE_EoRa-Hub`
- Input examples:
- `BUTTON_PIN` = `0`
- Radio examples:
- `USE_LR1121` = `1`
- `LORA_SCK` = `9`
- `LORA_MOSI` = `10`
- `LORA_MISO` = `11`
- `LORA_RESET` = `12`
- `LORA_CS` = `8`
- `LORA_DIO9` = `13`
- `LR1121_IRQ_PIN` = `14`
- Display examples:
- `HAS_SCREEN` = `1`
- `USE_SSD1306` = `1`
- I2C/SPI examples:
- `I2C_SCL` = `17`
- `I2C_SDA` = `18`
- `I2C_SCL1` = `21`
- `I2C_SDA1` = `10`
- Power examples:
- `BATTERY_PIN` = `1`
- `ADC_CHANNEL` = `ADC1_GPIO1_CHANNEL`
- `ADC_MULTIPLIER` = `103.0`
- `ADC_ATTENUATION` = `ADC_ATTEN_DB_0`
- `ADC_CTRL` = `37`
- `ADC_CTRL_ENABLED` = `LOW`
### esp32s2: `nugget-s2-lora`
- Variant directory: `variants/esp32s2/nugget_s2_lora`
- Input examples:
- `BUTTON_PIN` = `0`
- Radio examples:
- `USE_RF95` = `1`
- `LORA_SCK` = `6`
- `LORA_MISO` = `8`
- `LORA_MOSI` = `10`
- `LORA_CS` = `13`
- `LORA_DIO0` = `16`
- `LORA_RESET` = `5`
- `LORA_DIO1` = `RADIOLIB_NC`
- I2C/SPI examples:
- `I2C_SDA` = `34`
- `I2C_SCL` = `36`
- Connectivity/Other examples:
- `HAS_NEOPIXEL` = `1`
### native: `buildroot`
- Variant directory: `variants/native/portduino-buildroot`
- GPS examples:
- `HAS_GPS` = `1`
- Display examples:
- `HAS_SCREEN` = `1`
- `USE_TFTDISPLAY` = `1`
### nrf52840: `ME25LS01-4Y10TD`
- Variant directory: `variants/nrf52840/ME25LS01-4Y10TD`
- Input examples:
- `BUTTON_PIN` = `(0 + 27)`
- Radio examples:
- `LORA_RESET` = `(32 + 11)`
- `LORA_DIO1` = `(32 + 12)`
- `LORA_DIO2` = `(32 + 10)`
- `LORA_SCK` = `PIN_SPI_SCK`
- `LORA_MISO` = `PIN_SPI_MISO`
- `LORA_MOSI` = `PIN_SPI_MOSI`
- `LORA_CS` = `PIN_SPI_NSS`
- `USE_LR1110` = `1`
- GPS examples:
- `HAS_GPS` = `0`
- `PIN_GPS_EN` = `-1`
- `GPS_EN_ACTIVE` = `HIGH`
- `PIN_GPS_RESET` = `-1`
- `GPS_VRTC_EN` = `-1`
- `GPS_SLEEP_INT` = `-1`
- `GPS_RTC_INT` = `-1`
- `GPS_RESETB_OUT` = `-1`
- I2C/SPI examples:
- `WIRE_INTERFACES_COUNT` = `1`
- `SPI_INTERFACES_COUNT` = `1`
- `PIN_SPI_MISO` = `(0 + 29)`
- `PIN_SPI_MOSI` = `(0 + 2)`
- `PIN_SPI_SCK` = `(32 + 15)`
- `PIN_SPI_NSS` = `(32 + 13)`
- Power examples:
- `BATTERY_PIN` = `-1`
- `ADC_MULTIPLIER` = `(2.0F)`
- `ADC_RESOLUTION` = `14`
- `BATTERY_SENSE_RESOLUTION_BITS` = `12`
### rp2040: `catsniffer`
- Variant directory: `variants/rp2040/ec_catsniffer`
- Radio examples:
- `USE_SX1262` = `1`
- `LORA_SCK` = `18`
- `LORA_MISO` = `16`
- `LORA_MOSI` = `19`
- `LORA_CS` = `17`
- `LORA_DIO0` = `5`
- `LORA_RESET` = `24`
- `LORA_DIO1` = `4`
- GPS examples:
- `HAS_GPS` = `0`
- Display examples:
- `HAS_SCREEN` = `0`
### rp2350: `pico2`
- Variant directory: `variants/rp2350/rpipico2`
- Input examples:
- `BUTTON_PIN` = `17`
- Radio examples:
- `USE_SX1262` = `1`
- `LORA_SCK` = `10`
- `LORA_MISO` = `12`
- `LORA_MOSI` = `11`
- `LORA_CS` = `3`
- `LORA_DIO0` = `RADIOLIB_NC`
- `LORA_RESET` = `15`
- `LORA_DIO1` = `20`
- Power examples:
- `EXT_NOTIFY_OUT` = `22`
- `BATTERY_PIN` = `26`
- `ADC_MULTIPLIER` = `3.1`
- `BATTERY_SENSE_RESOLUTION_BITS` = `ADC_RESOLUTION`
### stm32: `CDEBYTE_E77-MBL`
- Variant directory: `variants/stm32/CDEBYTE_E77-MBL`
- Display examples:
- `USE_STM32WLx` = `1`
## Intake Guidance For New Hardware
When using this context to add a new board, collect these inputs before generating files:
- PlatformIO environment name
- `custom_meshtastic_hw_model` and `custom_meshtastic_hw_model_slug`
- Display name and architecture
- Whether the board is actively supported and its support level
- Partition scheme, DFU requirement, and image/tag metadata if applicable
- Radio chip family and complete radio pin group
- Input/button/rotary/keyboard pins
- Display interface pins and driver-related macros
- GPS, power-management, I2C, SPI, storage, and auxiliary peripheral definitions
- Any board-specific initialization that requires `variant.cpp` or extra variant hooks
## Inherited Defaults Note
Some architecture families rely on BSP (Board Support Package) or base-environment defaults rather than declaring every pin or capability macro explicitly in `variant.h`. When adding a new board for one of these families, check the relevant BSP headers before assuming a missing define means a feature is absent.
Directories scanned that had no `variant.h` (relying entirely on BSP/base-environment): diy: 4, esp32: 3, esp32s3: 1
### nrf52840
- VARIANT_MCK — nRF52 BSP clock constant (e.g., 64000000ul); always inherited from BSP unless overridden.
- USE_LFXO — low-frequency crystal oscillator selection; declared locally only when the board uses LFXO rather than the RC oscillator.
- PIN*SPI*_ / PIN*SPI1*_ — SPI bus pin numbers come from the BSP variant table; boards override only when the LoRa radio or display uses non-default SPI routing.
- WIRE_INTERFACES_COUNT / SPI_INTERFACES_COUNT — bus count comes from BSP; explicitly set only when the board deviates.
- LED_BLUE / PIN_LED1 / PINS_COUNT / NUM_DIGITAL_PINS — standard BSP pin-table entries inherited from the nRF52 Arduino core.
### rp2040
- PIN*SPI*\* — primary SPI pins come from the RP2040 Arduino BSP; most boards declare them explicitly, but the defaults align with the Pico pin assignments.
- NUM_DIGITAL_PINS / NUM_ANALOG_INPUTS — Arduino BSP counts; rarely overridden locally.
### stm32
- Radio and pin assignments for STM32WL targets are largely internal to the WL SoC and declared via STM32 HAL/BSP headers; variant.h files are minimal.
- USE_STM32WLx is typically the only explicit define; all other radio config comes from the BSP.
### native
- The native/Portduino target uses runtime configuration rather than compile-time pin defines; variant.h only sets display and GPS stubs.
## Cautions
- Some boards rely on architecture defaults rather than declaring every field locally.
- Some board families expose multiple environments or display variants that share one hardware model.
- Source materials such as schematics still need human verification before new pin mappings are trusted.
- This document is a starting context artifact, not proof that a new board definition is safe to merge.
+67 -9
View File
@@ -166,15 +166,73 @@ rather than auto-`sudo`'ing mid-run.
## Environment variables
| Var | Default | Purpose |
| -------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- |
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
| Var | Default | Purpose |
| -------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
| `MESHTASTIC_MCP_TCP_HOST` | unset | `host` or `host:port` of a `meshtasticd` daemon to surface as a TCP device (see "TCP / native-host nodes" below) |
## TCP / native-host nodes
The `native-macos` and `native` PlatformIO envs build a headless `meshtasticd`
binary that runs on the host (Apple Silicon / Intel macOS, or Linux Portduino).
The daemon exposes the meshtastic TCP API on port `4403` rather than a USB
serial endpoint — point the MCP server at it via `MESHTASTIC_MCP_TCP_HOST`:
```bash
# 1. Build + run a daemon on this host (see variants/native/portduino/platformio.ini
# for full Homebrew prereqs and CH341 LoRa-adapter setup).
pio run -e native-macos
~/.meshtasticd/meshtasticd
# 2. Point the MCP server at it.
export MESHTASTIC_MCP_TCP_HOST=localhost # or host:port, default port 4403
```
**First-run gotcha — MAC address.** `meshtasticd` derives its MAC from the
USB adapter's serial-number / product strings. Many cheap CH341 dongles
(MeshStick included — VID 0x1A86 / PID 0x5512) ship with `iSerialNumber=0`
and `iProduct=0`, so the daemon aborts on boot with `*** Blank MAC Address
not allowed!`. Set the MAC explicitly in `config.yaml`:
```yaml
# Under General:
MACAddress: 02:CA:FE:BA:BE:01
```
Use a locally-administered address (first byte's second-LSB set, e.g.
`02:*` / `06:*` / `0A:*` / `0E:*`) to avoid colliding with a real OUI.
There is also a `--hwid AA:BB:CC:DD:EE:FF` CLI flag visible in
`meshtasticd --help`, but it is **currently broken** in
`MAC_from_string()` (`src/platform/portduino/PortduinoGlue.cpp`): the
function strips colons from its parameter but then reads bytes from the
global `portduino_config.mac_address`, so `--hwid` is silently overridden
when `MACAddress:` is also set, and crashes the daemon (uncaught
`std::invalid_argument: stoi: no conversion`) when it isn't. Use the YAML
form until that's fixed upstream.
`list_devices` will surface the daemon as `tcp://localhost:4403` with
`likely_meshtastic=True`, so `device_info`, `list_nodes`, `get_config`,
`set_config`, `set_owner`, `send_text`, `userprefs_*`, and the admin RPCs
auto-select it when no `port` is passed. Pass `port="tcp://other-host:9999"`
explicitly to target a different daemon.
**Tools that don't apply to a TCP/native node** (no USB hardware to operate
on) raise a clear `ConnectionError` rather than failing mysteriously:
`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`,
`serial_open` (use info/admin tools directly), and the vendor escape hatches
`esptool_*`, `nrfutil_*`, `picotool_*`. `pio_flash` against a `native*` env
similarly raises — there's no upload step; use `build` and run the binary
directly.
The pytest harness in `tests/` still assumes USB-attached devices per role —
TCP-aware fixtures are not part of this surface yet.
## Hardware Test Suite
+157 -12
View File
@@ -1,4 +1,4 @@
"""Context manager for meshtastic.SerialInterface connections.
"""Context manager for meshtastic interface connections (serial + TCP).
Every info/admin tool goes through `connect(port)` so we have a single place
that:
@@ -6,8 +6,16 @@ that:
- fails fast if a serial_session is already holding the port,
- guarantees `.close()` is called, even on exception.
The `SerialInterface` blocks on construction waiting for the node database;
that's fine for v1 since every tool is a short-lived request.
Two transports:
- Serial: USB-attached firmware on `/dev/cu.*` / `/dev/ttyUSB*` / `COM*`.
- TCP: a `meshtasticd` daemon (e.g. the native macOS / Linux Portduino
headless build) addressed as `tcp://host[:port]` (default port 4403).
Surfaced by `devices.list_devices()` when `MESHTASTIC_MCP_TCP_HOST` is
set, so `resolve_port(None)` auto-selects it like a USB candidate.
Both `SerialInterface` and `TCPInterface` block on construction waiting for
the node database; that's fine for v1 since every tool is a short-lived
request.
"""
from __future__ import annotations
@@ -17,20 +25,107 @@ from typing import Iterator
from . import devices, registry
DEFAULT_TCP_PORT = 4403
TCP_SCHEME = "tcp://"
TCP_HOST_ENV = "MESHTASTIC_MCP_TCP_HOST"
class ConnectionError(RuntimeError):
pass
def is_tcp_port(port: str | None) -> bool:
return bool(port) and port.startswith(TCP_SCHEME)
def parse_tcp_port(port: str) -> tuple[str, int]:
"""Parse `tcp://host[:port]` → (host, port). Defaults to 4403.
Validates host shape (non-empty, no path separators) and port range
(1..65535). Raises `ConnectionError` on malformed input — never lets
a raw `ValueError` bubble up to a tool surface.
"""
if not port.startswith(TCP_SCHEME):
raise ConnectionError(
f"Invalid TCP endpoint {port!r}: expected '{TCP_SCHEME}host[:port]'."
)
rest = port[len(TCP_SCHEME) :]
if ":" in rest:
host, port_str = rest.rsplit(":", 1)
try:
tcp_port = int(port_str)
except ValueError as e:
raise ConnectionError(
f"Invalid TCP endpoint {port!r}: port {port_str!r} is not an integer."
) from e
else:
host, tcp_port = rest, DEFAULT_TCP_PORT
if not host:
raise ConnectionError(f"Invalid TCP endpoint {port!r}: empty host.")
if any(c in host for c in ("/", "\\")):
raise ConnectionError(
f"Invalid TCP endpoint {port!r}: host {host!r} contains a path "
"separator. TCP hostnames cannot contain '/' or '\\' — did you "
"pass a serial port path or a Windows drive path by mistake?"
)
if not (1 <= tcp_port <= 65535):
raise ConnectionError(
f"Invalid TCP endpoint {port!r}: port {tcp_port} out of range "
"(must be 1..65535)."
)
return host, tcp_port
def normalize_tcp_endpoint(endpoint: str) -> str:
r"""Normalize `host`, `host:port`, or `tcp://host[:port]` → canonical
`tcp://host:port` form. One place that owns the lock-key shape.
Defers all validation to `parse_tcp_port`, so path-like inputs
(`/dev/cu.foo`, `C:\Windows\…`), empty hosts, non-integer ports,
and out-of-range ports raise `ConnectionError` here too.
"""
if endpoint.startswith(TCP_SCHEME):
canonical = endpoint
elif ":" in endpoint:
canonical = f"{TCP_SCHEME}{endpoint}"
else:
canonical = f"{TCP_SCHEME}{endpoint}:{DEFAULT_TCP_PORT}"
host, port = parse_tcp_port(canonical)
return f"{TCP_SCHEME}{host}:{port}"
def reject_if_tcp(port: str | None, tool_name: str) -> None:
"""Raise if `port` is a TCP endpoint — for tools that need real USB
hardware (flash, bootloader, vendor escape hatches, serial monitor).
Only checks the explicit arg; auto-selection via env var is the caller's
responsibility to handle if it matters.
"""
if is_tcp_port(port):
raise ConnectionError(
f"{tool_name} is not applicable to TCP/native nodes ({port}). "
"This tool requires USB-attached hardware."
)
def resolve_port(port: str | None) -> str:
"""Pick a port: explicit > sole likely_meshtastic candidate > error."""
"""Pick a port: explicit > sole likely_meshtastic candidate > error.
A `tcp://` string passes through (after canonicalization). When `port`
is None and no USB candidates are present, `MESHTASTIC_MCP_TCP_HOST`
is consulted via `devices.list_devices()`.
"""
if port:
if is_tcp_port(port):
return normalize_tcp_endpoint(port)
return port
candidates = [d for d in devices.list_devices() if d["likely_meshtastic"]]
if not candidates:
raise ConnectionError(
"No Meshtastic devices detected. Plug one in or pass `port` explicitly. "
"Run `list_devices` with include_unknown=True to see all serial ports."
"No Meshtastic devices detected. Plug one in, set "
f"{TCP_HOST_ENV}=<host[:port]> for a meshtasticd daemon, "
"or pass `port` explicitly. Run `list_devices` with "
"include_unknown=True to see all serial ports."
)
if len(candidates) > 1:
ports = ", ".join(c["port"] for c in candidates)
@@ -43,17 +138,62 @@ def resolve_port(port: str | None) -> str:
@contextmanager
def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
"""Open a `meshtastic.SerialInterface` and always close it.
"""Open a meshtastic interface (serial or TCP) and always close it.
Raises `ConnectionError` immediately if another serial session holds the
port (a `pio device monitor` in `serial_sessions/`, for instance).
For serial: raises `ConnectionError` immediately if another serial
session holds the port (a `pio device monitor` in `serial_sessions/`).
For TCP: no exclusive-access requirement, so the serial-session check
is skipped — but the `port_lock` still serializes parallel `connect()`
calls to the same daemon endpoint.
`timeout_s` is plumbed through to both `SerialInterface(timeout=...)`
and `TCPInterface(timeout=...)`. The meshtastic library uses the value
as the reply-wait deadline for `localNode.waitForConfig()` during
construction and for any subsequent admin RPC. `int()`-converted at
the boundary because the upstream API expects whole seconds.
"""
resolved = resolve_port(port)
timeout = int(timeout_s)
if is_tcp_port(resolved):
from meshtastic.tcp_interface import (
TCPInterface, # type: ignore[import-untyped]
)
host, tcp_port = parse_tcp_port(resolved)
lock = registry.port_lock(resolved)
if not lock.acquire(blocking=False):
raise ConnectionError(
f"TCP endpoint {resolved} is busy — another device operation "
"is in flight. Retry shortly."
)
iface = None
try:
iface = TCPInterface(
hostname=host,
portNumber=tcp_port,
connectNow=True,
noProto=False,
timeout=timeout,
)
yield iface
finally:
if iface is not None:
try:
iface.close()
except Exception:
pass
try:
lock.release()
except RuntimeError:
pass
return
from meshtastic.serial_interface import (
SerialInterface, # type: ignore[import-untyped]
)
resolved = resolve_port(port)
active = registry.active_session_for_port(resolved)
if active is not None:
raise ConnectionError(
@@ -70,7 +210,12 @@ def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
iface = None
try:
iface = SerialInterface(devPath=resolved, connectNow=True, noProto=False)
iface = SerialInterface(
devPath=resolved,
connectNow=True,
noProto=False,
timeout=timeout,
)
yield iface
finally:
if iface is not None:
+63 -3
View File
@@ -1,13 +1,18 @@
"""USB/serial device discovery.
"""USB/serial + TCP device discovery.
Combines the canonical `meshtastic.util.findPorts()` allowlist/blocklist with
the richer metadata (`serial.tools.list_ports.comports()`) so callers see
VID/PID, descriptions, and manufacturer strings alongside the "is this likely
a Meshtastic device" signal.
If `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` is set, a synthetic entry for the
`meshtasticd` daemon at that endpoint is prepended to the result, so
`resolve_port(None)` auto-selects it like a USB candidate.
"""
from __future__ import annotations
import os
from typing import Any
from serial.tools import list_ports
@@ -19,6 +24,45 @@ def _to_hex(value: int | None) -> str | None:
return f"0x{value:04x}"
def _tcp_endpoint_from_env() -> dict[str, Any] | None:
"""Synthesize a TCP device entry from MESHTASTIC_MCP_TCP_HOST, if set.
If the env var is malformed (non-integer port, path-like host, etc.),
return an entry with `likely_meshtastic=False` and the parser error in
the description, rather than raising — `list_devices` is the diagnostic
tool a user reaches for when their env var isn't working, so it must
not crash on misconfiguration.
"""
host = os.environ.get("MESHTASTIC_MCP_TCP_HOST")
if not host:
return None
# Lazy import to avoid a circular dependency (connection imports devices).
from . import connection
try:
port = connection.normalize_tcp_endpoint(host)
description = "meshtasticd (TCP)"
likely = True
except connection.ConnectionError as e:
# Surface the raw env-var value plus the parser's reason so the
# user can see exactly what they set and why it was rejected.
# Don't double the scheme if the user already prefixed `tcp://`.
port = host if host.startswith(connection.TCP_SCHEME) else f"tcp://{host}"
description = f"meshtasticd (TCP) — invalid MESHTASTIC_MCP_TCP_HOST: {e}"
likely = False
return {
"port": port,
"vid": None,
"pid": None,
"description": description,
"manufacturer": None,
"product": None,
"serial_number": None,
"likely_meshtastic": likely,
"blacklisted": False,
}
def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
"""Return enriched info for serial ports, flagging Meshtastic candidates.
@@ -70,6 +114,22 @@ def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
}
)
# Stable ordering: likely_meshtastic first, then by port path
results.sort(key=lambda r: (not r["likely_meshtastic"], r["port"]))
# Append the TCP endpoint (if env var set) and sort everything together.
tcp_entry = _tcp_endpoint_from_env()
if tcp_entry is not None:
results.append(tcp_entry)
# Stable ordering: likely_meshtastic first; within rank, TCP wins over
# USB (explicit env-var configuration takes precedence over USB
# enumeration); then by port path. A misconfigured TCP entry has
# likely_meshtastic=False and lands among the other ignored entries —
# it does NOT pre-empt real USB devices at the top of the list.
results.sort(
key=lambda r: (
not r["likely_meshtastic"],
not r["port"].startswith("tcp://"),
r["port"],
)
)
return results
+18 -1
View File
@@ -17,7 +17,7 @@ from typing import Any
import serial
from . import boards, config, devices, pio, userprefs
from . import boards, config, connection, devices, pio, userprefs
# Meshtastic variants use both `esp32s3` and `esp32-s3` style names across
# variants/*/platformio.ini (no consistency enforced). Accept both spellings.
@@ -46,6 +46,18 @@ def _require_confirm(confirm: bool, operation: str) -> None:
)
def _reject_native_env(env: str, operation: str) -> None:
"""`native*` envs build a host executable, not firmware — there's no
upload step. The user wants `build` (or just runs the binary directly).
"""
if env.startswith("native"):
raise FlashError(
f"{operation} is not applicable for env {env!r}: native envs "
"produce a host executable, not flashable firmware. Use `build` "
"instead, then run the resulting binary directly."
)
def _artifacts_for(env: str) -> list[Path]:
build_dir = config.firmware_root() / ".pio" / "build" / env
if not build_dir.is_dir():
@@ -141,6 +153,8 @@ def flash(
that pio performs will pick up the injected values.
"""
_require_confirm(confirm, "flash")
_reject_native_env(env, "flash")
connection.reject_if_tcp(port, "flash")
with userprefs.temporary_overrides(userprefs_overrides) as effective:
result = pio.run(
["run", "-e", env, "-t", "upload", "--upload-port", port],
@@ -200,6 +214,7 @@ def erase_and_flash(
in that case) since a cached factory.bin would not reflect the new prefs.
"""
_require_confirm(confirm, "erase_and_flash")
connection.reject_if_tcp(port, "erase_and_flash")
_check_esp32_env(env)
if userprefs_overrides and skip_build:
@@ -257,6 +272,7 @@ def update_flash(
overrides are provided we always force a rebuild.
"""
_require_confirm(confirm, "update_flash")
connection.reject_if_tcp(port, "update_flash")
_check_esp32_env(env)
if userprefs_overrides and skip_build:
@@ -391,6 +407,7 @@ def touch_1200bps(
Returns `{ok, former_port, new_port, new_port_vid_pid, attempts}`.
"""
connection.reject_if_tcp(port, "touch_1200bps")
before_list = devices.list_devices(include_unknown=True)
before_ports = {d["port"] for d in before_list}
+6 -1
View File
@@ -16,7 +16,7 @@ import subprocess
from pathlib import Path
from typing import Any, Sequence
from . import config, pio
from . import config, connection, pio
_TIMEOUT_SHORT = 30
_TIMEOUT_LONG = 600
@@ -102,6 +102,7 @@ def _parse_esptool_chip_info(stdout: str) -> dict[str, Any]:
def esptool_chip_info(port: str) -> dict[str, Any]:
connection.reject_if_tcp(port, "esptool_chip_info")
binary = config.esptool_bin()
# `chip_id` prints chip + mac + crystal + features. `flash_id` adds flash.
combined = _run(binary, ["--port", port, "flash_id"], timeout=_TIMEOUT_SHORT)
@@ -116,6 +117,7 @@ def esptool_chip_info(port: str) -> dict[str, Any]:
def esptool_erase_flash(port: str, confirm: bool = False) -> dict[str, Any]:
"""Full-chip erase. Leaves the device unbootable until reflashed."""
_require_confirm(confirm, "esptool_erase_flash")
connection.reject_if_tcp(port, "esptool_erase_flash")
binary = config.esptool_bin()
# esptool v5 uses `erase-flash`, older uses `erase_flash`. Try the new name
# first; if it fails with unknown command, retry old.
@@ -134,6 +136,7 @@ def esptool_raw(
"""Raw esptool passthrough. Destructive subcommands require confirm=True."""
if not args:
raise ToolError("args must not be empty")
connection.reject_if_tcp(port, "esptool_raw")
# Find the first non-flag arg (the subcommand).
subcommand = next((a for a in args if not a.startswith("-")), None)
if subcommand and subcommand.replace("-", "_") in {
@@ -156,6 +159,7 @@ NRFUTIL_DESTRUCTIVE = {"dfu", "settings"}
def nrfutil_dfu(port: str, package_path: str, confirm: bool = False) -> dict[str, Any]:
_require_confirm(confirm, "nrfutil_dfu")
connection.reject_if_tcp(port, "nrfutil_dfu")
pkg = Path(package_path).expanduser()
if not pkg.is_file():
raise ToolError(f"Package not found: {pkg}")
@@ -213,6 +217,7 @@ def _parse_picotool_info(stdout: str) -> dict[str, Any]:
def picotool_info(port: str | None = None) -> dict[str, Any]:
"""Read device info from a Pico in BOOTSEL mode. `port` is informational
only — picotool auto-detects."""
connection.reject_if_tcp(port, "picotool_info")
binary = config.picotool_bin()
res = _run(binary, ["info", "-a"], timeout=_TIMEOUT_SHORT)
if res["exit_code"] != 0:
@@ -71,6 +71,10 @@ def open_session(
If `env` is supplied, pio resolves baud and filters from platformio.ini.
Otherwise uses the supplied `baud` and `filters` (default `['direct']`).
"""
# Lazy import to avoid circular: registry imports serial_session.
from . import connection
connection.reject_if_tcp(port, "serial_open")
args = ["device", "monitor", "--port", port, "--no-reconnect"]
effective_filters: list[str]
effective_baud: int = baud
@@ -0,0 +1,383 @@
"""TCP transport plumbing in connection.py + devices.py.
Pure-Python tests — no real device or daemon required. Mocks `TCPInterface`
when exercising `connect()`.
"""
from __future__ import annotations
from unittest.mock import patch
import pytest
from meshtastic_mcp import connection, devices
# ---------- helpers --------------------------------------------------------
class TestIsTcpPort:
def test_tcp_scheme(self) -> None:
assert connection.is_tcp_port("tcp://localhost") is True
assert connection.is_tcp_port("tcp://localhost:4403") is True
assert connection.is_tcp_port("tcp://192.168.1.50:9999") is True
def test_serial_paths(self) -> None:
assert connection.is_tcp_port("/dev/cu.usbmodem1234") is False
assert connection.is_tcp_port("/dev/ttyUSB0") is False
assert connection.is_tcp_port("COM3") is False
def test_empty_or_none(self) -> None:
assert connection.is_tcp_port(None) is False
assert connection.is_tcp_port("") is False
class TestParseTcpPort:
def test_default_port(self) -> None:
assert connection.parse_tcp_port("tcp://localhost") == ("localhost", 4403)
def test_explicit_port(self) -> None:
assert connection.parse_tcp_port("tcp://localhost:9999") == (
"localhost",
9999,
)
def test_ip_with_port(self) -> None:
assert connection.parse_tcp_port("tcp://192.168.1.50:4403") == (
"192.168.1.50",
4403,
)
class TestNormalizeTcpEndpoint:
def test_bare_host(self) -> None:
assert connection.normalize_tcp_endpoint("localhost") == "tcp://localhost:4403"
def test_host_port(self) -> None:
assert (
connection.normalize_tcp_endpoint("localhost:5000")
== "tcp://localhost:5000"
)
def test_full_url(self) -> None:
assert (
connection.normalize_tcp_endpoint("tcp://1.2.3.4") == "tcp://1.2.3.4:4403"
)
assert (
connection.normalize_tcp_endpoint("tcp://1.2.3.4:9999")
== "tcp://1.2.3.4:9999"
)
def test_idempotent(self) -> None:
once = connection.normalize_tcp_endpoint("localhost:4403")
twice = connection.normalize_tcp_endpoint(once)
assert once == twice == "tcp://localhost:4403"
def test_path_like_endpoint_rejected(self) -> None:
# Serial port paths and Windows drive paths are common config typos
# (someone passes a serial path to MESHTASTIC_MCP_TCP_HOST). Reject
# rather than producing a nonsense `tcp:///dev/cu.foo:4403` URL.
with pytest.raises(connection.ConnectionError, match="path separator"):
connection.normalize_tcp_endpoint("/dev/cu.foo")
with pytest.raises(connection.ConnectionError):
connection.normalize_tcp_endpoint("tcp:///dev/cu.foo:4403")
with pytest.raises(connection.ConnectionError):
connection.normalize_tcp_endpoint(r"C:\Windows\System32")
def test_non_integer_port_rejected(self) -> None:
with pytest.raises(connection.ConnectionError, match="not an integer"):
connection.normalize_tcp_endpoint("tcp://host:notaport")
with pytest.raises(connection.ConnectionError, match="not an integer"):
connection.normalize_tcp_endpoint("host:notaport")
def test_empty_host_rejected(self) -> None:
with pytest.raises(connection.ConnectionError, match="empty host"):
connection.normalize_tcp_endpoint("tcp://:4403")
def test_port_out_of_range_rejected(self) -> None:
with pytest.raises(connection.ConnectionError, match="out of range"):
connection.normalize_tcp_endpoint("tcp://host:0")
with pytest.raises(connection.ConnectionError, match="out of range"):
connection.normalize_tcp_endpoint("tcp://host:65536")
with pytest.raises(connection.ConnectionError, match="out of range"):
connection.normalize_tcp_endpoint("host:99999")
class TestParseTcpPortValidation:
def test_missing_scheme_rejected(self) -> None:
# parse_tcp_port is a low-level helper that requires the scheme.
# Misuse should fail loudly rather than silently mis-parsing.
with pytest.raises(connection.ConnectionError, match="expected"):
connection.parse_tcp_port("localhost:4403")
def test_negative_port_rejected(self) -> None:
with pytest.raises(connection.ConnectionError, match="out of range"):
connection.parse_tcp_port("tcp://host:-1")
# ---------- reject_if_tcp --------------------------------------------------
class TestRejectIfTcp:
def test_rejects_tcp(self) -> None:
with pytest.raises(connection.ConnectionError, match="not applicable"):
connection.reject_if_tcp("tcp://localhost", "esptool_chip_info")
def test_passes_through_serial(self) -> None:
connection.reject_if_tcp("/dev/cu.usbmodem1", "esptool_chip_info") # no raise
def test_passes_through_none(self) -> None:
# None means "auto-detect"; not the explicit-arg case we guard.
connection.reject_if_tcp(None, "esptool_chip_info") # no raise
# ---------- resolve_port ---------------------------------------------------
class TestResolvePort:
def test_explicit_serial_passthrough(self) -> None:
assert connection.resolve_port("/dev/cu.usbmodem999") == "/dev/cu.usbmodem999"
def test_explicit_tcp_normalized(self) -> None:
assert connection.resolve_port("tcp://localhost") == "tcp://localhost:4403"
def test_no_port_no_devices_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch.object(devices, "list_devices", return_value=[]):
with pytest.raises(
connection.ConnectionError, match="No Meshtastic devices"
):
connection.resolve_port(None)
def test_no_port_one_candidate_selected(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
fake = [{"port": "/dev/cu.usbmodem1", "likely_meshtastic": True}]
with patch.object(devices, "list_devices", return_value=fake):
assert connection.resolve_port(None) == "/dev/cu.usbmodem1"
def test_no_port_multiple_candidates_errors(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
fake = [
{"port": "/dev/cu.usbmodem1", "likely_meshtastic": True},
{"port": "/dev/cu.usbmodem2", "likely_meshtastic": True},
]
with patch.object(devices, "list_devices", return_value=fake):
with pytest.raises(connection.ConnectionError, match="Multiple"):
connection.resolve_port(None)
def test_env_var_surfaces_tcp_via_devices(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost")
# Don't patch list_devices — let the real env-var path run, but stub
# the USB enumeration to keep the test hermetic.
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
assert connection.resolve_port(None) == "tcp://localhost:4403"
# ---------- devices.list_devices TCP entry --------------------------------
class TestDevicesTcpEntry:
def test_no_env_var_no_tcp_entry(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices()
assert all(not d["port"].startswith("tcp://") for d in ds)
def test_env_var_adds_tcp_entry(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "myhost:9999")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices()
tcp = [d for d in ds if d["port"].startswith("tcp://")]
assert len(tcp) == 1
assert tcp[0]["port"] == "tcp://myhost:9999"
assert tcp[0]["likely_meshtastic"] is True
assert tcp[0]["description"] == "meshtasticd (TCP)"
def test_tcp_entry_first_in_results(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices()
assert ds, "expected at least the TCP entry"
assert ds[0]["port"].startswith("tcp://")
def test_invalid_env_var_does_not_break_list_devices(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# `list_devices` is the diagnostic tool reached for when an env var
# isn't working — it must not throw on misconfiguration.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices(include_unknown=True)
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
assert len(tcp) == 1
assert tcp[0]["likely_meshtastic"] is False
assert "invalid MESHTASTIC_MCP_TCP_HOST" in tcp[0]["description"]
assert "not an integer" in tcp[0]["description"]
def test_invalid_env_var_excluded_from_resolve_port_autodetect(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# `likely_meshtastic=False` keeps the bad TCP entry out of the
# auto-select path — `resolve_port(None)` should still report
# "no Meshtastic devices" rather than picking a broken endpoint.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
with pytest.raises(connection.ConnectionError, match="No Meshtastic"):
connection.resolve_port(None)
def test_invalid_env_var_does_not_double_tcp_scheme(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# If a user mistakenly sets `MESHTASTIC_MCP_TCP_HOST=tcp://host:bad`,
# the diagnostic entry must surface the raw value as-is rather than
# producing `tcp://tcp://host:bad`.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "tcp://host:notaport")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices(include_unknown=True)
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
assert len(tcp) == 1
assert tcp[0]["port"] == "tcp://host:notaport"
assert "tcp://tcp://" not in tcp[0]["port"]
def test_invalid_env_var_does_not_pre_empt_real_usb_devices(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# Sort ordering: a misconfigured TCP env var must NOT take position 0
# ahead of real USB candidates. Position 0 is reserved for the highest
# rank (likely_meshtastic=True), with TCP-before-USB as a tiebreaker
# within rank.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
# Stub a USB Meshtastic candidate (Espressif VID, port present in
# findPorts).
class FakeInfo:
def __init__(self, device: str, vid: int, pid: int) -> None:
self.device = device
self.vid = vid
self.pid = pid
self.description = "Heltec V3"
self.manufacturer = "Espressif"
self.product = "USB JTAG/serial"
self.serial_number = "abc"
fake_port = FakeInfo("/dev/cu.usbmodem4201", 0x303A, 0x1001)
with patch(
"meshtastic_mcp.devices.list_ports.comports", return_value=[fake_port]
), patch(
"meshtastic.util.findPorts",
return_value=["/dev/cu.usbmodem4201"],
):
ds = devices.list_devices(include_unknown=True)
assert ds, "expected at least the USB + TCP entries"
# Real USB candidate must be at position 0 — it's likely_meshtastic.
assert ds[0]["port"] == "/dev/cu.usbmodem4201"
assert ds[0]["likely_meshtastic"] is True
# The malformed TCP entry exists but lands among the unlikely entries.
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
assert len(tcp) == 1
assert tcp[0]["likely_meshtastic"] is False
assert ds.index(tcp[0]) > 0
def test_likely_tcp_entry_wins_tiebreak_over_usb(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# Conversely, a *valid* TCP env var should sort ahead of USB
# candidates of equal likely_meshtastic rank — explicit env-var
# configuration is a precedence signal.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost:4403")
class FakeInfo:
def __init__(self, device: str, vid: int, pid: int) -> None:
self.device = device
self.vid = vid
self.pid = pid
self.description = "Heltec V3"
self.manufacturer = "Espressif"
self.product = "USB JTAG/serial"
self.serial_number = "abc"
fake_port = FakeInfo("/dev/cu.usbmodem4201", 0x303A, 0x1001)
with patch(
"meshtastic_mcp.devices.list_ports.comports", return_value=[fake_port]
), patch(
"meshtastic.util.findPorts",
return_value=["/dev/cu.usbmodem4201"],
):
ds = devices.list_devices()
assert ds[0]["port"] == "tcp://localhost:4403"
assert ds[0]["likely_meshtastic"] is True
# ---------- connect() routing ---------------------------------------------
class TestConnectRoutesTcp:
def test_connect_uses_tcp_interface_for_tcp_port(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Verify the TCP branch instantiates `TCPInterface(hostname, portNumber)`
and never touches `SerialInterface`."""
# Make sure the env var doesn't leak in and confuse resolve_port.
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp, patch(
"meshtastic.serial_interface.SerialInterface"
) as mock_serial:
mock_tcp.return_value.close.return_value = None
with connection.connect(port="tcp://example.com:1234", timeout_s=12.0):
pass
mock_tcp.assert_called_once_with(
hostname="example.com",
portNumber=1234,
connectNow=True,
noProto=False,
timeout=12,
)
mock_serial.assert_not_called()
def test_connect_plumbs_timeout_to_serial_interface(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Verify the serial branch also propagates `timeout_s` so callers
passing a custom timeout to `device_info` / `list_nodes` / etc. don't
silently get the library default."""
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch("meshtastic.serial_interface.SerialInterface") as mock_serial, patch(
"meshtastic.tcp_interface.TCPInterface"
) as mock_tcp:
mock_serial.return_value.close.return_value = None
with connection.connect(port="/dev/cu.fake", timeout_s=20.0):
pass
mock_serial.assert_called_once_with(
devPath="/dev/cu.fake",
connectNow=True,
noProto=False,
timeout=20,
)
mock_tcp.assert_not_called()
def test_connect_releases_lock_on_tcp_failure(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
mock_tcp.side_effect = RuntimeError("boom")
with pytest.raises(RuntimeError, match="boom"):
with connection.connect(port="tcp://locktest:4403"):
pass
# Lock should be released — a second connect attempt must not fail
# with "busy".
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
mock_tcp.return_value.close.return_value = None
with connection.connect(port="tcp://locktest:4403"):
pass
+1 -1
View File
@@ -126,7 +126,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/1ddcc9da2e60c013d6fc515fb73fb63fac75f9fd.zip
https://github.com/meshtastic/device-ui/archive/4bf593a82100b911ff816dddf7158ffdee2114cd.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -0,0 +1,35 @@
# Specification Quality Checklist: Hardware Support Agent
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-25
**Feature**: [spec.md](/Users/benmeadors/Documents/GitHub/firmware/specs/129-hardware-support-agent/spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Specification validated against the repository constitution on 2026-03-25.
- Initial scope is intentionally bounded to reusable hardware context plus safe board-intake and scaffolding workflow preparation.
@@ -0,0 +1,58 @@
# Contract: Board Intake Request And Assessment
## Purpose
This contract defines the minimum maintainer input and the expected workflow output for the hardware-support-agent feature.
## Input Contract
### Required Fields
- `environment_name`: Proposed PlatformIO environment name for the new board.
- `hardware_model`: Meshtastic hardware model identifier to assign.
- `display_name`: Human-readable board name.
- `architecture`: Target architecture family such as `esp32`, `esp32-s3`, `nrf52840`, `rp2040`, or `stm32`.
### Recommended Fields
- `hardware_model_slug`: Repository-style uppercase slug if already known.
- `actively_supported`: Whether the board is intended to be actively supported.
- `support_level`: Intended `custom_meshtastic_support_level` value.
- `source_materials`: Links, file paths, or notes for schematics, pinouts, datasheets, or vendor pages.
- `board_notes`: Freeform notes about revisions, peripherals, or known uncertainty.
## Validation Rules
- The workflow must reject or pause on missing required fields.
- The workflow must detect conflicts with existing environment names or existing hardware identifiers where discoverable.
- The workflow must not invent radio, power, display, GPS, input, or auxiliary pin mappings.
- The workflow must identify when architecture defaults may be relevant and flag them for human review.
## Output Contract
### Required Assessment Sections
- `expected_artifacts`: What board-support artifacts are likely required.
- `required_metadata`: What `custom_meshtastic_*` metadata and related fields must be decided.
- `matched_patterns`: Existing repository examples that are closest to the request.
- `evidence_gaps`: Missing or conflicting facts that block safe scaffolding.
- `risk_flags`: Conditions that require special maintainer attention.
- `next_actions`: Concrete steps to move the request toward scaffold readiness.
- `scaffold_ready`: Boolean decision indicating whether draft file generation is safe.
## Artifact Expectations
Depending on the board and architecture, the assessment should consider whether the request will need:
- a new or reused variant directory under `variants/<architecture>/...`
- `variant.h`
- optional `variant.cpp`
- one or more PlatformIO environments with `custom_meshtastic_*` metadata
- board images, tags, partition-scheme metadata, or DFU metadata
- any architecture-specific board files or notes for inherited defaults
## Non-Goals For Phase 1
- Direct parsing of PDF schematics.
- Automatic merging of new board files into the repository.
- Any change to firmware runtime behavior, protocol behavior, or shared board defaults.
@@ -0,0 +1,124 @@
# Data Model: Hardware Support Agent
## Hardware Support Context
Purpose: Repository-backed summary of current target-definition patterns across supported board variants.
Fields:
- source_paths: list of repository glob roots used to build the context
- generated_at: timestamp or generation marker
- variant_count: integer count of scanned variant directories
- environment_count: integer count of summarized PlatformIO environments
- metadata*keys: list of observed `custom_meshtastic*\*` keys
- category_counts: mapping of capability category to common macros and frequencies
- architecture_inventory: list of architecture groups and their environments
- representative_examples: list of example boards with extracted metadata and macro samples
- cautions: list of caveats about inherited defaults, multi-environment boards, and verification limits
Validation rules:
- Must be derived from repository state rather than hand-maintained guesses.
- Must clearly separate explicit declarations from inherited/default behavior where known.
- Must remain read-only context and not imply that any new board is validated for merge.
Relationships:
- Used by Board Intake Request as the canonical repository pattern source.
## Board Intake Request
Purpose: Maintainer-supplied description of a proposed new board or board revision.
Fields:
- environment_name: proposed PlatformIO environment name
- hardware_model: numeric or repository-convention hardware model identifier
- hardware_model_slug: uppercase slug when known
- display_name: human-readable board name
- architecture: target family such as `esp32-s3` or `nrf52840`
- source_materials: optional list of schematic, pinout, datasheet, or board-page references
- support_level: optional intended support metadata
- actively_supported: optional boolean intent
- board_notes: optional maintainer notes about revisions, optional peripherals, or known gaps
Validation rules:
- `environment_name`, `hardware_model`, and `display_name` are required for phase 1 intake.
- Architecture is required before any artifact expectation can be considered complete.
- Source materials are optional for submission but required for moving unresolved hardware fields toward scaffold generation.
- Conflicts with existing environment names or hardware identifiers must be surfaced.
Relationships:
- Produces one Intake Assessment.
- May eventually lead to one or more Board Support Scaffolds.
## Intake Assessment
Purpose: Structured result of evaluating a Board Intake Request against repository patterns and evidence sufficiency.
Fields:
- expected_artifacts: list of files or sections likely needed, such as `variant.h`, optional `variant.cpp`, PlatformIO environment entries, board metadata, images, or tags
- required*metadata: list of mandatory `custom_meshtastic*\*` values and board-definition fields
- matched_patterns: list of related repository examples by architecture or board family
- evidence_gaps: list of unresolved or conflicting hardware facts
- risk_flags: list of issues such as ambiguous board revisions, unsupported peripherals, or inherited-default uncertainty
- next_actions: ordered maintainer actions needed before safe scaffold generation
- scaffold_ready: boolean indicating whether evidence is sufficient for a later scaffold phase
Validation rules:
- Must never infer unsupported pin mappings silently.
- Must describe missing information in maintainer-actionable language.
- Must remain architecture- and variant-scoped.
Relationships:
- Derived from Board Intake Request and Hardware Support Context.
- Blocks or permits creation of Board Support Scaffold.
## Evidence Gap
Purpose: Specific missing, conflicting, or ambiguous fact that prevents safe draft generation.
Fields:
- category: metadata, radio, display, input, GPS, power, storage, connectivity, or revision-scope
- description: maintainer-readable explanation of what is missing or conflicting
- affected_artifact: target file or configuration area impacted
- required_evidence: type of source needed to resolve the gap
- blocking: boolean indicating whether the gap prevents scaffold generation
Validation rules:
- Must be traceable to a missing repository pattern or missing board truth.
- Must not be collapsed into generic “needs more info” language when the specific blocker is knowable.
Relationships:
- Belongs to an Intake Assessment.
## Board Support Scaffold
Purpose: Draft board-support content for a new target once evidence is sufficient.
Fields:
- target_variant_dir: proposed variant directory path
- variant_h_content: draft content or structured sections for `variant.h`
- variant_cpp_content: optional draft for `variant.cpp`
- platformio_env_content: draft PlatformIO environment metadata and extends chain
- unresolved_annotations: inline markers for any remaining non-blocking maintainer review items
- source_basis: references to intake data and repository patterns used to draft content
Validation rules:
- Must only include fields backed by evidence and repository conventions.
- Must preserve variant-scoped truth and never modify unrelated board definitions.
- May only be produced when Intake Assessment marks `scaffold_ready` true.
Relationships:
- Produced from Intake Assessment after gaps are resolved.
+97
View File
@@ -0,0 +1,97 @@
# Implementation Plan: Hardware Support Agent
**Branch**: `[129-hardware-support-agent]` | **Date**: 2026-03-25 | **Spec**: /Users/benmeadors/Documents/GitHub/firmware/specs/129-hardware-support-agent/spec.md
**Input**: Feature specification from `/Users/benmeadors/Documents/GitHub/firmware/specs/129-hardware-support-agent/spec.md`
**Note**: This plan covers Phase 0 and Phase 1 outputs for a constitution-safe first increment. The first delivery scope centers on repository-backed hardware context plus intake validation and report generation; scaffold generation remains designed but gated behind sufficient evidence.
## Summary
Build a Copilot-oriented hardware support workflow for Meshtastic that starts from repository-derived board-definition context, accepts a constrained board intake request, and produces a structured readiness report before any new board files are drafted. The technical approach uses lightweight Python tooling and repository-local markdown/JSON contract artifacts to inventory existing `variant.h` and `platformio.ini` patterns, normalize maintainer inputs, identify evidence gaps, and prepare a later scaffolding phase without changing live firmware behavior.
## Technical Context
**Language/Version**: Python 3.x for workflow tooling, Markdown for generated artifacts, existing C/C++/PlatformIO repository conventions for downstream scaffold targets
**Primary Dependencies**: Python standard library for inventory/intake tooling, existing Spec Kit artifacts, repository-local Copilot prompt/agent files, PlatformIO environment metadata conventions already in `variants/**/platformio.ini`
**Storage**: Repository-local files under `docs/`, `specs/129-hardware-support-agent/`, optional future intake examples under repo docs or fixtures
**Testing**: Targeted script execution, generated artifact review, `python3 bin/generate_hardware_support_context.py`, `trunk fmt` where applicable for markdown/templates, and targeted follow-up validation against representative variant files
**Target Platform**: Maintainer workflow inside the Meshtastic firmware repository on macOS/Linux development environments; outputs describe supported firmware architectures including ESP32, ESP32-S3, ESP32-C3, ESP32-C6, nRF52, RP2040/RP2350, STM32, and native patterns
**Project Type**: Repository tooling and Copilot workflow support
**Performance Goals**: Generate context and intake reports quickly enough for interactive maintainer use on a local checkout; avoid repository-wide processing that would materially slow a normal Copilot session
**Constraints**: No changes to mesh protocol behavior or shared device defaults; no guessing of pin mappings; architecture-specific truth must stay variant-scoped; keep implementation dependency-light and reviewable
**Scale/Scope**: Inventory currently spans 166 variant directories and 212 PlatformIO environments; phase 1 scope covers context generation, intake contract, evidence-gap reporting, and custom-agent planning, not autonomous end-to-end board enablement
## Constitution Check
_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
- Safety-critical mesh impact: Pass. This feature is workflow/documentation/tooling only and does not modify routing, airtime, MQTT, channel, packet-path, or public default behavior.
- Variant and platform scope: Pass. The workflow is explicitly scoped to board-definition artifacts and requires variant-specific evidence before any scaffold output is permitted.
- Validation evidence: Pass with explicit plan. Validation for this phase is `python3 bin/generate_hardware_support_context.py`, manual spot-check against representative variants, and review of generated documentation/contracts. Later implementation phases should add targeted intake fixture checks.
- Resource, power, memory, dependency impact: Pass. The feature adds lightweight local tooling and markdown contracts only, with no runtime firmware impact and no new external runtime dependency requirement.
- Constitutional violations or gaps: No active violations for Phase 0/1 planning. The only open product choice is whether future phase 2 includes scaffold generation immediately or remains report-only until more intake validation exists.
### Post-Design Re-Check
- Safety-critical mesh impact remains unchanged after design: no firmware runtime path is altered.
- Variant-scoped hardware truth is reinforced by the intake contract and evidence-gap model.
- Validation evidence remains sufficient for design artifacts, with implementation tasks needing targeted script-level checks.
- Resource and dependency impact remains minimal and repository-local.
- No justification entries are required in Complexity Tracking for this plan.
## Project Structure
### Documentation (this feature)
```text
specs/129-hardware-support-agent/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── board-intake-contract.md
└── tasks.md
```
### Source Code (repository root)
```text
.github/
├── agents/
└── prompts/
bin/
├── generate_hardware_support_context.py
├── board_intake.py
└── board_scaffold.py
docs/
└── hardware-support-context.md
variants/
├── esp32/
├── esp32c3/
├── esp32c6/
├── esp32s2/
├── esp32s3/
├── native/
├── nrf52840/
├── rp2040/
├── rp2350/
└── stm32/
```
**Structure Decision**: Use the existing single-repository tooling structure. Planning artifacts live under `specs/129-hardware-support-agent/`, reusable generated context stays in `docs/`, implementation scripts live in `bin/`, and any future custom Copilot workflow files belong in `.github/prompts/` and `.github/agents/`. No new top-level application structure is needed.
## Complexity Tracking
No constitutional violations or complexity exceptions are currently required.
## Review Notes
- **SC-004** is a post-launch business metric and is not a blocking acceptance gate before merge. A timed baseline comparison should be collected after the first real board intake using the completed workflow.
- Validation completed so far: `python3 bin/generate_hardware_support_context.py --validate`, `python3 bin/board_intake.py bin/fixtures/intake_minimal.json --validate`, `python3 bin/board_intake.py bin/fixtures/intake_full.json`, `python3 bin/board_intake.py bin/fixtures/intake_multi_env.json`, `python3 bin/board_scaffold.py bin/fixtures/intake_full.json`, and `python3 bin/board_scaffold.py bin/fixtures/intake_multi_env.json`.
- `bin/board_scaffold.py` intentionally emits placeholder pin and metadata values plus `// TODO: verify — ...` annotations. Generated scaffold output is draft-only and not suitable for direct merge without maintainer review against schematics and existing board patterns.
- Scaffold generation currently writes to `generated/hardware-support/` rather than directly into `variants/` to keep the workflow reviewable and constitution-safe.
- Skipped validations: no targeted `pio run`, native test, or simulator run was executed because this feature adds repository tooling and generated draft artifacts only; no firmware runtime files under `src/` were changed.
@@ -0,0 +1,111 @@
# Quickstart: Hardware Support Agent
## Goal
Use the repository-backed hardware support workflow to understand current board-definition patterns and evaluate whether a new board request is ready for safe scaffolding.
## Prerequisites
- Work from the repository root.
- Ensure Python 3 is available.
- Have the new board’s minimum intake information ready:
- proposed PlatformIO environment name
- hardware model identifier
- display name
- target architecture
- any available schematic, pinout, or datasheet references
## Step 1: Regenerate the repository context artifact
Run:
```bash
python3 bin/generate_hardware_support_context.py
```
Review:
- `docs/hardware-support-context.md`
Confirm that the architectures, representative examples, and metadata keys still reflect the current repository state.
## Step 2: Prepare the board intake request
Capture the request using the contract in:
- `specs/129-hardware-support-agent/contracts/board-intake-contract.md`
At minimum, fill:
- environment name
- hardware model
- display name
- architecture
Add source materials for radio, display, power, GPS, input, and auxiliary peripherals when available.
## Step 3: Evaluate readiness
Run:
```bash
python3 bin/board_intake.py path/to/intake.json
```
The intake report now includes:
- request summary
- expected artifacts
- required metadata
- matched repository patterns
- blocking and non-blocking evidence gaps
- risk flags
- next actions
- a `scaffold_ready` decision
For gate-style validation, run:
```bash
python3 bin/board_intake.py path/to/intake.json --validate
```
This exits non-zero when blocking gaps remain.
## Step 4: Generate scaffold output when ready
If Step 3 reports `scaffold_ready: true`, run:
```bash
python3 bin/board_scaffold.py path/to/intake.json --output-dir generated/hardware-support
```
Expected outputs:
- `generated/hardware-support/variants/<arch>/<variant-dir>/variant.h`
- `generated/hardware-support/variants/<arch>/<variant-dir>/platformio.ini`
- optional `variant.cpp` for ESP32-family targets
If the intake is not scaffold-ready, the scaffold command prints the assessment and exits non-zero instead of generating files.
## Step 5: Validate the workflow artifacts
Run:
```bash
python3 bin/generate_hardware_support_context.py
python3 bin/board_intake.py bin/fixtures/intake_full.json
python3 bin/board_scaffold.py bin/fixtures/intake_full.json --output-dir generated/hardware-support
```
Then manually spot-check representative targets such as:
- `variants/esp32/tbeam`
- `variants/esp32s3/tlora-pager`
- `variants/nrf52840/t-echo`
- `variants/rp2040/rak11310`
Ensure the generated context, intake assessment, and scaffold output remain consistent with repository truth.
## Next Step
Use [hardware-support.prompt.md](.github/prompts/hardware-support.prompt.md) and [hardware-support.agent.md](.github/agents/hardware-support.agent.md) to invoke the workflow directly from Copilot.
@@ -0,0 +1,46 @@
# Research: Hardware Support Agent
## Decision: Use repository-local Python tooling and markdown artifacts as the first implementation slice
Rationale: The repository already uses lightweight scripts under `bin/` and maintains board truth primarily in `variants/**/platformio.ini` and `variants/**/variant.h`. A Python script plus markdown outputs fits existing repo patterns, keeps dependencies minimal, and provides immediate value without touching firmware runtime code.
Alternatives considered:
- Implement the first increment directly as a full custom Copilot agent that generates new board files. Rejected because the workflow still needs a safer evidence-validation layer before scaffolding hardware definitions.
- Build a standalone service or extension-backed parser. Rejected because it would add unnecessary complexity, operational overhead, and dependencies for a repo-local maintainer workflow.
## Decision: Treat intake validation and readiness reporting as the first generation boundary
Rationale: The constitution requires verified, variant-scoped hardware truth and forbids guessing pins or capabilities. A report-first boundary lets maintainers capture missing evidence, expected artifacts, and metadata requirements before draft files are produced.
Alternatives considered:
- Generate `variant.h` and `platformio.ini` scaffolding immediately from minimum inputs. Rejected because environment name, `hw_model`, and display name are not enough to guarantee correct radio, power, display, and peripheral mappings.
- Block all workflow progress until every future scaffold field is known. Rejected because maintainers still need a useful way to understand what is missing and what repository patterns apply.
## Decision: Reuse the existing hardware inventory document as the canonical context input for planning
Rationale: `docs/hardware-support-context.md` already inventories metadata keys, common macro categories, and representative board examples across the repository. That artifact can serve as the phase 1 foundation for both maintainers and future agent prompts.
Alternatives considered:
- Generate a new per-architecture context file for each family. Rejected for now because a single canonical context artifact is easier to review and sufficient for the first intake/report workflow.
- Depend on maintainers manually browsing variant files during intake. Rejected because it defeats the feature goal of reducing rediscovery work.
## Decision: Represent the maintainer workflow as a small set of explicit entities and a human-readable contract
Rationale: The feature is primarily a repository workflow, not a networked API. A markdown contract describing required fields, validations, and outputs is sufficient for Spec Kit design and future agent/prompt implementation.
Alternatives considered:
- Define a JSON Schema or OpenAPI contract immediately. Rejected for phase 1 because no external service boundary exists yet and the workflow is still evolving.
- Keep the contract implicit in prompt text only. Rejected because it would be harder to review, test, and keep aligned with the constitution.
## Decision: Keep scaffold generation as a designed later phase gated by evidence sufficiency
Rationale: The user wants the end state to include new board-support scaffolding, but constitutional safety requires a stronger intake and validation model first. Planning the later phase now preserves momentum without collapsing safe and unsafe scopes together.
Alternatives considered:
- Remove scaffolding from the feature entirely. Rejected because it is core to the requested long-term outcome.
- Merge report generation and scaffolding into a single undifferentiated phase. Rejected because it weakens reviewability and blurs the safety boundary.
+170
View File
@@ -0,0 +1,170 @@
# Feature Specification: Hardware Support Agent
**Feature Branch**: `[129-hardware-support-agent]`
**Created**: 2026-03-25
**Status**: Draft
**Input**: User description: "Implement the feature specification based on the updated constitution. I want to build an agent inside copilot to add new hardware support, including variant.h/cpp, any platformio ini environments will all of our custom metadata, and all of the pinmappings. I would start this process by just giving the board environment name, hw_model, display name, and perhaps some source materials illustrating the pin mappings in a PDF schematic for instance. I think we should start by creating a context for you in the form of a markdown file documenting all of the current input, button, radio, gpio, and other common pins we use in the meshtastic firmware at a device target definition level, so that we reuse instead of re-invent."
## User Scenarios & Testing _(mandatory)_
<!--
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
you should still have a viable MVP (Minimum Viable Product) that delivers value.
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
Think of each story as a standalone slice of functionality that can be:
- Developed independently
- Tested independently
- Deployed independently
- Demonstrated to users independently
-->
### User Story 1 - Build Reusable Hardware Context (Priority: P1)
As a firmware maintainer adding support for a new device, I want a single repository-backed reference
that summarizes the common board-definition inputs already used across Meshtastic targets so I can
start from verified patterns instead of re-deriving pins, feature flags, and board metadata from scratch.
**Why this priority**: Without an authoritative context source, any later agent workflow will repeat the
same manual discovery work and risks copying incorrect or incomplete target definitions.
**Independent Test**: Can be fully tested by generating the hardware context artifact from the current
repository and confirming it captures existing target-definition fields, common pins, and board-scoped
capability patterns for a representative set of boards.
**Acceptance Scenarios**:
1. **Given** an existing firmware checkout with multiple board variants, **When** a maintainer requests
the hardware support context, **Then** the system produces a markdown artifact that documents current
board-definition inputs, common pin categories, and recurring variant-level capabilities from the repo.
2. **Given** a maintainer reviewing an existing board, **When** they inspect the context artifact,
**Then** they can identify the board environment, hardware model identifiers, display-related fields,
radio pin definitions, input pins, and other commonly reused target-definition elements without
manually scanning many variant files.
---
### User Story 2 - Define New Board Intake (Priority: P2)
As a firmware maintainer, I want to provide a small set of board inputs such as environment name,
hardware model, display name, and source materials so the Copilot workflow can determine what new
hardware support artifacts need to be created or filled in.
**Why this priority**: A constrained intake contract is required before the workflow can safely generate
variant files and PlatformIO environments for new hardware support.
**Independent Test**: Can be tested independently by supplying the declared board inputs for a hypothetical
new target and confirming the workflow identifies required artifacts, missing evidence, and board-definition
fields that must be resolved before code generation proceeds.
**Acceptance Scenarios**:
1. **Given** a maintainer provides an environment name, hardware model, display name, and source references,
**When** the intake workflow runs, **Then** it identifies the expected target-definition artifacts,
required metadata fields, and unresolved board details that still need confirmation.
2. **Given** the supplied materials do not establish enough hardware truth for safe generation,
**When** the workflow evaluates the request, **Then** it explicitly flags the missing pin mappings,
peripheral capabilities, or board metadata instead of guessing silently.
---
### User Story 3 - Generate Board Support Scaffolding (Priority: P3)
As a firmware maintainer, I want the Copilot workflow to use the validated intake and reusable context to
draft board-support artifacts such as `variant.h`, optional `variant.cpp`, and PlatformIO environment content
with repository-specific metadata so I can add new hardware support consistently and with less manual setup.
**Why this priority**: This delivers the actual acceleration benefit, but it depends on verified context and
safe intake rules to avoid generating incorrect hardware support.
**Independent Test**: Can be tested independently by running the workflow for a new board request and
confirming it produces scaffold content or structured instructions that align with repository patterns and
does not invent unsupported hardware details.
**Acceptance Scenarios**:
1. **Given** validated board inputs and sufficient source evidence, **When** the maintainer requests new
hardware support scaffolding, **Then** the workflow drafts the required board-support files and metadata
using existing repository conventions.
2. **Given** the request would affect hardware flags, pin mappings, or build metadata beyond the available
evidence, **When** scaffolding is attempted, **Then** the workflow limits output to supported fields and
clearly marks unresolved items for human confirmation.
---
### Edge Cases
- The requested board environment name conflicts with an existing PlatformIO environment or target directory.
- The same hardware model appears under multiple existing naming conventions and the correct repository form is ambiguous.
- A schematic or PDF source omits some pins or names them differently than the repository’s existing macros.
- A target uses architecture defaults today, so the context artifact must distinguish explicitly declared pins from inherited defaults.
- A board has multiple display or radio options, optional peripherals, or revision-specific pinouts that cannot be collapsed into one truth.
- The request includes peripherals that exist in source material but are not currently supported by repository patterns.
- A generated board definition would require changing shared defaults or introducing unverified power, timing, or RF assumptions.
## Requirements _(mandatory)_
### Functional Requirements
- **FR-001**: The system MUST produce a repository-local hardware context artifact that documents the current
target-definition patterns used for board support in this firmware repository.
- **FR-002**: The hardware context artifact MUST describe, at minimum, the board environment name,
hardware model identifier, display-related identifiers, radio pin group, input/button-related pins,
and other commonly reused pin or capability categories present at the device-target-definition level.
- **FR-003**: The hardware context artifact MUST distinguish board-specific declarations from architecture-level
defaults or inherited behavior when that distinction affects new board support work.
- **FR-004**: Users MUST be able to initiate the workflow by providing a minimal set of board inputs that includes
board environment name, hardware model, display name, and target architecture, with optional supporting source materials.
Architecture is required because expected artifacts and matched patterns cannot be determined without it.
- **FR-005**: The intake workflow MUST identify which board-support artifacts are expected for the request,
including variant files and PlatformIO environment content where applicable.
- **FR-006**: The intake workflow MUST surface missing or conflicting hardware evidence instead of inventing
unresolved pins, capabilities, metadata, or power assumptions.
- **FR-007**: The workflow MUST preserve variant-scoped hardware truth by keeping generated or suggested values
scoped to the intended target architecture, board, and board revision when known.
- **FR-008**: The workflow MUST support repository-specific metadata required for new PlatformIO environments,
including custom support metadata already used in this codebase.
- **FR-009**: The workflow MUST be able to draft scaffold content for `variant.h`, optional `variant.cpp`, and
related target files only when the provided evidence is sufficient to do so safely.
- **FR-010**: The workflow MUST record unresolved questions in a form the maintainer can act on before using any
scaffolded board support in the repository.
- **FR-011**: The workflow MUST be applicable to current Meshtastic hardware target definitions across supported
architectures, while allowing architecture-specific details to remain architecture-scoped.
- **FR-012**: The workflow MUST not change public protocol behavior, shared radio safety defaults, or unrelated
board definitions as part of preparing new hardware support context.
Where relevant, requirements MUST also state:
- affected architectures, boards, or modules: ESP32, ESP32-S3, ESP32-C3, nRF52, RP2040/RP2350, STM32WL, and Portduino-style target-definition patterns where present in repo conventions
- whether behavior changes public defaults, protocol compatibility, or generated artifacts: this feature must not alter public mesh defaults or protocol compatibility; it may create or update documentation artifacts for board-support workflow context
- any required validation evidence for high-risk mesh, hardware, or power behavior: targeted verification of generated context against representative variant files and naming patterns is required before relying on it for scaffolding
### Key Entities _(include if feature involves data)_
- **Hardware Support Context**: A repository-backed reference artifact that summarizes the current board-support
fields, common pin categories, variant capability macros, and target-definition conventions used across the firmware.
- **Board Intake Request**: The maintainer-provided input set for a proposed new board, including environment name,
hardware model, display name, target architecture, and optional source materials such as schematics.
- **Target Definition Pattern**: A reusable repository convention describing how a board is represented through
`variant.h`, optional companion files, PlatformIO environments, and board-specific metadata.
- **Evidence Gap**: A missing, ambiguous, or conflicting hardware fact that blocks safe generation of new board support.
- **Board Support Scaffold**: The draft output for new hardware support artifacts, limited to fields supported by
verified evidence and current repository conventions.
## Success Criteria _(mandatory)_
### Measurable Outcomes
- **SC-001**: Maintainers can locate the common target-definition inputs and pin categories for an existing board in one artifact within 5 minutes, without manually reading multiple variant directories.
- **SC-002**: For a representative set of existing boards, the context artifact correctly captures the board environment name, key capability categories, and primary pin groups with no unresolved mismatches after maintainer review.
- **SC-003**: A maintainer can submit a new board intake request using the declared minimum inputs and receive a complete list of required board-support artifacts and unresolved evidence gaps in a single workflow pass.
- **SC-004**: _(Post-launch metric — not a pre-merge acceptance gate)_ For new board requests with sufficient source evidence, the workflow reduces manual setup time for initial board-support scaffolding by at least 50% compared with manually assembling variant and PlatformIO definitions from scratch. A baseline timed comparison should be conducted after the first production intake.
## Assumptions
- The initial increment focuses on repository context and intake/scaffolding workflow support, not full autonomous end-to-end board enablement.
- Maintainers using this workflow already have access to the repository, Copilot, and any board source materials they want to reference.
- The first implementation may rely on repository-readable sources and manually supplied board details rather than automated PDF parsing.
- Existing Meshtastic variant files and PlatformIO environments provide enough representative patterns to build a useful reusable context artifact.
- The workflow will be allowed to stop and request clarification when hardware truth is incomplete instead of forcing a guessed output.
+170
View File
@@ -0,0 +1,170 @@
# Tasks: Hardware Support Agent
**Branch**: `129-hardware-support-agent`
**Input**: Design documents from `specs/129-hardware-support-agent/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/board-intake-contract.md ✅
**Note on existing work**: The Phase 3 (US1) inventory script and generated context artifact already exist.
Tasks T001–T004 treat them as in-scope for validation and hardening rather than creation from scratch.
---
## Phase 1: Setup
**Purpose**: Confirm repository structure and tooling baseline for this feature.
- [x] T001 Confirm `bin/generate_hardware_support_context.py` executes without errors from the repo root via `python3 bin/generate_hardware_support_context.py`
- [x] T002 [P] Confirm `docs/hardware-support-context.md` exists and is committed or tracked in the working tree
- [x] T003 [P] Clean up the duplicated `"Where relevant, requirements MUST also state:"` block in `specs/129-hardware-support-agent/spec.md`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Shared utilities and data structures that US2 and US3 both depend on.
**⚠️ CRITICAL**: US2 and US3 cannot begin until this phase is complete.
- [x] T004 Add a `BoardIntakeRequest` dataclass (or typed dict) capturing the fields from `specs/129-hardware-support-agent/data-model.md` in `bin/board_intake.py`
- [x] T005 Add a `EvidenceGap` dataclass in `bin/board_intake.py` matching the data model
- [x] T006 Add an `IntakeAssessment` dataclass in `bin/board_intake.py` matching the data model
- [x] T007 Implement a `load_hardware_context(path)` helper in `bin/board_intake.py` that reads `docs/hardware-support-context.md` and returns architecture family names and metadata key list for use by the assessment logic
**Checkpoint**: Shared data structures and context loader in place — US2 and US3 implementation can begin.
---
## Phase 3: User Story 1 — Build Reusable Hardware Context (Priority: P1) 🎯 MVP
**Goal**: A single repository-backed markdown document accurately inventories current board-support patterns across all supported architectures so maintainers can start a new board definition from verified patterns.
**Independent Test**: Run `python3 bin/generate_hardware_support_context.py` and manually spot-check output against four representative variants from different architectures.
### Validation for User Story 1
- [x] T008 [P] [US1] Spot-check generated `docs/hardware-support-context.md` against `variants/esp32/tbeam` — confirm radio pins, metadata keys, and category counts match the variant.h and platformio.ini declarations
- [x] T009 [P] [US1] Spot-check against `variants/esp32s3/tlora-pager` — confirm all 17 `custom_meshtastic_*` metadata keys and high Connectivity/Other category macro count are reflected
- [x] T010 [P] [US1] Spot-check against `variants/nrf52840/t-echo` — confirm nRF52-specific macros (`USE_LFXO`, `VARIANT_MCK`, nRF52-style SPI pins) appear correctly
- [x] T011 [P] [US1] Spot-check against `variants/rp2040/rak11310` — confirm RP2040 architecture entry is present and radio pins match
- [x] T012 [US1] Run `trunk fmt bin/generate_hardware_support_context.py` and fix any formatting issues
### Implementation for User Story 1
- [x] T013 [P] [US1] Add `--validate` CLI flag to `bin/generate_hardware_support_context.py` that prints a summary of how many variants were scanned, how many had metadata keys, and how many had no `variant.h` (for inherited-defaults audit)
- [x] T014 [US1] Add a `## Inherited Defaults Note` section to the generated `docs/hardware-support-context.md` that lists architecture families known to rely on BSP/base-environment defaults rather than locally declared macros (informed by validate output)
**Checkpoint**: `docs/hardware-support-context.md` is validated, formatted, and includes inherited-defaults guidance. US1 fully testable and deliverable independently.
---
## Phase 4: User Story 2 — Define New Board Intake (Priority: P2)
**Goal**: A maintainer can provide an environment name, hardware model, display name, and architecture and receive back a structured assessment listing expected artifacts, required metadata, closest matching patterns, and any evidence gaps.
**Independent Test**: Run the intake workflow against a sample hypothetical board (e.g., a new ESP32-S3 board with only minimum inputs) and confirm it returns a complete assessment with at least one evidence gap identified.
### Validation for User Story 2
- [x] T015 [P] [US2] Create `bin/fixtures/intake_minimal.json` with just the required fields for a hypothetical new ESP32-S3 board and confirm `bin/board_intake.py` parses it without error
- [x] T016 [P] [US2] Create `bin/fixtures/intake_full.json` with all recommended fields and source materials and confirm the workflow marks `scaffold_ready: true`
- [x] T016b [P] [US2] Create `bin/fixtures/intake_multi_env.json` representing a board with two display variants sharing one hardware model (e.g., a TFT and an e-ink variant) and confirm `assess_intake` flags `revision-scope` ambiguity as a blocking Evidence Gap rather than collapsing the options silently
- [x] T017 [US2] Confirm `bin/board_intake.py --validate bin/fixtures/intake_minimal.json` correctly flags missing radio, display, and power evidence as blocking gaps
### Implementation for User Story 2
- [x] T018 [P] [US2] Implement `validate_intake(request: BoardIntakeRequest) -> list[str]` in `bin/board_intake.py` that checks required fields and detects environment-name conflicts against the existing architecture inventory in `docs/hardware-support-context.md`
- [x] T019 [P] [US2] Implement `find_matched_patterns(request: BoardIntakeRequest, context) -> list[dict]` that returns the three closest existing board examples by architecture from the context document
- [x] T020 [US2] Implement `build_evidence_gaps(request: BoardIntakeRequest) -> list[EvidenceGap]` that identifies missing pin group evidence (radio, display, GPS, power, input) based on declared source materials
- [x] T021 [US2] Implement `assess_intake(request: BoardIntakeRequest, context) -> IntakeAssessment` combining T018–T020 to produce a full structured assessment
- [x] T022 [US2] Implement `render_assessment_markdown(assessment: IntakeAssessment) -> str` that formats the assessment as a maintainer-readable markdown report
- [x] T023 [US2] Add a CLI entry point `bin/board_intake.py <intake.json>` that prints the assessment markdown to stdout or an output file
- [x] T024 [US2] Run `trunk fmt bin/board_intake.py` and fix any formatting issues
**Checkpoint**: `bin/board_intake.py` fully processes a new board request and prints an assessment. US2 independently testable.
---
## Phase 5: User Story 3 — Generate Board Support Scaffolding (Priority: P3)
**Goal**: When intake assessment marks `scaffold_ready: true`, the workflow drafts `variant.h`, optional `variant.cpp`, and PlatformIO environment content using repository conventions, with inline annotations for unresolved items.
**Independent Test**: Run the scaffold generator for a fully-specified test case and confirm the output files follow existing repository patterns and include `// TODO:` markers for any fields not backed by supplied evidence.
### Validation for User Story 3
- [x] T025 [P] [US3] Run scaffold generator for `bin/fixtures/intake_full.json` and confirm `variant.h` output contains required radio pin group, capability macros, and metadata section
- [x] T026 [P] [US3] Confirm scaffold generator emits `// TODO: verify —` annotations for any recommended fields absent from the intake fixture
- [x] T027 [US3] Confirm generated PlatformIO env block includes all `custom_meshtastic_*` metadata keys from the contract and uses `extends` to reference the correct base environment for the declared architecture
### Implementation for User Story 3
- [x] T028 [P] [US3] Create `bin/board_scaffold.py` with a `generate_variant_h(assessment: IntakeAssessment, context) -> str` function that drafts a `variant.h` file using architecture-appropriate macro order from the context document
- [x] T029 [P] [US3] Implement `generate_platformio_env(assessment: IntakeAssessment) -> str` in `bin/board_scaffold.py` that emits the PlatformIO environment block with `custom_meshtastic_*` fields
- [x] T030 [US3] Implement `annotate_unresolved(content: str, gaps: list[EvidenceGap]) -> str` that inserts `// TODO: verify — {gap.description}` comments next to lines that correspond to unresolved evidence gaps
- [x] T031 [US3] Implement `scaffold_board(assessment: IntakeAssessment, context, output_dir: Path)` which orchestrates T028–T030 and writes files to the proposed variant directory path
- [x] T032 [US3] Add CLI entry point `bin/board_scaffold.py <intake.json> [--output-dir <path>]` that reads an intake file, runs the full intake + scaffold pipeline, and writes output
- [x] T033 [US3] Guard scaffold entry: if `assess_intake` returns `scaffold_ready: false`, print the assessment report and exit with a non-zero code rather than generating files
- [x] T034 [US3] Run `trunk fmt bin/board_scaffold.py` and fix any formatting issues
**Checkpoint**: All three user stories independently functional. Hardware context, intake assessment, and scaffold generation each work as standalone deliverables.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Custom Copilot agent integration, documentation, and final review.
- [x] T035 [P] Create `.github/prompts/hardware-support.prompt.md` that routes the hardware support workflow to the custom agent
- [x] T036 [P] Create `.github/agents/hardware-support.agent.md` with step-by-step instructions for the Copilot hardware-support workflow: context regeneration → intake → assessment → optional scaffold
- [x] T037 [US1] Update `specs/129-hardware-support-agent/quickstart.md` Step 3 and Step 4 to reference the completed `bin/board_intake.py` CLI and describe the expected output shape
- [x] T038 [P] Confirm that no firmware runtime files under `src/`, `variants/`, or `protobufs/` were modified as part of this feature (constitution compliance check)
- [x] T039 Run `python3 bin/generate_hardware_support_context.py` one final time to confirm the regenerated artifact is up to date and passes spot-checks from T008–T011
- [x] T040 Summarize any skipped validations, open `// TODO:` items in scaffold output, and any known limitations in `specs/129-hardware-support-agent/plan.md` under a `## Review Notes` section
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1 (Setup)**: No dependencies — start immediately
- **Phase 2 (Foundational)**: Depends on Phase 1 — blocks US2 and US3
- **Phase 3 (US1)**: Can start after Phase 1; does not depend on Phase 2
- **Phase 4 (US2)**: Depends on Phase 2 (shared data structures)
- **Phase 5 (US3)**: Depends on Phase 2 and Phase 4 (uses IntakeAssessment output)
- **Phase 6 (Polish)**: Depends on all story phases
### User Story Dependencies
- **US1 (P1)**: Depends on Phase 1 only — fully independent MVP
- **US2 (P2)**: Depends on Phase 2; integrates with US1 context document but not the script directly
- **US3 (P3)**: Depends on Phase 2 and US2 `assess_intake` result
### Parallel Opportunities
**Within Phase 2**:
- T004, T005, T006 can be written in parallel (same file, separate dataclasses — serialize to avoid conflicts)
**Within Phase 3 (US1)**:
- T008, T009, T010, T011 (spot-checks) are fully independent and can run in parallel
- T013 [P] is independent of validation tasks
**Within Phase 4 (US2)**:
- T015, T016 fixture creation can run in parallel
- T018 and T019 are independent of each other and can be implemented in parallel
**Within Phase 5 (US3)**:
- T028 and T029 are independent scaffold generators — can be written in parallel
- T025 and T026 validation tasks are independent
**Within Phase 6**:
- T035, T036, T038 are independent files — can all be done in parallel
### MVP Scope
To deliver US1 as a standalone MVP: complete Phases 1 and 3 (T001–T003, T008–T014). This verifies the hardware context artifact is accurate and useful without requiring any intake or scaffold tooling.
+7 -6
View File
@@ -512,11 +512,6 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
// LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
// nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
// Occasional progress logging. (readIndex==2 will be true for the first non-us node)
if (readIndex == 2 || readIndex % 20 == 0) {
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
}
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
fromRadioScratch.node_info = infoToSend;
prefetchNodeInfos();
@@ -647,9 +642,11 @@ void PhoneAPI::releaseQueueStatusPhonePacket()
void PhoneAPI::prefetchNodeInfos()
{
bool added = false;
bool wasEmpty = false;
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
{
concurrency::LockGuard guard(&nodeInfoMutex);
wasEmpty = nodeInfoQueue.empty();
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
auto nextNode = nodeDB->readNextMeshNode(readIndex);
if (!nextNode)
@@ -663,11 +660,15 @@ void PhoneAPI::prefetchNodeInfos()
info.via_mqtt = isUs ? false : info.via_mqtt;
info.is_favorite = info.is_favorite || isUs;
nodeInfoQueue.push_back(info);
// Log progress here (at fetch time) so readIndex is accurate and each value logs only once.
if (readIndex == 2 || readIndex % 20 == 0) {
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
}
added = true;
}
}
if (added)
if (added && wasEmpty)
onNowHasData(0);
}
+1 -1
View File
@@ -55,7 +55,7 @@ extern const pb_msgdesc_t meshtastic_ChannelSet_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size
#define meshtastic_ChannelSet_size 682
#define meshtastic_ChannelSet_size 685
#ifdef __cplusplus
} /* extern "C" */
+8 -4
View File
@@ -618,6 +618,8 @@ typedef struct _meshtastic_Config_LoRaConfig {
bool config_ok_to_mqtt;
/* Set where LORA FEM is enabled, disabled, or not present */
meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode;
/* Don't use radiolib to initialize the radio, instead listen for a serialHal connection */
bool serial_hal_only;
} meshtastic_Config_LoRaConfig;
typedef struct _meshtastic_Config_BluetoothConfig {
@@ -779,7 +781,7 @@ extern "C" {
#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0}
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN}
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
#define meshtastic_Config_SessionkeyConfig_init_default {0}
@@ -790,7 +792,7 @@ extern "C" {
#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0}
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN}
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
#define meshtastic_Config_SessionkeyConfig_init_zero {0}
@@ -877,6 +879,7 @@ extern "C" {
#define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104
#define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105
#define meshtastic_Config_LoRaConfig_fem_lna_mode_tag 106
#define meshtastic_Config_LoRaConfig_serial_hal_only_tag 107
#define meshtastic_Config_BluetoothConfig_enabled_tag 1
#define meshtastic_Config_BluetoothConfig_mode_tag 2
#define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3
@@ -1029,7 +1032,8 @@ X(a, STATIC, SINGULAR, BOOL, pa_fan_disabled, 15) \
X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \
X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \
X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) \
X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106)
X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106) \
X(a, STATIC, SINGULAR, BOOL, serial_hal_only, 107)
#define meshtastic_Config_LoRaConfig_CALLBACK NULL
#define meshtastic_Config_LoRaConfig_DEFAULT NULL
@@ -1086,7 +1090,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
#define meshtastic_Config_BluetoothConfig_size 10
#define meshtastic_Config_DeviceConfig_size 100
#define meshtastic_Config_DisplayConfig_size 36
#define meshtastic_Config_LoRaConfig_size 88
#define meshtastic_Config_LoRaConfig_size 91
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
#define meshtastic_Config_NetworkConfig_size 204
#define meshtastic_Config_PositionConfig_size 62
@@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
/* Maximum encoded size of messages (where known) */
/* meshtastic_NodeDatabase_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
#define meshtastic_BackupPreferences_size 2429
#define meshtastic_BackupPreferences_size 2432
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196
+1 -1
View File
@@ -205,7 +205,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
#define meshtastic_LocalConfig_size 754
#define meshtastic_LocalConfig_size 757
#define meshtastic_LocalModuleConfig_size 820
#ifdef __cplusplus
@@ -0,0 +1,19 @@
/* Automatically generated nanopb constant definitions */
/* Generated by nanopb-0.4.9.1 */
#include "meshtastic/serial_hal.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
#endif
PB_BIND(meshtastic_SerialHalCommand, meshtastic_SerialHalCommand, 2)
PB_BIND(meshtastic_SerialHalResponse, meshtastic_SerialHalResponse, 2)
@@ -0,0 +1,135 @@
/* Automatically generated nanopb header */
/* Generated by nanopb-0.4.9.1 */
#ifndef PB_MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_INCLUDED
#define PB_MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_INCLUDED
#include <pb.h>
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
#endif
/* Enum definitions */
typedef enum _meshtastic_SerialHalCommand_Type {
meshtastic_SerialHalCommand_Type_UNSET = 0,
meshtastic_SerialHalCommand_Type_PIN_MODE = 1,
meshtastic_SerialHalCommand_Type_DIGITAL_WRITE = 2,
meshtastic_SerialHalCommand_Type_DIGITAL_READ = 3,
meshtastic_SerialHalCommand_Type_ATTACH_INTERRUPT = 4,
meshtastic_SerialHalCommand_Type_DETACH_INTERRUPT = 5,
meshtastic_SerialHalCommand_Type_SPI_TRANSFER = 6,
meshtastic_SerialHalCommand_Type_NOOP = 7
} meshtastic_SerialHalCommand_Type;
typedef enum _meshtastic_SerialHalResponse_Result {
meshtastic_SerialHalResponse_Result_OK = 0,
meshtastic_SerialHalResponse_Result_ERROR = 1,
meshtastic_SerialHalResponse_Result_BAD_REQUEST = 2,
meshtastic_SerialHalResponse_Result_UNSUPPORTED = 3
} meshtastic_SerialHalResponse_Result;
/* Struct definitions */
typedef PB_BYTES_ARRAY_T(512) meshtastic_SerialHalCommand_data_t;
typedef struct _meshtastic_SerialHalCommand {
/* Host-assigned request id. Replies echo this id back in
SerialHalResponse.transaction_id. */
uint32_t transaction_id;
meshtastic_SerialHalCommand_Type type;
uint32_t pin;
uint32_t value;
uint32_t mode;
meshtastic_SerialHalCommand_data_t data;
} meshtastic_SerialHalCommand;
typedef PB_BYTES_ARRAY_T(512) meshtastic_SerialHalResponse_data_t;
typedef struct _meshtastic_SerialHalResponse {
/* Matches the originating SerialHalCommand.transaction_id for normal
request/response traffic.
A value of 0 indicates an unsolicited interrupt notification generated by
the device. In that case, the host should interpret value as the GPIO pin
that triggered. */
uint32_t transaction_id;
meshtastic_SerialHalResponse_Result result;
/* Used by DIGITAL_READ replies and interrupt notifications. For interrupt
notifications (transaction_id == 0), this carries the pin number. */
uint32_t value;
meshtastic_SerialHalResponse_data_t data;
char error[80];
} meshtastic_SerialHalResponse;
#ifdef __cplusplus
extern "C" {
#endif
/* Helper constants for enums */
#define _meshtastic_SerialHalCommand_Type_MIN meshtastic_SerialHalCommand_Type_UNSET
#define _meshtastic_SerialHalCommand_Type_MAX meshtastic_SerialHalCommand_Type_NOOP
#define _meshtastic_SerialHalCommand_Type_ARRAYSIZE ((meshtastic_SerialHalCommand_Type)(meshtastic_SerialHalCommand_Type_NOOP+1))
#define _meshtastic_SerialHalResponse_Result_MIN meshtastic_SerialHalResponse_Result_OK
#define _meshtastic_SerialHalResponse_Result_MAX meshtastic_SerialHalResponse_Result_UNSUPPORTED
#define _meshtastic_SerialHalResponse_Result_ARRAYSIZE ((meshtastic_SerialHalResponse_Result)(meshtastic_SerialHalResponse_Result_UNSUPPORTED+1))
#define meshtastic_SerialHalCommand_type_ENUMTYPE meshtastic_SerialHalCommand_Type
#define meshtastic_SerialHalResponse_result_ENUMTYPE meshtastic_SerialHalResponse_Result
/* Initializer values for message structs */
#define meshtastic_SerialHalCommand_init_default {0, _meshtastic_SerialHalCommand_Type_MIN, 0, 0, 0, {0, {0}}}
#define meshtastic_SerialHalResponse_init_default {0, _meshtastic_SerialHalResponse_Result_MIN, 0, {0, {0}}, ""}
#define meshtastic_SerialHalCommand_init_zero {0, _meshtastic_SerialHalCommand_Type_MIN, 0, 0, 0, {0, {0}}}
#define meshtastic_SerialHalResponse_init_zero {0, _meshtastic_SerialHalResponse_Result_MIN, 0, {0, {0}}, ""}
/* Field tags (for use in manual encoding/decoding) */
#define meshtastic_SerialHalCommand_transaction_id_tag 1
#define meshtastic_SerialHalCommand_type_tag 2
#define meshtastic_SerialHalCommand_pin_tag 3
#define meshtastic_SerialHalCommand_value_tag 4
#define meshtastic_SerialHalCommand_mode_tag 5
#define meshtastic_SerialHalCommand_data_tag 6
#define meshtastic_SerialHalResponse_transaction_id_tag 1
#define meshtastic_SerialHalResponse_result_tag 2
#define meshtastic_SerialHalResponse_value_tag 3
#define meshtastic_SerialHalResponse_data_tag 4
#define meshtastic_SerialHalResponse_error_tag 5
/* Struct field encoding specification for nanopb */
#define meshtastic_SerialHalCommand_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, transaction_id, 1) \
X(a, STATIC, SINGULAR, UENUM, type, 2) \
X(a, STATIC, SINGULAR, UINT32, pin, 3) \
X(a, STATIC, SINGULAR, UINT32, value, 4) \
X(a, STATIC, SINGULAR, UINT32, mode, 5) \
X(a, STATIC, SINGULAR, BYTES, data, 6)
#define meshtastic_SerialHalCommand_CALLBACK NULL
#define meshtastic_SerialHalCommand_DEFAULT NULL
#define meshtastic_SerialHalResponse_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, transaction_id, 1) \
X(a, STATIC, SINGULAR, UENUM, result, 2) \
X(a, STATIC, SINGULAR, UINT32, value, 3) \
X(a, STATIC, SINGULAR, BYTES, data, 4) \
X(a, STATIC, SINGULAR, STRING, error, 5)
#define meshtastic_SerialHalResponse_CALLBACK NULL
#define meshtastic_SerialHalResponse_DEFAULT NULL
extern const pb_msgdesc_t meshtastic_SerialHalCommand_msg;
extern const pb_msgdesc_t meshtastic_SerialHalResponse_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
#define meshtastic_SerialHalCommand_fields &meshtastic_SerialHalCommand_msg
#define meshtastic_SerialHalResponse_fields &meshtastic_SerialHalResponse_msg
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_MAX_SIZE meshtastic_SerialHalResponse_size
#define meshtastic_SerialHalCommand_size 541
#define meshtastic_SerialHalResponse_size 610
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif
+25 -10
View File
@@ -1,22 +1,34 @@
/*
Adds a WebServer and WebService callbacks to meshtastic as Linux Version. The WebServer & Webservices
runs in a real linux thread beside the portdunio threading emulation. It replaces the complete ESP32
Webserver libs including generation of SSL certifcicates, because the use ESP specific details in
the lib that can't be emulated.
Adds a WebServer and WebService callbacks to meshtastic via the Portduino/native target (Linux and
macOS). The WebServer & Webservices run in a real host thread beside the Portduino threading
emulation. It replaces the complete ESP32 Webserver libs including generation of SSL certificates,
because those libs use ESP-specific details that can't be emulated.
The WebServices adapt to the two major phoneapi functions "handleAPIv1FromRadio,handleAPIv1ToRadio"
The WebServer just adds basaic support to deliver WebContent, so it can be used to
deliver the WebGui definded by the WebClient Project.
The WebServer just adds basic support to deliver WebContent, so it can be used to
deliver the WebGui defined by the WebClient Project.
Steps to get it running:
1.) Add these Linux Libs to the compile and target machine:
Linux (apt):
1.) Add these libs to the compile and target machine:
sudo apt update && \
apt -y install openssl libssl-dev libopenssl libsdl2-dev \
apt -y install openssl libssl-dev libsdl2-dev \
libulfius-dev liborcania-dev
macOS (Homebrew):
1.) Install prerequisites via Homebrew:
brew install ulfius openssl@3
The PlatformIO env (native-macos) picks up compiler/linker flags via
`pkg-config`. In particular, OpenSSL needs `pkg-config --cflags --libs openssl@3`
so both the Homebrew include path and linker flags are provided; ulfius and its
dependencies (liborcania, libyder) are also resolved via `pkg-config`.
2.) Configure the root directory of the web Content in the config.yaml file.
The followinng tags should be included and set at your needs
The following tags should be included and set at your needs
Example entry in the config.yaml
Webserver:
@@ -34,7 +46,10 @@ Author: Marc Philipp Hammermann
mail: marchammermann@googlemail.com
*/
#ifdef PORTDUINO_LINUX_HARDWARE
// Mirrors the guard in PiWebServer.h — see comment there. macOS Homebrew
// provides ulfius + deps; Linux pulls them via apt. Either way, this
// translation unit only compiles when the headers are present.
#ifdef ARCH_PORTDUINO
#if __has_include(<ulfius.h>)
#include "PiWebServer.h"
#include "NodeDB.h"
+5 -1
View File
@@ -1,5 +1,9 @@
#pragma once
#ifdef PORTDUINO_LINUX_HARDWARE
// Portduino webserver is built whenever the ulfius headers are reachable,
// not only on Linux. macOS users can `brew install ulfius` to enable it;
// without ulfius the entire body is skipped and main.cpp's matching
// __has_include guard avoids referencing the type.
#ifdef ARCH_PORTDUINO
#if __has_include(<ulfius.h>)
#include "PhoneAPI.h"
#include "ulfius-cfg.h"
+62 -11
View File
@@ -13,6 +13,7 @@
#include <ErriezCRC32.h>
#include <Utility.h>
#include <assert.h>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <iostream>
@@ -32,6 +33,16 @@
#include <cxxabi.h>
#endif
#ifdef __APPLE__
// Used by getMacAddr()'s macOS fallback to read the en0 link-layer address.
// `getifaddrs()` is the BSD-portable way; `<net/if_dl.h>` provides the
// `sockaddr_dl` cast and the `LLADDR()` macro that points at the 6-byte MAC.
#include <cstring> // strcmp, memcpy
#include <ifaddrs.h>
#include <net/if.h>
#include <net/if_dl.h>
#endif
#include "platform/portduino/USBHal.h"
portduino_config_struct portduino_config;
@@ -155,9 +166,35 @@ void getMacAddr(uint8_t *dmac)
dmac[3] = di.bdaddr.b[2];
dmac[4] = di.bdaddr.b[1];
dmac[5] = di.bdaddr.b[0];
#elif defined(__APPLE__)
// No BlueZ on macOS, but we can fall back to the host's primary
// network interface MAC. `en0` is Wi-Fi on every shipping Mac
// (Ethernet, when present, is en1 or higher), which gives the user
// the same kind of stable, host-derived identifier that the BlueZ
// path provides on Linux. If en0 isn't found or has no MAC, dmac is
// left untouched and the caller's "Blank MAC Address not allowed!"
// check will still fire — preserving existing behavior for users
// who deliberately rely on --hwid or YAML override.
struct ifaddrs *ifap = nullptr;
if (getifaddrs(&ifap) == 0) {
for (struct ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) {
if (p->ifa_addr == nullptr || p->ifa_addr->sa_family != AF_LINK) {
continue;
}
if (strcmp(p->ifa_name, "en0") != 0) {
continue;
}
auto *sdl = reinterpret_cast<struct sockaddr_dl *>(p->ifa_addr);
if (sdl->sdl_alen == 6) {
memcpy(dmac, LLADDR(sdl), 6);
break;
}
}
freeifaddrs(ifap);
}
#else
// No BlueZ on non-Linux hosts (e.g. macOS). Leave dmac at its default;
// the caller can override via the --hwid CLI flag or the YAML config.
// No platform-specific MAC source; leave dmac at its default. Caller
// can override via the --hwid CLI flag or the YAML config.
(void)dmac;
#endif
}
@@ -1052,17 +1089,31 @@ static bool ends_with(std::string_view str, std::string_view suffix)
bool MAC_from_string(std::string mac_str, uint8_t *dmac)
{
mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end());
if (mac_str.length() == 12) {
dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16);
dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16);
dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16);
dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16);
dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16);
dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16);
return true;
} else {
if (mac_str.length() != 12) {
return false;
}
// Validate every character is a hex digit before parsing. std::stoi
// would otherwise skip leading whitespace and silently truncate at the
// first non-digit, which is too lenient for a MAC address.
for (char c : mac_str) {
if (!isxdigit(static_cast<unsigned char>(c))) {
return false;
}
}
// Parse into a temporary so dmac is not partially modified if a later
// byte fails. At least one caller in getMacAddr() ignores the bool
// return, so leaving stale bytes in dmac on failure would silently
// produce a wrong MAC.
uint8_t tmp[6];
try {
for (int i = 0; i < 6; i++) {
tmp[i] = static_cast<uint8_t>(std::stoi(mac_str.substr(i * 2, 2), nullptr, 16));
}
} catch (const std::exception &) {
return false;
}
memcpy(dmac, tmp, 6);
return true;
}
std::string exec(const char *cmd)
+195
View File
@@ -0,0 +1,195 @@
// Unit tests for MAC_from_string in src/platform/portduino/PortduinoGlue.cpp.
//
// Regression coverage for when the function stripped colons from
// its mac_str parameter but then read bytes from the global
// portduino_config.mac_address. Symptoms: --hwid silently ignored when
// MACAddress: was also set, and SIGABRT (stoi: no conversion) when --hwid
// was used without MACAddress: in config.yaml.
#include "Arduino.h"
#include "TestUtil.h"
#include <cstdint>
#include <cstring>
#include <string>
#include <unity.h>
// Forward-declare instead of including PortduinoGlue.h to avoid pulling in
// LR11x0Interface, USBHal, mesh.pb.h, yaml-cpp, and the full portduino_config
// struct just to test a self-contained string parser. The symbol is defined
// in PortduinoGlue.cpp and resolved at link time.
bool MAC_from_string(std::string mac_str, uint8_t *dmac);
void setUp(void) {}
void tearDown(void) {}
// --- Happy-path parsing ---
void test_colon_separated_uppercase()
{
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AA:BB:CC:DD:EE:FF", dmac));
TEST_ASSERT_EQUAL_HEX8(0xAA, dmac[0]);
TEST_ASSERT_EQUAL_HEX8(0xBB, dmac[1]);
TEST_ASSERT_EQUAL_HEX8(0xCC, dmac[2]);
TEST_ASSERT_EQUAL_HEX8(0xDD, dmac[3]);
TEST_ASSERT_EQUAL_HEX8(0xEE, dmac[4]);
TEST_ASSERT_EQUAL_HEX8(0xFF, dmac[5]);
}
void test_colon_separated_lowercase()
{
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("02:ca:fe:ba:be:01", dmac));
TEST_ASSERT_EQUAL_HEX8(0x02, dmac[0]);
TEST_ASSERT_EQUAL_HEX8(0xCA, dmac[1]);
TEST_ASSERT_EQUAL_HEX8(0xFE, dmac[2]);
TEST_ASSERT_EQUAL_HEX8(0xBA, dmac[3]);
TEST_ASSERT_EQUAL_HEX8(0xBE, dmac[4]);
TEST_ASSERT_EQUAL_HEX8(0x01, dmac[5]);
}
void test_no_colons_packed_hex()
{
// The CLI form produced by some tools — 12 hex chars, no separators.
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AABBCCDDEEFF", dmac));
TEST_ASSERT_EQUAL_HEX8(0xAA, dmac[0]);
TEST_ASSERT_EQUAL_HEX8(0xFF, dmac[5]);
}
void test_two_distinct_inputs_yield_distinct_outputs()
{
// Direct regression for the original bug: parsing two different MAC
// strings in succession must produce two different byte sequences.
// Pre-fix, both calls would have produced identical bytes derived from
// the (untouched) global portduino_config.mac_address.
uint8_t a[6] = {0};
uint8_t b[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AA:BB:CC:DD:EE:FF", a));
TEST_ASSERT_TRUE(MAC_from_string("02:CA:FE:BA:BE:01", b));
TEST_ASSERT_NOT_EQUAL(0, std::memcmp(a, b, 6));
TEST_ASSERT_EQUAL_HEX8(0xAA, a[0]);
TEST_ASSERT_EQUAL_HEX8(0x02, b[0]);
}
void test_does_not_read_external_state()
{
// The function must derive every byte from its parameter, not from any
// global. Provide a unique MAC and verify all six bytes match the input
// exactly — leaves no room for the function to be smuggling bytes from
// elsewhere.
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("12:34:56:78:9A:BC", dmac));
const uint8_t expected[6] = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC};
TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, dmac, 6);
}
// --- Rejected inputs ---
// Pre-fix, the empty/short cases either crashed (stoi exception on substr("")
// of the empty global) or silently filled dmac with stale bytes. Post-fix,
// the length guard rejects them cleanly with `false` and dmac is unchanged.
void test_empty_string_returns_false()
{
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string("", dmac));
// dmac must be untouched on failure.
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_too_short_returns_false()
{
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string("AA:BB:CC", dmac));
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_too_long_returns_false()
{
uint8_t dmac[6] = {0};
// 14 hex chars after colon-strip > 12.
TEST_ASSERT_FALSE(MAC_from_string("AA:BB:CC:DD:EE:FF:00", dmac));
}
void test_only_colons_returns_false()
{
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string(":::::", dmac));
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_extra_colons_still_parses()
{
// Colon stripping happens before length check, so an unconventional
// grouping that totals 12 hex chars after stripping is still accepted.
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AABB:CCDD:EEFF", dmac));
TEST_ASSERT_EQUAL_HEX8(0xAA, dmac[0]);
TEST_ASSERT_EQUAL_HEX8(0xFF, dmac[5]);
}
void test_non_hex_input_returns_false()
{
// 12 chars of non-hex would have made std::stoi throw before the
// try/catch wrapper was added, killing the daemon. Now must return false
// and leave dmac untouched.
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string("ZZ:ZZ:ZZ:ZZ:ZZ:ZZ", dmac));
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_partial_hex_failure_preserves_dmac()
{
// First five bytes are valid hex; the sixth ("ZZ") is not. Without the
// temp-buffer staging, dmac would be partially overwritten with the five
// good bytes plus stale data in slot 5 — silently producing a wrong MAC
// since the only caller that uses this in getMacAddr() ignores the bool
// return value.
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string("AA:BB:CC:DD:EE:ZZ", dmac));
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_embedded_non_hex_returns_false()
{
// std::stoi tolerates leading whitespace and a "0x" prefix, so a stray
// space inside a 2-char window like " F" would silently parse as 0xF.
// The per-character isxdigit() pre-check rejects these. The 14-char
// "0xAABBCCDDEEFF" is also rejected by the length check.
uint8_t dmac[6] = {0};
TEST_ASSERT_FALSE(MAC_from_string("AA:BB:CC:DD:EE: F", dmac));
TEST_ASSERT_FALSE(MAC_from_string("0xAABBCCDDEEFF", dmac));
}
// --- Unity lifecycle ---
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_colon_separated_uppercase);
RUN_TEST(test_colon_separated_lowercase);
RUN_TEST(test_no_colons_packed_hex);
RUN_TEST(test_two_distinct_inputs_yield_distinct_outputs);
RUN_TEST(test_does_not_read_external_state);
RUN_TEST(test_empty_string_returns_false);
RUN_TEST(test_too_short_returns_false);
RUN_TEST(test_too_long_returns_false);
RUN_TEST(test_only_colons_returns_false);
RUN_TEST(test_extra_colons_still_parses);
RUN_TEST(test_non_hex_input_returns_false);
RUN_TEST(test_partial_hex_failure_preserves_dmac);
RUN_TEST(test_embedded_non_hex_returns_false);
exit(UNITY_END());
}
void loop() {}
+1 -1
View File
@@ -2,7 +2,7 @@
[portduino_base]
platform =
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
https://github.com/meshtastic/platform-native/archive/4ea5e09ac7d51a593e12ec7c1ebb6cd06745ce53.zip
https://github.com/meshtastic/platform-native/archive/cab4b21d902973e43c938dab3cf4844ba02547ec.zip
framework = arduino
build_src_filter =
+37 -1
View File
@@ -125,6 +125,8 @@ test_testing_command =
;
; Prerequisites (Homebrew):
; brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
; # Optional: enable the HTTP API (PiWebServer) on macOS:
; brew install ulfius
;
; The macOS-side patches now live upstream:
; * meshtastic/platform-native — `String.h`-shadow shim, `-Wno-enum-constexpr-conversion`,
@@ -191,7 +193,16 @@ build_flags = ${portduino_base.build_flags_common}
; style screen-driver hooks scattered through sensor sources.
-DHAS_SCREEN=0
-DMESHTASTIC_EXCLUDE_SCREEN=1
!pkg-config --libs openssl --silence-errors || :
; openssl@3 is the keg-only Homebrew formula; --cflags is required so the
; compiler finds <openssl/*.h> in the Homebrew prefix (not just the linker).
!pkg-config --cflags --libs openssl --silence-errors || :
; PiWebServer (src/mesh/raspihttp/PiWebServer.cpp) auto-engages when ulfius
; headers are reachable via `#if __has_include(<ulfius.h>)`. The `|| :`
; tail keeps the build green when the user hasn't run `brew install ulfius`
; — they just don't get the HTTP API in that case.
!pkg-config --cflags --libs liborcania --silence-errors || :
!pkg-config --cflags --libs libyder --silence-errors || :
!pkg-config --cflags --libs libulfius --silence-errors || :
; src/input/Linux*.{cpp,h} drive evdev (`<linux/input.h>`) which doesn't exist
; on macOS. graphics/Panel_sdl.* and graphics/TFTDisplay.cpp pull LovyanGFX
; (which we lib_ignore on macOS for the <malloc.h> issue). Neither is needed
@@ -206,3 +217,28 @@ build_src_filter = ${native_base.build_src_filter}
lib_ignore =
${portduino_base.lib_ignore}
LovyanGFX
; ---------------------------------------------------------------------------
; Same as [env:native-macos] but built with AddressSanitizer for catching
; use-after-free, leaks, and OOB access during local development. Headless
; (no SDL/X11/libinput) so it stays cheap to build. Mirrors the shape of
; [env:native-tft-debug] but without the TFT/X11 dependencies.
;
; pio run -e native-macos-debug
; .pio/build/native-macos-debug/meshtasticd -s
;
; ASan runtime tuning (set in the shell before launching):
; ASAN_OPTIONS=detect_leaks=1:halt_on_error=0:abort_on_error=1
; MallocStackLogging=1 # macOS: nicer stack traces in malloc reports
; ---------------------------------------------------------------------------
[env:native-macos-debug]
extends = native_base
build_type = debug
build_unflags = ${env:native-macos.build_unflags}
build_flags = ${env:native-macos.build_flags}
-O0
-g
-fsanitize=address
-fno-omit-frame-pointer
build_src_filter = ${env:native-macos.build_src_filter}
lib_ignore = ${env:native-macos.lib_ignore}