mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-04 03:18:10 +10:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4386ea26f5 | ||
|
|
41f53177a1 | ||
|
|
2d761f6453 | ||
|
|
7cb071c780 | ||
|
|
0240a00d09 | ||
|
|
c0fcf807c0 | ||
|
|
55f40ecdfd | ||
|
|
90744ee0b7 | ||
|
|
4ee9598107 | ||
|
|
7066abbb86 | ||
|
|
21cef8c2e5 | ||
|
|
173ac58ed7 | ||
|
|
83adfd417a | ||
|
|
24d64a0013 | ||
|
|
3a87fc82c0 | ||
|
|
478444eb02 | ||
|
|
ad23c42fcc | ||
|
|
ad03f6f6d8 |
@@ -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 -->
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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?"
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.plan
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.specify
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.tasks
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.taskstoissues
|
||||
---
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
Executable
+193
@@ -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
|
||||
Executable
+329
@@ -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
|
||||
}
|
||||
Executable
+335
@@ -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
|
||||
Executable
+75
@@ -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
@@ -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
|
||||
@@ -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 -->
|
||||
@@ -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 -->
|
||||
@@ -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] |
|
||||
@@ -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]
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Vendored
+11
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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."
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"environment_name": "hypothetical_esp32s3_board",
|
||||
"hardware_model": "9981",
|
||||
"display_name": "Hypothetical ESP32-S3 Board",
|
||||
"architecture": "esp32-s3"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
+1
-1
Submodule protobufs updated: 249a80855a...1d6f1a71ff
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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" */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user