AXP Magisk resurrection track: Speckit 009 port AXP/LOS22 to LOS23.

Ship Magisk like GOLDEN; KernelSU optional recycle only; parallel to sunfish-a16-rom standby.
This commit is contained in:
sunfish-admin committed 2026-09-16 21:56:22 +00:00
commit 12d2179c20
150 files changed
+26263

No files matched your search

+259
View File
@@ -0,0 +1,259 @@
---
name: "speckit-analyze"
description: "Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/analyze.md"
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before analysis)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_analyze` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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 Goal.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## 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.)
### 9. 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_analyze` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## 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
+373
View File
@@ -0,0 +1,373 @@
---
name: "speckit-checklist"
description: "Generate a custom checklist for the current feature based on user requirements."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/checklist.md"
---
## 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).
## Pre-Execution Checks
**Check for extension hooks (before checklist generation)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_checklist` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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 Execution Steps.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## 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. **IF EXISTS**: Load `.specify/memory/constitution.md` for project principles and governance constraints.
3. **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.
4. **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)
5. **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
6. **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]?"
7. **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.
8. **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?"
## Post-Execution Checks
**Check for extension hooks (after checklist generation)**:
Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_checklist` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
+291
View File
@@ -0,0 +1,291 @@
---
name: "speckit-clarify"
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."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/clarify.md"
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before clarification)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_clarify` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## 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. **IF EXISTS**: Load `.specify/memory/constitution.md` for project principles and governance constraints.
3. 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)
4. 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.
5. Sequential questioning loop (interactive):
- Present EXACTLY ONE question at a time.
- **Question writing quality (applies to every question, MC or short-answer):**
- Lead with `**Question:**` followed by a full interrogative that ends with `?`. The question text before the `?` must make sense on its own.
- NEVER use a topic label, section heading, or requirement id as the question itself. For example, `Acceptance device/runtime matrix (FR-023)` is INVALID — it is a label, not a question.
- After the `?`, the only permitted suffix is an optional parenthesized requirement/question id. Exact format: `**Question:** <interrogative>?` or `**Question:** <interrogative>? (FR-023)`. Never put the id before the `?`, and never use the id (alone or with a topic label) as the whole prompt.
- Immediately after the question line, add one plain-language "Why it matters" sentence (the stake for acceptance or shipping) before the recommendation/options.
- Use everyday wording; introduce jargon only if defined in the same sentence. Self-check: a reader who does not know Spec Kit must be able to answer from the Question line alone. Terse is fine; cryptic labels are not.
- 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.
6. 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).
7. 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.
8. Write the updated spec back to `FEATURE_SPEC`.
9. **Re-validate Spec Quality Checklist** (if it exists):
- Check if `FEATURE_DIR/checklists/requirements.md` exists.
- If it does NOT exist, skip this step silently.
- If it exists:
1. Read the checklist file.
2. Identify all GitHub task-list checkbox lines — lines matching `- [ ]`, `- [x]`, or `- [X]` (case-insensitive, tolerant of leading whitespace for nested items) outside of code fences. Ignore all other content (headings, notes, non-checkbox bullets, metadata).
3. For each checkbox line, record its current marker state (checked or unchecked) and item text into a before-snapshot list.
4. Re-evaluate each checkbox item against the **updated** spec (the version just saved in step 7).
5. For each checkbox item, update only if the checked/unchecked state actually changes:
- If the item now passes and was unchecked: change `[ ]` to `[x]`.
- If the item now fails and was checked: change `[x]`/`[X]` to `[ ]`.
- If the state is unchanged: leave the marker as-is (preserve existing case to avoid cosmetic diffs).
6. Save the updated checklist file. **Only toggle the `[ ]`/`[x]` marker portion of checkbox lines whose state changed.** All other file content — headings, metadata, notes, line ordering, whitespace — must remain unchanged to avoid noisy diffs.
7. Compare the before-snapshot with the current state to compute three lists for the Completion Report:
- **Newly passing**: items that changed from unchecked to checked.
- **Regressions**: items that changed from checked to unchecked.
- **Still unchecked**: items that remain unchecked.
8. Record the before/after pass counts as checked/total checkbox items (e.g., "12/16 → 15/16 items passing").
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
## Mandatory Post-Execution Hooks
**You MUST complete this section before reporting completion to the user.**
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_clarify`, skip to the Completion Report.
- If it exists, read it and look for entries under the `hooks.after_clarify` key.
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report.
- 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- For each executable hook, output the following based on its `optional` flag:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion Report
Report completion (after questioning loop ends or early termination):
- Number of questions asked & answered.
- Path to updated spec.
- Sections touched (list names).
- Spec quality checklist status (if `FEATURE_DIR/checklists/requirements.md` was re-validated): show before/after pass counts (e.g., "Spec Quality Checklist: 12/16 → 15/16 items passing") and list any items that changed state — both newly checked (unchecked → checked) and any regressions (checked → unchecked). If any items remain unchecked, list them as areas needing attention.
- 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.
## Done When
- [ ] Spec ambiguities identified and clarifications integrated into spec file
- [ ] Spec quality checklist re-validated against updated spec (if `FEATURE_DIR/checklists/requirements.md` exists)
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with questions answered, sections touched, checklist status, and coverage summary
@@ -0,0 +1,168 @@
---
name: "speckit-constitution"
description: "Create or update the project constitution from interactive or provided principle inputs."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/constitution.md"
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Scope Guard
This command's own work is limited to updating the project constitution itself. Dependent templates
and commands read the constitution at runtime and are not modified here.
- Classify every part of the user input as either constitution content or a separate,
non-governance intent.
- If the input includes feature implementation, code generation, refactoring, building, or
deployment requests, you **MUST NOT** execute them. Extract them as deferred intents instead.
- You **MUST NOT** create, modify, or delete application source files, feature routes,
components, tests, deployment files, or other artifacts unrelated to the constitution
workflow.
- If it is unclear whether an instruction is constitution content, ask for clarification before
making changes.
- After completing the constitution update, include a `Next Actions` section for each deferred
intent. List the original intent and suggest the appropriate follow-up Spec Kit command, such
as `/speckit-specify`, without invoking it.
- If there are no non-governance intents, omit the `Next Actions` section.
## Pre-Execution Checks
**Check for extension hooks (before constitution update)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_constitution` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## 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 and (b) fill the template precisely.
**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. 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
- Follow-up TODOs if any placeholders intentionally deferred.
5. 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).
6. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite).
7. Output a final summary to the user with:
- New version and bump rationale.
- Any TODO placeholders or deferred items requiring manual follow-up.
- Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`).
- A `Next Actions` section for any deferred non-governance intents.
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.
## Post-Execution Checks
**Check for extension hooks (after constitution update)**:
Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_constitution` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
+277
View File
@@ -0,0 +1,277 @@
---
name: "speckit-converge"
description: "Assess the current codebase against the feature's spec, plan, and tasks, then append any remaining unbuilt work as new tasks to tasks.md so implement can complete it."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/converge.md"
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before convergence)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_converge` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```text
## Extension Hooks
**Optional Pre-Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```text
## Extension Hooks
**Automatic Pre-Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
Wait for the result of the hook command before proceeding to the Goal.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Goal
Close the gap between what a feature's specification, plan, and tasks call for and what the
codebase currently implements. Read `spec.md`, `plan.md`, and `tasks.md` as the **sole
source of intent** (with the constitution as governing constraints), assess the current
state of the code, determine which requirements, acceptance criteria, plan decisions, and
existing tasks are unmet, incomplete, or only partially satisfied, and **append each piece
of remaining work as a new, traceable task** at the bottom of `tasks.md` so that
`/speckit-implement` can complete it. This command MUST run only after
`/speckit-implement` has run on the current `tasks.md`, and after `/speckit-tasks` has produced a complete `tasks.md`.
This is **not** a diff tool and does **not** track changes. It assesses the present state
of the code relative to the feature's artifacts — no git, no branch comparison, no history.
## Operating Constraints
**APPEND-ONLY, NEVER REWRITE**: The command's **only** write is appending a new
`## Phase N: Convergence` section to `tasks.md`. It MUST NOT:
- modify `spec.md` or `plan.md` in any way;
- rewrite, renumber, reorder, or delete any existing task (including tasks from a prior
Convergence phase);
- modify, create, or delete any application code — completing the appended tasks is the
job of `/speckit-implement`.
When the codebase already satisfies everything, the command MUST leave `tasks.md`
**byte-for-byte unchanged** (no empty Convergence header) and report a clean result.
**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is
**non-negotiable**. Code that violates a MUST principle is the highest-severity finding and
produces a corresponding remediation task. If the constitution is an unfilled template,
skip constitution checks gracefully rather than failing.
## Execution Steps
### 1. Initialize Convergence 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
- CONSTITUTION = `.specify/memory/constitution.md` (if present)
If `spec.md`, `plan.md`, or `tasks.md` is missing, STOP with a clear, actionable message naming the
prerequisite command to run (`/speckit-specify` for a missing spec, `/speckit-plan` for a missing plan,
`/speckit-tasks` for missing tasks). Do not produce partial output.
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:**
- Functional Requirements (FR-###)
- Success Criteria (SC-###) — include only items requiring buildable work; exclude
post-launch outcome metrics and business KPIs
- User Stories and their Acceptance Scenarios
- Edge Cases (if present)
**From plan.md:**
- Architecture/stack choices and technical decisions
- Data Model references
- Phases and named touch-points (files/components the plan says will be created or edited)
- Technical constraints
**From tasks.md:**
- Task IDs (to compute the next ID and next phase number)
- Descriptions, phase grouping, and referenced file paths
**From constitution (if not an unfilled template):**
- Principle names and MUST/SHOULD normative statements
### 3. Build the Intent Inventory
Create an internal model (do not echo raw artifacts):
- **Requirements inventory**: one stable key per FR-### / SC-### / user-story acceptance
scenario (e.g. `US1/AC2`), plus the plan decisions and constitution principles that
impose buildable obligations.
- **Code-scope map**: from the file paths named in `plan.md` and `tasks.md`, plus a keyword
search for the concepts each requirement describes, derive the set of source files and
components in scope for assessment. Bound the assessment to these — do **not** infer
scope beyond what the artifacts define.
### 4. Assess the Codebase and Classify Findings
For each item in the intent inventory, inspect the current code in scope and produce a
`Finding` only where there is a gap. Classify every finding by **gap type**:
- **`missing`**: the required work is absent from the code entirely.
- **`partial`**: the work exists but does not yet fully satisfy the requirement /
acceptance criterion / plan decision.
- **`contradicts`**: the code does something that conflicts with stated intent or a
constitution MUST principle.
- **`unrequested`**: the code contains work not called for by the spec, plan, or tasks
(surfaced for awareness — converge does **not** delete code, it only appends a task to
review/justify or remove it).
Each `Finding` records: a stable id, the `source-ref` it traces to, the `gap-type`, a
severity, and a short human-readable description with the evidence (the file/area observed).
**Edge cases:**
- **Little or no code yet**: treat the entire specified scope as `missing` remaining work
rather than failing.
- **Nothing remains**: produce zero findings and follow the converged branch in Step 7.
### 5. Assign Severity
- **CRITICAL**: violates a constitution MUST principle, or a `missing`/`contradicts` gap
that blocks baseline functionality of a P1 user story.
- **HIGH**: a `missing` or `partial` gap on a core functional requirement or acceptance
criterion.
- **MEDIUM**: a `partial` gap on a secondary requirement, or an `unrequested` addition with
unclear justification.
- **LOW**: minor partial gaps, polish, or low-risk `unrequested` additions.
### 6. Present the In-Session Findings Summary
Before appending anything, output a compact, severity-graded summary (no file writes yet):
## Convergence Findings
| ID | Gap Type | Severity | Source | Evidence | Remaining Work |
|----|----------|----------|--------|----------|----------------|
| F1 | missing | HIGH | FR-008 | Example: no append-only guard detected in path/to/module.py when writing tasks.md | Add append-only enforcement |
**Summary metrics:**
- Requirements / acceptance criteria checked
- Plan decisions checked
- Constitution principles checked (or "skipped — template")
- Findings by gap type (missing / partial / contradicts / unrequested)
- Findings by severity
### 7. Append Convergence Tasks (or report converged)
**If there are one or more actionable findings** (`tasks_appended` outcome):
Append to the **end** of `tasks.md`, per the append contract:
1. Scan all existing task IDs; let `M` be the maximum. Determine the next phase number `N`
(highest existing phase + 1).
2. Write a single new section header `## Phase N: Convergence`.
3. Emit one checklist item per actionable finding, ordered CRITICAL/HIGH first, assigning
zero-padded IDs `T{M+1:03d}, T{M+2:03d}, …`:
```markdown
- [ ] T042 <imperative description> per <source-ref> (<gap-type>)
```
`<source-ref>` traces the task to its origin: e.g. `FR-003`, `SC-002`,
`US1/AC2`, `plan: storage decision`, `Constitution II`.
`<gap-type>` is one of `missing`, `partial`, `contradicts`, `unrequested`.
Constitution-violation tasks MUST be emitted first and described as
`CRITICAL`.
4. Never reuse or renumber existing IDs. If a prior Convergence phase exists, add a new,
separately-numbered one below it — do not touch the old one.
**If there are no actionable findings** (`converged` outcome):
- Do **not** modify `tasks.md` at all — no empty phase header.
- Report: **"✅ Converged — the implementation satisfies the spec, plan, and tasks."**
- Include the summary counts of what was checked.
### 8. Provide Next Actions (Handoff)
- On `tasks_appended`: state how many tasks were appended under which phase, and recommend
running `/speckit-implement` to complete them; note that a follow-up converge
run will find fewer or no remaining items.
- On `converged`: recommend proceeding to review / opening a PR. No further implement pass
is needed for this feature's specified scope.
### 9. Check for extension hooks
After producing the result, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_converge` 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
- Report the convergence outcome (`converged` or `tasks_appended`) in-session before listing
any hooks, so users can decide whether to run optional follow-up commands.
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- For each executable hook, output the following based on its `optional` flag:
- **Optional hook** (`optional: true`):
```text
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
- **Mandatory hook** (`optional: false`):
```text
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
+223
View File
@@ -0,0 +1,223 @@
---
name: "speckit-implement"
description: "Execute the implementation plan by processing and executing all tasks defined in tasks.md"
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/implement.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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- 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 .specify/memory/constitution.md for governance 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
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.
## Mandatory Post-Execution Hooks
**You MUST complete this section before reporting completion to the user.**
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_implement`, skip to the Completion Report.
- 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 to the Completion Report.
- 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- For each executable hook, output the following based on its `optional` flag:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion Report
Report final status with summary of completed work.
## Done When
- [ ] All tasks in tasks.md completed and marked `[X]`
- [ ] Implementation validated against specification, plan, and test coverage
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with summary of completed work
+166
View File
@@ -0,0 +1,166 @@
---
name: "speckit-plan"
description: "Execute the implementation planning workflow using the plan template to generate design artifacts."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/plan.md"
---
## 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- 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
- Re-evaluate Constitution Check post-design
## Mandatory Post-Execution Hooks
**You MUST complete this section before reporting completion to the user.**
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_plan`, skip to the Completion Report.
- 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 to the Completion Report.
- 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- For each executable hook, output the following based on its `optional` flag:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion Report
Command ends after Phase 1 design. Report branch, IMPL_PLAN path, and generated artifacts.
## 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. **Create quickstart validation guide** → `quickstart.md`:
- Document runnable validation scenarios that prove the feature works end-to-end
- Include prerequisites, setup commands, test/run commands, and expected outcomes
- Use links or references to contracts and data model details instead of duplicating them
- Do not include full implementation code, model/service/controller bodies, migrations, or complete test suites
- Keep this artifact as a validation/run guide; implementation details belong in `tasks.md` and the implementation phase
**Output**: data-model.md, /contracts/*, quickstart.md
## Key rules
- Use absolute paths for filesystem operations; use project-relative paths for references in documentation
- ERROR on gate failures or unresolved clarifications
## Done When
- [ ] Plan workflow executed and design artifacts generated
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with branch, plan path, and generated artifacts
+345
View File
@@ -0,0 +1,345 @@
---
name: "speckit-specify"
description: "Create or update the feature specification from a natural language feature description."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/specify.md"
---
## 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- 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 feature:
- 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. **Branch creation** (optional, via hook):
If a `before_specify` hook ran successfully in the Pre-Execution Checks above, it will have created/switched to a git branch and output JSON containing `BRANCH_NAME` and `FEATURE_NUM`. Note these values for reference, but the branch name does **not** dictate the spec directory name.
If the user explicitly provided `GIT_BRANCH_NAME`, pass it through to the hook so the branch script uses the exact value as the branch name (bypassing all prefix/suffix generation).
3. **Create the spec feature directory**:
Specs live under the default `specs/` directory unless the user explicitly provides `SPECIFY_FEATURE_DIRECTORY`.
**Resolution order for `SPECIFY_FEATURE_DIRECTORY`**:
1. If the user explicitly provided `SPECIFY_FEATURE_DIRECTORY` (e.g., via environment variable, argument, or configuration), use it as-is
2. Otherwise, auto-generate it under `specs/`:
- Check `.specify/init-options.json` for `feature_numbering` (preferred) or `branch_numbering` (deprecated, migration only — will be removed in a future release)
- If `"timestamp"`: prefix is `YYYYMMDD-HHMMSS` (current timestamp)
- If `"sequential"` or absent: prefix is `NNN` (next available 3-digit number after scanning existing directories in `specs/`)
- Construct the directory name: `<prefix>-<short-name>` (e.g., `003-user-auth` or `20260319-143022-user-auth`)
- Set `SPECIFY_FEATURE_DIRECTORY` to `specs/<directory-name>`
- If `branch_numbering` was used (and `feature_numbering` was absent), emit a one-line warning: "⚠️ `branch_numbering` in init-options.json is deprecated. Rename to `feature_numbering`."
**Create the directory and spec file**:
- `mkdir -p SPECIFY_FEATURE_DIRECTORY`
- Resolve the active `spec-template` through the Spec Kit preset/template resolution stack (equivalent to `specify preset resolve spec-template`)
- Copy the resolved `spec-template` file to `SPECIFY_FEATURE_DIRECTORY/spec.md` as the starting point
- Set `SPEC_FILE` to `SPECIFY_FEATURE_DIRECTORY/spec.md`
- Persist the resolved path to `.specify/feature.json`:
```json
{
"feature_directory": "<resolved feature dir>"
}
```
Write the actual resolved directory path value (for example, `specs/003-user-auth`), not the literal string `SPECIFY_FEATURE_DIRECTORY`.
This allows downstream commands (`/speckit-plan`, `/speckit-tasks`, etc.) to locate the feature directory without relying on git branch name conventions.
**IMPORTANT**:
- You must only create one feature per `/speckit-specify` invocation
- The spec directory name and the git branch name are independent — they may be the same but that is the user's choice
- The spec directory and file are always created by this command, never by the hook
4. Load the resolved active `spec-template` file to understand required sections.
5. **IF EXISTS**: Load `.specify/memory/constitution.md` for project principles and governance constraints.
6. Follow this execution flow:
1. Parse user description from arguments
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)
7. 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.
8. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria:
a. **Create Spec Quality Checklist**: Generate a checklist file at `SPECIFY_FEATURE_DIRECTORY/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 the Mandatory Post-Execution Hooks section
- **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
## Mandatory Post-Execution Hooks
**You MUST complete this section before reporting completion to the user.**
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_specify`, skip to the Completion Report.
- 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 to the Completion Report.
- 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- For each executable hook, output the following based on its `optional` flag:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion Report
Report completion to the user with:
- `SPECIFY_FEATURE_DIRECTORY` — the feature directory path
- `SPEC_FILE` — the spec file path
- Checklist results summary
- Readiness for the next phase (`/speckit-clarify` or `/speckit-plan`)
**NOTE:** Branch creation is handled by the `before_specify` hook (git extension). Spec directory and file creation are always handled by this core command.
## 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)
## Done When
- [ ] Specification written to `SPEC_FILE` and validated against quality checklist
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with feature directory, spec file path, and checklist results
+214
View File
@@ -0,0 +1,214 @@
---
name: "speckit-tasks"
description: "Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/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 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
## Outline
1. **Setup**: Run `.specify/scripts/bash/setup-tasks.sh --json` from repo root and parse FEATURE_DIR, TASKS_TEMPLATE, and AVAILABLE_DOCS list. `FEATURE_DIR` and `TASKS_TEMPLATE` must be absolute paths when provided. `AVAILABLE_DOCS` is a list of document names/relative paths available under `FEATURE_DIR` (for example `research.md` or `contracts/`). 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)
- **IF EXISTS**: Load `.specify/memory/constitution.md` for project principles and governance constraints
- 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**: Read the tasks template from TASKS_TEMPLATE (from the JSON output above) and use it as structure. If TASKS_TEMPLATE is empty, fall back to `.specify/templates/tasks-template.md`. 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)
## Mandatory Post-Execution Hooks
**You MUST complete this section before reporting completion to the user.**
Check if `.specify/extensions.yml` exists in the project root.
- If it does not exist, or no hooks are registered under `hooks.after_tasks`, skip to the Completion Report.
- 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 to the Completion Report.
- 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- For each executable hook, output the following based on its `optional` flag:
- **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**:
```
## Extension Hooks
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- **Optional hook** (`optional: true`):
```
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
## Completion 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)
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
## Done When
- [ ] tasks.md generated with all phases, task IDs, and file paths
- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above
- [ ] Completion reported to user with task count, story breakdown, and MVP scope
@@ -0,0 +1,109 @@
---
name: "speckit-taskstoissues"
description: "Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts."
compatibility: "Requires spec-kit project structure with .specify/ directory"
metadata:
author: "github-spec-kit"
source: "templates/commands/taskstoissues.md"
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Pre-Execution Checks
**Check for extension hooks (before tasks-to-issues conversion)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_taskstoissues` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- 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").
1. **IF EXISTS**: Load `.specify/memory/constitution.md` for project principles and governance constraints.
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. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by three digits, e.g. `T001`). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3}\b` (word boundaries so tokens like `ST001` or `T0010` are not matched by mistake; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked.
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. Task lines in `tasks.md` start with a markdown checkbox, so first strip the leading `- [ ]` (and any `[P]` / `[US#]` markers) to recover the task ID and its description. Create the issue with a single canonical title of the form `T001: <description>`, with the ID written once followed by the task description (for example, the line `- [ ] T001 Create project structure` becomes the title `T001: Create project structure`).
- **Skip** any task whose ID is already present in the set of existing issues from the previous step, and report it (for example, `T001 already has an issue, skipping`).
- Only create issues for tasks that do not yet have a matching issue.
> [!CAUTION]
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL
## Post-Execution Checks
**Check for extension hooks (after tasks-to-issues conversion)**:
Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_taskstoissues` 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
- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
- 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}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
+43
View File
@@ -0,0 +1,43 @@
# sunfish-a16-rom
## Git
.DS_Store
*.swp
*~
.idea/
.vscode/
# Spec Kit local state (also ignored under .specify/)
.specify/feature.json
# Private signing material / keystores (public *.x509.pem certs MAY be committed)
*.jks
*.keystore
*.pk8
**/release-keys/
**/signing-keys/
**/private-keys/
**/*_private_key*
**/*.pem.key
# Proprietary binaries / device captures (store on rom-dev01, not Git)
*.apk
*.apkm
*.xapk
**/bugreport*
**/reference/raw/
# Android build trees (must live on rom-dev01)
.repo/
out/
ccache/
/.ccache/
# Local env / laptop artifact pulls
.env
.env.*
.local-artifacts/
# 004 proprietary / large prebuilts (host-only)
vendor/sunfish-a16/prebuilt/
*.apk
+9
View File
@@ -0,0 +1,9 @@
# Machine-local Spec Kit state — not meant to be shared.
# Managed by the Specify CLI; safe to edit (your changes are preserved on refresh).
# Local pointer to the current feature directory. Rewritten every time you
# switch features, so it is per-checkout state rather than something to share.
feature.json
# Per-machine extension config overrides.
extensions/*/local-config.yml
+9
View File
@@ -0,0 +1,9 @@
{
"ai": "cursor-agent",
"ai_skills": true,
"feature_numbering": "sequential",
"here": true,
"integration": "cursor-agent",
"script": "sh",
"speckit_version": "0.16.1"
}
+15
View File
@@ -0,0 +1,15 @@
{
"version": "0.16.1",
"integration_state_schema": 1,
"installed_integrations": [
"cursor-agent"
],
"integration_settings": {
"cursor-agent": {
"script": "sh",
"invoke_separator": "-"
}
},
"integration": "cursor-agent",
"default_integration": "cursor-agent"
}
@@ -0,0 +1,17 @@
{
"integration": "cursor-agent",
"version": "0.16.1",
"installed_at": "2026-08-09T08:59:06.444541+00:00",
"files": {
".cursor/skills/speckit-analyze/SKILL.md": "0b5d1d053de3d181cd5c4d9c307a273abf67fd4be0023e67e0c8082d73baa35b",
".cursor/skills/speckit-clarify/SKILL.md": "e1a1ddba9346721f41e7b8d0ad278b54d69399c0625db9d753756fd33e51a6e1",
".cursor/skills/speckit-constitution/SKILL.md": "deba621f10b5aea5601711654fdff009880ba0ebff795b42adfe378b3a04c8f0",
".cursor/skills/speckit-implement/SKILL.md": "7b5e8ecd9b47fbcd84bb89f70cd8c60d15ee9ea43d3dde9dfa42fffb2433109c",
".cursor/skills/speckit-converge/SKILL.md": "f73713dcbd10aa7b252bfae1400a63a606f033fe9492b44f77a0cb1ab40ac090",
".cursor/skills/speckit-plan/SKILL.md": "3d13e010accc60766db3549a52f598e643f5f6c26570a475df418c7a74ba4cbb",
".cursor/skills/speckit-checklist/SKILL.md": "a0540c12d4224fc038abdbc06b883d0eec9aea28c77b63b4e405a8d0644f73b0",
".cursor/skills/speckit-specify/SKILL.md": "d0b83970b14be40bd1dcd5d3c15f3186c37ebebbe4617dda7e8d487763b70f79",
".cursor/skills/speckit-tasks/SKILL.md": "8eb4d6c31f861b01ee01730d02380282ed7b6fd6c7870123e0c4c5346530fa03",
".cursor/skills/speckit-taskstoissues/SKILL.md": "9485e69916af164f8067d87b021bbee7c1813b145765b03845155c93a544b772"
}
}
@@ -0,0 +1,18 @@
{
"integration": "speckit",
"version": "0.16.1",
"installed_at": "2026-08-09T08:59:06.466884+00:00",
"files": {
".specify/scripts/bash/setup-plan.sh": "4469b22960f43c07c33dca00de6dedb252145e9a9ce8fbb0e63be82e02b082ab",
".specify/scripts/bash/setup-tasks.sh": "cf21ba2212b4dd5b435c5ea8527500cfd27768b86c0bbc7ebc3207759f118d27",
".specify/scripts/bash/common.sh": "6ff86bf39f6b4684b0f80927dc7a1dadec26b4671988a3fe4d6c2523cbd3aa22",
".specify/scripts/bash/create-new-feature.sh": "ad09a94a2c1107e25e5386a834da1d7a31f9abb06ab8bfd323a7b84038221e39",
".specify/scripts/bash/check-prerequisites.sh": "a7d8a14ecf87332b600cd966b5d0e7cb9d594abce7e4d1ee4372b2b5b3efff06",
".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3",
".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e",
".specify/templates/plan-template.md": "7e637502d41eccf0ca672496636365691fdca62ef37b27ec07fcb412dbfa90d4",
".specify/templates/checklist-template.md": "709d8ab8384a3a49f5e0f64479f71553ef6d6f8bb4f00281b05f47837993b536",
".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842",
".specify/.gitignore": "8c908410d177a1ef3d0dee16d7ad55f2ac3333df3104c4d4adee1c9b82f1dbc1"
}
}
@@ -0,0 +1,4 @@
{
"sha256": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3",
"source": "core"
}
+580
View File
@@ -0,0 +1,580 @@
# sunfish-a16-rom Constitution
Project: Android 16 custom ROM for Google Pixel 4a (`sunfish`) —
**AXP.OS Magisk resurrection** track (`AXP_Resurection`).
This project resurrects AXP.OS Pro capability on LineageOS 23.2 / Android 16
for sunfish (Magisk like GOLDEN). Functionality and application compatibility
first, then hardening. The project must remain debuggable and reproducible.
This constitution is specific to `AXP_Resurection`. Sibling `sunfish-a16-rom`
is the KernelSU standby track. It does **not** inherit SSYSTEMS, company
GitLab, Redmine, or buildout-saar governance.
**Version**: 0.3.0 | **Ratified**: 2026-08-09 | **Last Amended**: 2026-09-16
## Core Principles
### I. Independence from SSYSTEMS governance
This is a personal project. Code, Spec Kit, documentation, Git hosting, and
workflow are owned by this project.
Do not apply SSYSTEMS `AGENTS.md`, Redmine tickets, `r{ticket}_*` branch rules,
mandatory company MRs, production-read-only company policy, client conventions,
company GitLab, or corporate CI unless an explicit later decision says otherwise.
`pm-test01` (or any company hypervisor) may host `rom-dev01` with permission.
That is infrastructure only. It is **not** part of this project's logical
architecture or governance.
### II. Spec-as-source
For each feature, `specs/NNN-*/spec.md` is the only human-edited requirements
source. `plan.md` and `tasks.md` are derived artifacts and must be regenerated
after meaningful spec changes (`/speckit-plan`, `/speckit-tasks`).
Unresolved technical decisions must be investigated in the relevant feature
spec. Do not invent implementation details in the constitution, commits, or
agent improvisation.
### III. Functionality first, hardening second
Compatibility with the known-good AXP.OS workflow on this sunfish device is the
MVP gate. Hardening must not knowingly break that baseline without the
regression being isolated, documented, and reversible.
**AXP.OS Pro parity intent:** the product goal is to reproduce **AXP.OS Pro**
functionality, except features that are obsolete, incompatible with LineageOS
23.2 / Android 16 / sunfish, or deliberately substituted (for example Magisk →
KernelSU-Next; AXP backup tooling → NeoBackup as an external user tool).
`docs/axp-pro-parity.md` and `docs/hardening-backlog.md` are **non-normative**
product/backlog trackers. A row becomes a requirement only when promoted into a
feature `spec.md`. For an active feature, this constitution plus that feature's
`spec.md` remain authoritative if a backlog row conflicts.
### IV. Small, attributable, reversible changes
Prefer minimal patches with clear upstream attribution. Never change several
high-risk subsystems at once when the result cannot be attributed to one change.
Keep a known-good flashable build for each completed stage.
### V. Prefer native, auditable ROM functionality
Prefer ROM-native features over LSPosed, Zygisk, or root modules when a feature
can reasonably be implemented in the ROM. Do not make unrestricted signature
spoofing a general platform capability.
## Core architecture
- **Base ROM**: LineageOS 23.2 / Android 16 for sunfish.
- **Base kernel / device stack**: the stack compatible with that LineageOS 23.2
sunfish build.
- Do **not** substitute an old GrapheneOS, stock Android 13, or unrelated
prebuilt kernel image.
- Port applicable historical GrapheneOS sunfish kernel hardening into the
current kernel where still relevant. (Last GrapheneOS sunfish line was
**Android 13** — useful as historical reference only; GOLDEN today runs
**AXP on Android 15**, which is a different stack.)
- Use modern GrapheneOS Android 16 implementations as the preferred reference
for framework-level hardening where they can be cleanly adapted.
- Use modern GrapheneOS **kernel** hardening as a technique/reference source as
well: identify the security property provided by current implementations and
evaluate whether an equivalent can be safely adapted or backported to the
LineageOS sunfish 4.14 kernel. Do **not** directly transplant patches that
depend on newer GKI/kernel/hardware facilities.
- Audit the AXP.OS sunfish kernel patch stack, including AXP-specific changes as
well as patches inherited from DivestOS, GrapheneOS or upstream. Prefer the
original upstream source when provenance can be established and avoid applying
duplicate/equivalent fixes already present in LineageOS.
- Port applicable security/privacy patches from AXP.OS and DivestOS.
- Use AXP.OS as the principal compatibility reference (microG, patched
Phonesky/Google Play, signature spoofing, call recording, privacy behaviour).
- Use msg-GrapheneOS only as a reference for concepts such as KernelSU/SukiSU
integration, SUSFS, root-on-boot modes, signing, and OTA design.
### Reference source order
| Role | Source |
|------|--------|
| Base | LineageOS 23.2 |
| Compatibility / privacy behaviour | AXP.OS |
| Modern + historical sunfish hardening | GrapheneOS |
| Upstream/historical patches behind AXP | DivestOS |
DivestOS is a recognised reference, not a fourth ROM base.
## Compatibility baseline
There are **two** Pixel 4a (`sunfish`) devices — see `docs/device-roles.md`:
- **GOLDEN** — daily-driver AXP.OS with working banks/microG/Phonesky/Insular;
**do not** flash experimental project ROMs on it.
- **LAB** — dedicated flash/test device; all experimental images go here.
The GOLDEN AXP installation is the live compatibility **oracle** (essential
state documented in `002`; may be re-queried via `adb` later). Broader product
intent is AXP.OS Pro feature parity per `docs/axp-pro-parity.md`.
The new ROM must reproduce at minimum (MVP hard gates):
- working microG;
- working patched Google Play / Phonesky;
- working push notifications;
- Insular managed profile;
- Yuh working inside Insular;
- Swissquote working inside Insular;
- application installation and updates through patched Google Play;
- banking applications running without root privileges.
**Yuh** and **Swissquote** inside Insular are hard-gates of the compatibility MVP.
**N26** and **Revolut** are later optional targets and must not block the MVP.
### MVP levels
| Level | Through | Meaning |
|-------|---------|---------|
| **Compatibility MVP** | `007` | microG, patched Phonesky, managed profile, Yuh and Swissquote PASS, Play install/update; banking apps never receive root |
| **Daily-driver MVP** | `010` | Compatibility MVP plus accepted KernelSU integration, native call recording, FLAG_SECURE and accepted usability/privacy features |
`007` owns banking acceptance. A rootless snapshot of our current ROM is recorded
before adding root; PASS is not a prerequisite. `006` is deferred. Root is not
accepted unless both Yuh and Swissquote pass on the tested stack. If experiments
fail, keep the feature incomplete or withdraw root; do not claim compatibility.
Signing/AVB (`011`) repeats banking regression; hardening follows in `012`–`013`.
Island is the current LAB managed-profile tool; historical Insular references
remain valid as reference workflows, not a requirement to install both tools.
Exact package versions, signatures, Magisk/DenyList state, UIDs, and related
inventory belong in `002-axp-reference-capture`. Do not invent those values here.
## Google compatibility stack
Expected package architecture:
- `com.google.android.gms` = microG Services Core
- `com.google.android.gsf` = GSF Proxy
- `com.android.vending` = patched Google Play / Phonesky
Do not install FakeStore or microG Companion if it conflicts with the real
patched `com.android.vending`.
Patched Phonesky must be integrated as a privileged system application with the
required permission / default-permission XML configuration.
Signature spoofing must be native, restricted to explicitly authorized
packages/signers, and auditable. It must not depend on LSPosed. microG and
patched Phonesky must continue working when optional root/Zygisk-style
functionality is unavailable.
Phonesky is an external/proprietary input. Do not commit Google proprietary APKs
to Git. `004-microg-phonesky-integration` defines the reproducible obtain →
patch → sign → priv-app procedure. Phonesky signing strategy must be decided
before shipping an updatable Play Store build.
Concrete spoofing implementation is decided in
`005-restricted-signature-spoofing` after studying LineageOS 23.2, microG, and
AXP. Prefer package/signer-restricted support over an open generic fake-signature
permission.
## Banking / Insular workflow
Preserve the known-good AXP golden banking flow as the initial compatibility
test (exact package/process names from `002`):
1. Install the banking application via **Aurora Store**.
2. **Do not open** the application.
3. Add it to Magisk DenyList **before** the first process start (include
relevant processes, not only the package name).
4. Clone/install it into the Island work profile (GOLDEN measured tool).
5. First launch occurs inside Island.
6. Patched Google Play is also available inside Island.
7. If the banking app requires a Play Store update, perform that update from
inside the work profile.
8. Re-test login, biometrics, push, confirmations, and reboot behaviour.
Insular should initially remain an ordinary user application for easier testing
and upgrades. It may later become a normal preinstalled system app, but not a
privileged app unless a concrete audited requirement exists.
Managed Mainland must remain optional and disabled by default.
microG, GSF Proxy, and patched Phonesky should eventually be automatically
available in managed profiles.
## Root architecture
**Ship root for this track (`AXP_Resurection` / `009`): Magisk**, matching
AXP.OS / GOLDEN (newest Magisk stable that works on sunfish / LOS 23.2;
AXP-like preinstalled but not fully activated layout; DenyList-class hiding
so banking apps never receive root).
KernelSU-Next / SusFS work from the sibling `sunfish-a16-rom` track remains
**out of ship here**. Existing functional KernelSU kernels MAY be recycled
for debug/reference only (`009` FR-016) and MUST NOT gate Magisk acceptance.
Required properties:
- root is explicitly granted only as Magisk intends (owner-approved apps);
- banking applications never receive root (DenyList-class);
- managed-profile UIDs must be tested independently from owner-profile UIDs;
- Magisk version pins follow newest-stable-then-pin policy (fetch fresh
upstreams; never ship from shallow/stale host copies).
Do not introduce Zygisk or LSPosed for ROM features that can be native
(spoofing, call recording, FLAG_SECURE). Magisk modules only where AXP/GOLDEN
parity truly requires them.
An optional boot mode with root unavailable may be implemented later as a
diagnostic/fallback mechanism.
### Relationship to sibling KernelSU track
The `sunfish-a16-rom` workspace/repo keeps the earlier KernelSU / split
roadmap on standby. This constitution applies only to **AXP_Resurection**.
Do not silently reintroduce KernelSU as the default ship root in this tree.
## Features integrated directly in the ROM
Prefer native ROM functionality for:
- unrestricted call recording independent of region. Use AXP.OS / EXTENDROM /
iodéOS as **behavioural and implementation references**, but re-investigate
Android 16 / LineageOS 23.2 Dialer, CarrierConfig, and framework paths before
porting. Do not assume AXP's older (e.g. Android 11/13) restriction-toggle
overlays apply unchanged to Android 16 (`008`);
- native configurable `FLAG_SECURE` override (`009`):
- Android's normal `FLAG_SECURE` behaviour remains the **default**;
- user-selectable **global** override only (no per-app lists);
- covers screenshots and screen recording/capture;
- no LSPosed, Zygisk, or root-module dependency;
- configurable hosts/DNS/privacy support;
- automatic reboot after configurable inactivity;
- configurable automatic Wi-Fi/Bluetooth shutdown where practical.
Do not require LSPosed for call recording or `FLAG_SECURE`.
`FLAG_SECURE` override is a **project-specific** requirement (not an AXP Pro
catalog feature). Track it under project-specific additions in
`docs/axp-pro-parity.md`, not as AXP parity.
## Hardening strategy
Hardening is incremental. Every significant hardening change must:
- be isolated in a comprehensible commit;
- document its upstream/reference source;
- remain revertible;
- pass boot and basic hardware tests;
- pass the compatibility regression suite before being accepted.
Preferred source order for modern framework hardening:
1. modern GrapheneOS Android 16 implementation;
2. AXP.OS / DivestOS implementation;
3. historical GrapheneOS sunfish implementation where hardware-specific.
Do not attempt to emulate hardware protections unsupported by the Pixel 4a.
Document EOL limits in `013` / hardening work: patches we can backport in-tree
versus proprietary vendor/modem firmware we cannot replace. Never blindly copy
patches; establish what problem they solve, whether Android 16 / LineageOS
already solves it, and whether they remain applicable to sunfish.
Candidate ideas live in non-normative `docs/hardening-backlog.md` until promoted
into a feature spec.
## Build targets and signing
- Development target: `userdebug`.
- Release target: `user` (when signing / AVB / release work lands).
- Early phases: development/test-keys.
- Project-owned platform/release/AVB keys: introduced in `011-signing-avb-ota`
after a stable functional ROM exists.
- Exception: any key that must remain stable early for updateability
(especially patched Phonesky signing) must be decided before distributing an
updatable first version.
`userdebug` is the default development build. Banking compatibility failures
observed on `userdebug` must **not** be considered definitive until retested on
a `user` / release-like test build where build type or test-key state may affect
application behaviour. Final production signing, AVB, and relocking still belong
to `011`, which must re-run the full Yuh + Swissquote regression on the
really signed/relocked ROM.
Experimental flashing happens **only on LAB** and requires an unlocked
bootloader on that device. LAB and GOLDEN lock/bootloader state are measured in
`002`; if LAB is locked, unlocking LAB is a prerequisite before the first `003`
flash. Never unlock/wipe/flash GOLDEN for project experiments. After the ROM is
stable:
- generate dedicated ROM signing keys;
- implement reproducible release signing;
- configure Android Verified Boot with project-owned keys;
- support factory/recovery images and OTA generation;
- test bootloader relocking with the custom trust chain.
KernelSU compiled into a project-signed kernel is compatible with the project
signing model; do not treat root presence itself as an AVB failure.
Private signing material must never be committed to Git. Public certificates
(e.g. `*.x509.pem`) may be versioned when useful for documentation/reproducibility.
Any signing key whose identity must remain stable across updates (Phonesky,
platform/release, AVB, OTA, etc.) must have an **encrypted/offline backup**
before the first build that depends on that key is distributed or adopted as a
baseline.
OTA is considered architecturally so doors stay open, but is not implemented and
does not block early builds. Real OTA/signing/AVB work belongs to
`011-signing-avb-ota`.
## Host boundary: rom-dev01 is a prerequisite
Provisioning of the Proxmox VM, public IP selection, Hetzner/vswitch details,
PVE lifecycle, and SSYSTEMS Salt `saltenv=base` bootstrap are **out of scope**
for this repository.
Assume once development starts on the host:
- `rom-dev01` exists;
- Ubuntu Server 24.04 LTS is installed;
- one reachable public IPv4 address (public NIC only; no required
`10.100.67.0/18` NIC);
- SSH administration works;
- SSYSTEMS base provisioning is complete.
This project must not manage `pm-test01`, company Salt states, or company
governance from this repository.
### Isolation posture (guest)
Goal: keep `rom-dev01` as isolated as practical from SSYSTEMS internal
infrastructure while accepting that the public vSwitch/VLAN may be shared with
other lab/gateway VMs.
Required posture from the ROM project's perspective:
- **Public NIC only** — no additional NIC on SSYSTEMS internal networks
(e.g. `10.100.67.0/18`) unless a later documented exception appears.
- **No dependency** on internal SSYSTEMS services for normal ROM work: internal
DNS, LDAP/SSO, company VPN, company GitLab, NFS/CIFS to corporate storage,
or company Zabbix as a required path.
- **Project UIs** (Gitea, Semaphore) must not need to be exposed to the whole
Internet; prefer localhost / SSH tunnel / existing tight SSH access.
- **Guest firewall** should favour default-deny inbound beyond SSH administration
and should block or avoid unnecessary traffic toward known SSYSTEMS internal
ranges once those ranges are known to the operator.
- **DNS** for day-to-day project use should not require SSYSTEMS internal
resolvers.
- Sharing the **public** VLAN/subnet with gateway VMs is acceptable and does not
by itself place this project under SSYSTEMS product governance.
- Hypervisor/storage (`pm-test01`) remain unavoidable physical hosting; they are
not a license to couple the guest into internal ops services.
Concrete public IPs, PVE bridges, and company Salt firewall templates stay
outside this repository. Feature `001` implements/verifies the guest-side
posture on the already-provisioned host.
### Capacity assumptions (do not oversize builds)
Initial host capacity to plan around:
- 6 vCPU
- 52 GB RAM
- 2 TB disk (expandable if required)
- 32 GB VM swap
Builds must support conservative parallelism and tolerate swap when required.
Do not assume additional resources are available.
### Project account
Do not hard-code laptop admin usernames as architectural requirements.
After the host exists, the project may create/use a dedicated account:
- `android-build`
for Android source trees and builds.
### Laptop vs host roles
Laptop:
- Cursor UI / Remote SSH client;
- adb / fastboot workstation;
- Pixel physically attached for flashing/testing;
- small local checkouts where useful.
`rom-dev01`:
- Android source trees, `.repo`, `out/`, ccache;
- Gitea, Semaphore, Ansible;
- build scripts, logs, artifacts under `/srv/rom/`.
Do not design the normal workflow around remote USB passthrough.
## Tooling ownership inside this project
Once `rom-dev01` exists:
- **Ansible** (+ Semaphore UI) owns ROM workflow: sync, patches, builds,
cleanup, manifest capture, artifact collection, publication.
- Semaphore must not hide undocumented build behaviour and must not be exposed
directly to the Internet; access via existing network/SSH constraints.
- Gitea is private project Git hosting on the VM; not company GitLab.
- The first LineageOS build should be performed manually before fully
automating the workflow.
Salt used by SSYSTEMS to create the VM remains outside this project. This repo
does not encode company Salt VM creation.
## Git / Gitea model
Use a private Gitea instance on `rom-dev01` with multiple repositories, not one
giant monorepo. Preserve Android/`repo` natural layout. Fork only repositories
that this project actually modifies.
Initial repositories:
- `sunfish-a16-rom` — Spec Kit, constitution, roadmap, global documentation
(this meta repository; laptop staging is temporary until Gitea exists)
- `sunfish-rom-manifest` — repo manifests
- `sunfish-rom-build` — scripts / Ansible / Semaphore / build tooling
- `sunfish-rom-patches` — patchsets / tracking
Later, only when needed: kernel fork, `frameworks/base`, Settings, Dialer,
vendor/config.
Do not commit:
- `.repo`;
- `out/`;
- ccache;
- complete upstream Android source trees;
- private signing keys (including Phonesky signing keys);
- personal phone dumps containing credentials or identifiers;
- proprietary Google APKs.
Initially a single developer. Do not design multi-developer permissions/CI until
needed.
Project-owned Gitea repositories must have an **off-VM backup or mirror**. The
build VM must not be the only copy of project history (laptop clones, Borg, or
another agreed target are acceptable).
Artifacts initially live under `/srv/rom/artifacts/` and are copied with `rsync`
to the laptop for flashing. Significant releases may later use Gitea Releases
(ROM ZIP, needed images, hashes, manifest, build metadata). Intermediate build
outputs are not published.
## Reproducibility
Every meaningful build must capture at least:
- exact manifest;
- source revisions;
- project patch revisions;
- build variant;
- relevant configuration;
- SHA-256 hashes;
- compressed build log.
Prefer small, source-attributed commits over large opaque patch dumps.
## Development stages
Progress in this order unless a documented technical dependency requires
otherwise:
1. Document the GOLDEN AXP oracle and label GOLDEN vs LAB (`002`) — essential
capture + verify Yuh/Swissquote on GOLDEN; **no** NeoBackup/wipe of GOLDEN.
2. Build and boot vanilla LineageOS 23.2 on **LAB only** (`003`).
3. Integrate microG, GSF Proxy, patched Phonesky, and restricted native
signature spoofing on LAB (`004`–`005`).
4. Record current rootless LAB banking outcomes in `007` preflight; defer `006`.
5. Integrate KernelSU-Next and verified module unmount on LAB (`007`), followed
by optional SUSFS and evaluated attestation experiments. Both banks must PASS
to accept root and the Compatibility MVP.
6. Native call recording and configurable `FLAG_SECURE` (`008`–`009`).
7. Remaining AXP Pro usability/privacy features (`010`; matrix spans `004`–`013`,
see non-normative `docs/axp-pro-parity.md`).
8. Signing, AVB, OTA + full banking regression on signed/relocked **LAB** ROM
(`011`).
9. Incremental userspace then kernel hardening (`012`–`013`; candidates in
non-normative `docs/hardening-backlog.md`).
10. Optional later: alternative KernelSU, root-disabled boot, extra
banking apps.
Before stressing LAB: document official Google factory-image **emergency rescue
for LAB** (`002`/`003`). GOLDEN remains a live AXP reference — not something we
wipe and rebuild for day-to-day bring-up. NeoBackup is optional personal
preference only, **not** a project gate.
`001-development-environment` starts **after** `rom-dev01` exists. It specifies
the ROM development environment on that host (packages, `android-build`,
repo/git, ccache, `/srv/rom`, Gitea, Semaphore, Ansible, Spec Kit, Remote SSH
workflow). It does **not** specify Proxmox VM creation.
For `005-restricted-signature-spoofing`: first verify what LineageOS 23.2 already
provides for restricted microG spoofing; extend only as needed for patched
Phonesky — do not assume a from-scratch spoofing stack.
## Testing rule
Never change several high-risk subsystems simultaneously when the result cannot
be attributed to one change.
Regression testing should include, as applicable to the stage:
boot; encryption/PIN; calls/mobile data; Wi-Fi; Bluetooth; GPS; camera; audio;
NFC; sensors; microG Self-Check; Google Play login/install/update; push;
Insular work profile; Yuh; Swissquote; biometrics; banking confirmations;
reboot; KernelSU application profiles where applicable.
## Project identity
The result is a LineageOS-based custom ROM with selected compatibility, privacy,
and hardening features. It must not be presented as official GrapheneOS,
LineageOS, AXP.OS, DivestOS, or iodéOS.
Public marketing/ROM brand naming is deferred and must not contaminate
repository, spec, or architecture names. The technical project name remains
`sunfish-a16-rom`.
## Spec Kit and documentation language
Primary technical documentation and the constitution are written in English.
Personal notes may be Spanish. Do not maintain duplicated bilingual docs.
Use a clean upstream Spec Kit. Do not copy SSYSTEMS Spec Kit constitutions,
company project-create flows, or company runtime configuration.
Create feature specs only when ready to work them. Keep a non-binding roadmap
in `specs/README.md` for later features; do not create empty stub directories for
all future numbers in advance.
## Governance
- This constitution supersedes conflicting informal notes for project rules.
- Feature requirements live in `spec.md` files; amend those rather than
overloading this document with implementation detail.
- `docs/axp-pro-parity.md` and `docs/hardening-backlog.md` are non-normative
until promoted into a feature spec.
- Amendments to this constitution require an explicit documented change and
version/date bump.
- `AGENTS.md` explains agent working rules for this repository only; it must
not duplicate this constitution.
### Amendment 0.2.0 — 2026-09-15
Owner-authorized remediation of C1/C2: defer the rootless PASS gate, retain a
measured pre-root snapshot, and assign both-bank acceptance to `007`. This
changes sequencing, not LAB-only protection or the prohibition on banking root.
### Amendment 0.2.1 — 2026-09-15
Upstream freshness for root stack and similar deps: integrate from newest
compatible stable/tagged upstream (full git history), verify version codes
against the matching manager **before** long builds; pin only after LAB works.
Triggered by stale shallow ReSukiSU mirror yielding `KSU_VERSION=30701`.
+195
View File
@@ -0,0 +1,195 @@
#!/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.
# In --paths-only mode this is pure resolution, so pass --no-persist to opt out
# of the feature.json write side effect (issue #3025).
if $PATHS_ONLY; then
_paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
else
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
fi
eval "$_paths_output"
unset _paths_output
# If paths-only mode, output paths and exit (no validation)
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
+704
View File
@@ -0,0 +1,704 @@
#!/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
}
# Resolve an explicit SPECIFY_INIT_DIR project override (the directory that
# *contains* .specify/), for non-interactive / CI use — e.g. running a Spec Kit
# command against a member project from a monorepo root without cd.
#
# Precondition: SPECIFY_INIT_DIR is non-empty. Echoes the validated absolute
# project root, or prints an error and returns 1. Strict by design: the path
# must exist and contain .specify/, with no silent fallback to cwd or the
# script-location default (which would silently write to the wrong project).
#
# This is the single resolver: bundled extensions inherit it by sourcing core
# (e.g. the git extension's create-new-feature-branch) rather than duplicating it.
resolve_specify_init_dir() {
local init_root
# Normalize: relative paths resolve against $(pwd); a trailing slash collapses.
# CDPATH="" so a relative value cannot be resolved against the caller's CDPATH
# (which would also echo to stdout and corrupt the captured path).
if ! init_root="$(CDPATH="" cd -- "$SPECIFY_INIT_DIR" 2>/dev/null && pwd)"; then
echo "ERROR: SPECIFY_INIT_DIR does not point to an existing directory: $SPECIFY_INIT_DIR" >&2
return 1
fi
if [[ ! -d "$init_root/.specify" ]]; then
echo "ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): $init_root" >&2
return 1
fi
printf '%s\n' "$init_root"
}
# Get repository root, prioritizing .specify directory
# This prevents using a parent repository when spec-kit is initialized in a subdirectory
get_repo_root() {
# Explicit project override wins (see resolve_specify_init_dir).
if [[ -n "${SPECIFY_INIT_DIR:-}" ]]; then
resolve_specify_init_dir
return
fi
# 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
# Final fallback to script location
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
(cd "$script_dir/../../.." && pwd)
}
# Get current feature name from explicit state only.
# Returns the feature identifier or empty string if none is set.
# Feature state is set by SPECIFY_FEATURE (from create-new-feature or
# the git extension) or implicitly via .specify/feature.json.
get_current_branch() {
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
echo "$SPECIFY_FEATURE"
return
fi
# No explicit feature set — caller must handle this via feature.json
# in get_feature_paths(). Return empty to signal "unknown".
echo ""
}
# Safely read .specify/feature.json's "feature_directory" value.
# Prints the raw value (possibly relative) to stdout, or empty string if the file
# is missing, unparseable, or does not contain the key. Always returns 0 so callers
# under `set -e` cannot be aborted by parser failure.
# Parser order mirrors the historical get_feature_paths behavior: jq -> python3 -> grep/sed.
read_feature_json_feature_directory() {
local repo_root="$1"
local fj="$repo_root/.specify/feature.json"
[[ -f "$fj" ]] || { printf '%s' ''; return 0; }
# Try parsers in order (jq -> python3 -> grep/sed), falling through on
# failure. Selection is by *parse success*, not mere availability: on
# Windows `python3` commonly resolves to the Microsoft Store App Execution
# Alias stub, which passes `command -v` but fails at runtime (exit 49), so
# an availability-gated `elif` would pick python3, swallow its failure, and
# never reach the grep/sed fallback -- leaving feature.json unreadable even
# though it is valid (issue #3304).
local _fd=''
if command -v jq >/dev/null 2>&1; then
if ! _fd=$(jq -r '.feature_directory // empty' "$fj" 2>/dev/null); then
_fd=''
fi
fi
if [[ -z "$_fd" ]] && command -v python3 >/dev/null 2>&1; then
# Use Python so pretty-printed/multi-line JSON still parses correctly.
if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then
_fd=''
fi
fi
if [[ -z "$_fd" ]]; then
# Last-resort single-line grep/sed fallback. The `|| true` guards against
# grep returning 1 (no match) aborting under `set -e` / `pipefail`.
_fd=$( { grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } \
| head -n 1 \
| sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
fi
printf '%s' "$_fd"
return 0
}
# Persist a feature_directory value to .specify/feature.json.
# Writes only when the file is missing or the value differs from what's stored.
# Accepts the raw (possibly relative) path — callers should pass the original
# user-supplied value, not the normalized absolute path.
_persist_feature_json() {
local repo_root="$1"
local feature_dir_value="$2"
local fj="$repo_root/.specify/feature.json"
# Strip repo_root prefix if the value is absolute and under repo_root
if [[ "$feature_dir_value" == "$repo_root/"* ]]; then
feature_dir_value="${feature_dir_value#"$repo_root/"}"
fi
# Read current value (if any) and skip write when unchanged
local current_val
current_val=$(read_feature_json_feature_directory "$repo_root")
if [[ "$current_val" == "$feature_dir_value" ]]; then
return 0
fi
# Ensure .specify/ directory exists
mkdir -p "$repo_root/.specify"
# Write feature.json — prefer jq for safe JSON, fall back to printf
if command -v jq >/dev/null 2>&1; then
jq -cn --arg fd "$feature_dir_value" '{feature_directory:$fd}' > "$fj"
else
printf '{"feature_directory":"%s"}\n' "$(json_escape "$feature_dir_value")" > "$fj"
fi
}
get_feature_paths() {
# Read-only callers (e.g. check-prerequisites.sh --paths-only) pass
# --no-persist so pure path resolution never writes .specify/feature.json,
# which would dirty the working tree or overwrite a pinned value (issue #3025).
local no_persist=false
if [[ "${1:-}" == "--no-persist" ]]; then
no_persist=true
shift
fi
# Split decl/assignment so a SPECIFY_INIT_DIR validation failure in
# get_repo_root propagates as a hard error instead of being masked by `local`.
local repo_root
repo_root=$(get_repo_root) || return 1
local current_branch
current_branch=$(get_current_branch)
# Resolve feature directory. Priority:
# 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
# 2. .specify/feature.json "feature_directory" key (persisted by specify command)
# 3. Error — no feature context available
local feature_dir
if [[ -n "${SPECIFY_FEATURE_DIRECTORY:-}" ]]; then
feature_dir="$SPECIFY_FEATURE_DIRECTORY"
# Normalize relative paths to absolute under repo root
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
# Persist to feature.json so future sessions without the env var still
# work — unless the caller opted out for read-only resolution (#3025).
if [[ "$no_persist" != true ]]; then
_persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
fi
elif [[ -f "$repo_root/.specify/feature.json" ]]; then
local _fd
_fd=$(read_feature_json_feature_directory "$repo_root")
if [[ -n "$_fd" ]]; then
feature_dir="$_fd"
# Normalize relative paths to absolute under repo root
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
else
echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or ensure .specify/feature.json contains feature_directory." >&2
return 1
fi
else
echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or run the specify command to create .specify/feature.json." >&2
return 1
fi
# When no branch context exists (no SPECIFY_FEATURE, feature resolved via
# SPECIFY_FEATURE_DIRECTORY or feature.json), fall back to the feature
# directory basename so CURRENT_BRANCH is a usable identifier rather than
# an empty, misleading value (issue #3026).
if [[ -z "$current_branch" ]]; then
local feature_dir_trimmed="${feature_dir%/}"
current_branch="${feature_dir_trimmed##*/}"
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 '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
}
get_invoke_separator() {
local repo_root="${1:-$(get_repo_root)}"
if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
printf '%s\n' "$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
return 0
fi
local integration_json="$repo_root/.specify/integration.json"
local separator="."
local parsed=0
if [[ -f "$integration_json" ]]; then
# Try parsers in order (jq -> python3 -> awk), falling through on
# failure. Selection is by *parse success*, not mere availability: on
# Windows `python3` commonly resolves to the Microsoft Store App
# Execution Alias stub, which passes `command -v` but fails at runtime
# (exit 49). An availability-gated branch would pick python3, swallow
# its failure, and — because this function historically had no text
# fallback — silently return "." even for `-`-separator integrations
# (e.g. forge, cline), yielding wrong command hints (issue #3304).
if command -v jq >/dev/null 2>&1; then
local jq_separator
if jq_separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then
case "$jq_separator" in
"."|"-") separator="$jq_separator"; parsed=1 ;;
esac
fi
fi
if [[ "$parsed" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then
local py_separator
if py_separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null
import json
import sys
try:
with open(sys.argv[1], encoding="utf-8") as fh:
state = json.load(fh)
key = state.get("default_integration") or state.get("integration") or ""
settings = state.get("integration_settings")
separator = "."
if isinstance(key, str) and isinstance(settings, dict):
entry = settings.get(key)
if isinstance(entry, dict) and entry.get("invoke_separator") in {".", "-"}:
separator = entry["invoke_separator"]
print(separator)
except Exception:
sys.exit(1)
PY
); then
case "$py_separator" in
"."|"-") separator="$py_separator"; parsed=1 ;;
esac
fi
fi
if [[ "$parsed" -eq 0 ]]; then
# Last-resort text fallback for environments with neither jq nor a
# working python3 (e.g. stock Windows + Git Bash). Reads the active
# integration key (default_integration, else integration) and its
# invoke_separator from within the integration_settings object.
# Handles both pretty-printed (the written form) and compact JSON.
# Accumulate all lines into one buffer in END rather than using
# gawk-only whole-file slurp (RS="^$"), so this stays portable to
# the BSD awk on macOS.
local awk_separator
awk_separator=$(awk '
function keyval(d, name, v) {
if (match(d, "\"" name "\"[ \t\r\n]*:[ \t\r\n]*\"[^\"]*\"")) {
v=substr(d,RSTART,RLENGTH); sub(/^.*:[ \t\r\n]*"/,"",v); sub(/"$/,"",v); return v
}
return ""
}
{ doc = doc $0 "\n" }
END {
key=keyval(doc,"default_integration"); if (key=="") key=keyval(doc,"integration")
sep="."
if (key!="") {
settings=doc
if (match(doc, /"integration_settings"[ \t\r\n]*:[ \t\r\n]*[{]/)) {
settings=substr(doc, RSTART+RLENGTH-1)
}
if (match(settings, "\"" key "\"[ \t\r\n]*:[ \t\r\n]*[{]")) {
start=RSTART+RLENGTH-1
depth=0
obj=""
for (i=start; i<=length(settings); i++) {
c=substr(settings,i,1)
obj=obj c
if (c=="{") depth++
else if (c=="}") { depth--; if (depth==0) break }
}
if (match(obj, /"invoke_separator"[ \t\r\n]*:[ \t\r\n]*"[-.]"/)) {
tok=substr(obj,RSTART,RLENGTH); s=substr(tok,length(tok)-1,1)
if (s=="." || s=="-") sep=s
}
}
}
print sep
}
' "$integration_json" 2>/dev/null)
case "$awk_separator" in
"."|"-") separator="$awk_separator" ;;
esac
fi
fi
_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
printf '%s\n' "$separator"
}
format_speckit_command() {
local command_name="$1"
local repo_root="${2:-$(get_repo_root)}"
local separator
if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
separator="$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
else
separator=$(get_invoke_separator "$repo_root")
_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
fi
command_name="${command_name#/}"
command_name="${command_name#speckit.}"
command_name="${command_name#speckit-}"
command_name="${command_name//./$separator}"
printf '/speckit%s%s\n' "$separator" "$command_name"
}
# 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) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
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
}
# Resolve a template name to composed content using composition strategies.
# Reads strategy metadata from preset manifests and composes content
# from multiple layers using prepend, append, or wrap strategies.
#
# Usage: CONTENT=$(resolve_template_content "template-name" "$REPO_ROOT")
# Returns composed content string on stdout; exit code 1 if not found.
resolve_template_content() {
local template_name="$1"
local repo_root="$2"
local base="$repo_root/.specify/templates"
# Collect all layers (highest priority first)
local -a layer_paths=()
local -a layer_strategies=()
# Priority 1: Project overrides (always "replace")
local override="$base/overrides/${template_name}.md"
if [ -f "$override" ]; then
layer_paths+=("$override")
layer_strategies+=("replace")
fi
# 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"
local sorted_presets=""
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
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) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null); then
if [ -n "$sorted_presets" ]; then
local yaml_warned=false
while IFS= read -r preset_id; do
# Read strategy and file path from preset manifest
local strategy="replace"
local manifest_file=""
local manifest="$presets_dir/$preset_id/preset.yml"
if [ -f "$manifest" ] && command -v python3 >/dev/null 2>&1; then
# Requires PyYAML; falls back to replace/convention if unavailable
local result
local py_stderr
py_stderr=$(mktemp)
result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" python3 -c "
import sys, os
try:
import yaml
except ImportError:
print('yaml_missing', file=sys.stderr)
print('replace\t')
sys.exit(0)
try:
with open(os.environ['SPECKIT_MANIFEST']) as f:
data = yaml.safe_load(f)
for t in data.get('provides', {}).get('templates', []):
if t.get('name') == os.environ['SPECKIT_TMPL'] and t.get('type', 'template') == 'template':
print(t.get('strategy', 'replace') + '\t' + t.get('file', ''))
sys.exit(0)
print('replace\t')
except Exception:
print('replace\t')
" 2>"$py_stderr")
local parse_status=$?
if [ $parse_status -eq 0 ] && [ -n "$result" ]; then
IFS=$'\t' read -r strategy manifest_file <<< "$result"
strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
fi
if [ "$yaml_warned" = false ] && grep -q 'yaml_missing' "$py_stderr" 2>/dev/null; then
echo "Warning: PyYAML not available; composition strategies may be ignored" >&2
yaml_warned=true
fi
rm -f "$py_stderr"
fi
# Try manifest file path first, then convention path
local candidate=""
if [ -n "$manifest_file" ]; then
# Reject absolute paths and parent traversal
case "$manifest_file" in
/*|*../*|../*) manifest_file="" ;;
esac
fi
if [ -n "$manifest_file" ]; then
local mf="$presets_dir/$preset_id/$manifest_file"
[ -f "$mf" ] && candidate="$mf"
fi
if [ -z "$candidate" ]; then
local cf="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$cf" ] && candidate="$cf"
fi
if [ -n "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("$strategy")
fi
done <<< "$sorted_presets"
fi
else
# python3 failed — fall back to unordered directory scan (replace only)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
else
# No python3 or registry — fall back to unordered directory scan (replace only)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
fi
# Priority 3: Extension-provided templates (always "replace")
local ext_dir="$repo_root/.specify/extensions"
if [ -d "$ext_dir" ]; then
for ext in "$ext_dir"/*/; do
[ -d "$ext" ] || continue
case "$(basename "$ext")" in .*) continue;; esac
local candidate="$ext/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
# Priority 4: Core templates (always "replace")
local core="$base/${template_name}.md"
if [ -f "$core" ]; then
layer_paths+=("$core")
layer_strategies+=("replace")
fi
local count=${#layer_paths[@]}
[ "$count" -eq 0 ] && return 1
# Check if any layer uses a non-replace strategy
local has_composition=false
for s in "${layer_strategies[@]}"; do
[ "$s" != "replace" ] && has_composition=true && break
done
# If the top (highest-priority) layer is replace, it wins entirely —
# lower layers are irrelevant regardless of their strategies.
if [ "${layer_strategies[0]}" = "replace" ]; then
cat "${layer_paths[0]}"
return 0
fi
if [ "$has_composition" = false ]; then
cat "${layer_paths[0]}"
return 0
fi
# Find the effective base: scan from highest priority (index 0) downward
# to find the nearest replace layer. Only compose layers above that base.
local base_idx=-1
local i
for (( i=0; i<count; i++ )); do
if [ "${layer_strategies[$i]}" = "replace" ]; then
base_idx=$i
break
fi
done
if [ $base_idx -lt 0 ]; then
return 1 # no base layer found
fi
# Read the base content; compose layers above the base (higher priority)
local content
content=$(cat "${layer_paths[$base_idx]}"; printf x)
content="${content%x}"
for (( i=base_idx-1; i>=0; i-- )); do
local path="${layer_paths[$i]}"
local strat="${layer_strategies[$i]}"
local layer_content
# Preserve trailing newlines
layer_content=$(cat "$path"; printf x)
layer_content="${layer_content%x}"
case "$strat" in
replace) content="$layer_content" ;;
prepend) content="$(printf '%s\n\n%s' "$layer_content" "$content")" ;;
append) content="$(printf '%s\n\n%s' "$content" "$layer_content")" ;;
wrap)
case "$layer_content" in
*'{CORE_TEMPLATE}'*) ;;
*) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 1 ;;
esac
while [[ "$layer_content" == *'{CORE_TEMPLATE}'* ]]; do
local before="${layer_content%%\{CORE_TEMPLATE\}*}"
local after="${layer_content#*\{CORE_TEMPLATE\}}"
layer_content="${before}${content}${after}"
done
content="$layer_content"
;;
*) echo "Error: unknown strategy '$strat'" >&2; return 1 ;;
esac
done
printf '%s' "$content"
return 0
}
+392
View File
@@ -0,0 +1,392 @@
#!/usr/bin/env bash
set -e
JSON_MODE=false
DRY_RUN=false
ALLOW_EXISTING=false
SHORT_NAME=""
BRANCH_NUMBER=""
USE_TIMESTAMP=false
NUMBER_EXPLICIT=false
ARGS=()
i=1
while [ $i -le $# ]; do
arg="${!i}"
case "$arg" in
--json)
JSON_MODE=true
;;
--dry-run)
DRY_RUN=true
;;
--allow-existing-branch)
ALLOW_EXISTING=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"
if [ -n "$BRANCH_NUMBER" ]; then
NUMBER_EXPLICIT=true
fi
;;
--timestamp)
USE_TIMESTAMP=true
;;
--help|-h)
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
echo ""
echo "Options:"
echo " --json Output in JSON format"
echo " --dry-run Compute feature name and paths without creating directories or files"
echo " --allow-existing-branch Reuse an existing feature directory if it already exists"
echo " --short-name <name> Provide a custom short name (2-4 words) for the feature"
echo " --number N Prefer a feature number (auto-corrected if its specs prefix exists)"
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] [--dry-run] [--allow-existing-branch] [--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" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
exit 1
fi
MAX_FEATURE_NUMBER=9223372036854775807
MAX_BRANCH_LENGTH=244
is_feature_number_in_range() {
local value="$1"
local normalized="${value#"${value%%[!0]*}"}"
[ -n "$normalized" ] || normalized=0
[ ${#normalized} -lt ${#MAX_FEATURE_NUMBER} ] && return 0
[ ${#normalized} -gt ${#MAX_FEATURE_NUMBER} ] && return 1
# Equal-length digit strings must be compared without arithmetic overflow.
# shellcheck disable=SC2071
[[ "$normalized" < "$MAX_FEATURE_NUMBER" || "$normalized" == "$MAX_FEATURE_NUMBER" ]]
}
# 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")
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
number=$(echo "$dirname" | grep -Eo '^[0-9]+')
if is_feature_number_in_range "$number"; then
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
fi
fi
done
fi
echo "$highest"
}
# Return success when a spec directory owns the given numeric prefix.
spec_prefix_exists() {
local specs_dir="$1"
local feature_num="$2"
for spec_path in "$specs_dir/${feature_num}-"*; do
[ -d "$spec_path" ] && return 0
done
return 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/-$//'
}
# Fit a feature prefix and suffix within GitHub's branch-name limit.
fit_branch_name() {
local feature_num="$1"
local branch_suffix="$2"
local branch_name="${feature_num}-${branch_suffix}"
if [ ${#branch_name} -gt $MAX_BRANCH_LENGTH ]; then
local prefix_length=$(( ${#feature_num} + 1 ))
local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length))
local truncated_suffix
truncated_suffix=$(printf '%s' "$branch_suffix" | cut -c "1-$max_suffix_length" | sed 's/-$//')
branch_name="${feature_num}-${truncated_suffix}"
fi
printf '%s' "$branch_name"
}
# Quote a value for POSIX shell reuse, byte-identical to Python's shlex.quote
# so the persistence hints match the Python variant exactly (printf %q output
# differs between bash versions and from shlex.quote for spaces/metachars).
shell_quote() {
local value="$1" LC_ALL=C
if [[ "$value" =~ ^[A-Za-z0-9_@%+=:,./-]+$ ]]; then
printf '%s' "$value"
else
local q="'\"'\"'"
printf "'%s'" "${value//\'/$q}"
fi
}
# Resolve repository root using common.sh functions which prioritize .specify
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
REPO_ROOT=$(get_repo_root) || exit 1
cd "$REPO_ROOT"
SPECS_DIR="$REPO_ROOT/specs"
if [ "$DRY_RUN" != true ]; then
mkdir -p "$SPECS_DIR"
fi
# 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=$(printf '%s' "$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")
# Keep short words that appear as an uppercase acronym in the original.
# Uppercase via tr and match with grep -w (both portable) rather than
# bash's 4+ "^^" case expansion (breaks on macOS bash 3.2) and \b (non-POSIX).
elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then
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
>&2 echo "[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
if [ -n "$BRANCH_NUMBER" ] && [[ ! "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then
echo "Error: --number must be an unsigned integer, got '$BRANCH_NUMBER'" >&2
exit 1
fi
# Bash arithmetic is signed 64-bit; reject digit strings that would wrap.
if [ -n "$BRANCH_NUMBER" ] && ! is_feature_number_in_range "$BRANCH_NUMBER"; then
echo "Error: --number must be between 0 and $MAX_FEATURE_NUMBER, got '$BRANCH_NUMBER'" >&2
exit 1
fi
# Determine branch number from existing feature directories
if [ -z "$BRANCH_NUMBER" ]; then
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
if [ "$HIGHEST" -eq "$MAX_FEATURE_NUMBER" ]; then
echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
exit 1
fi
BRANCH_NUMBER=$((HIGHEST + 1))
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))")
# Treat an explicit number as a preference when its prefix is already used
# by a feature directory. Auto-detected numbers are already conflict-free.
if [ "$NUMBER_EXPLICIT" = true ]; then
SPEC_CONFLICT=false
REQUESTED_BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
REQUESTED_DIR="$SPECS_DIR/$REQUESTED_BRANCH_NAME"
if [ "$ALLOW_EXISTING" != true ] || [ ! -d "$REQUESTED_DIR" ]; then
spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" && SPEC_CONFLICT=true
fi
if [ "$SPEC_CONFLICT" = true ]; then
REQUESTED_NUM="$FEATURE_NUM"
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
BRANCH_NUMBER=$HIGHEST
while true; do
if [ "$BRANCH_NUMBER" -eq "$MAX_FEATURE_NUMBER" ]; then
echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
exit 1
fi
BRANCH_NUMBER=$((BRANCH_NUMBER + 1))
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" || break
done
>&2 echo "[specify] Warning: --number $REQUESTED_NUM conflicts with an existing spec directory; using $FEATURE_NUM instead"
fi
fi
fi
# GitHub enforces a 244-byte limit on branch names
# Validate and truncate if necessary
ORIGINAL_BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
if [ "$BRANCH_NAME" != "$ORIGINAL_BRANCH_NAME" ]; then
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
fi
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
SPEC_FILE="$FEATURE_DIR/spec.md"
if [ "$DRY_RUN" != true ]; then
if [ -d "$FEATURE_DIR" ] && [ "$ALLOW_EXISTING" != true ]; then
if [ "$USE_TIMESTAMP" = true ]; then
>&2 echo "Error: Feature directory '$FEATURE_DIR' already exists. Rerun to get a new timestamp or use a different --short-name."
else
>&2 echo "Error: Feature directory '$FEATURE_DIR' already exists. Please use a different feature name or specify a different number with --number."
fi
exit 1
fi
mkdir -p "$FEATURE_DIR"
if [ ! -f "$SPEC_FILE" ]; then
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
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
fi
# Persist to .specify/feature.json so downstream commands can find the feature
_persist_feature_json "$REPO_ROOT" "$FEATURE_DIR"
# Inform the user how to set feature state in their own shell
printf '# To persist: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" >&2
printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")" >&2
fi
if $JSON_MODE; then
if command -v jq >/dev/null 2>&1; then
if [ "$DRY_RUN" = true ]; 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,DRY_RUN:true}'
else
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}'
fi
else
if [ "$DRY_RUN" = true ]; then
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$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
fi
else
echo "BRANCH_NAME: $BRANCH_NAME"
echo "SPEC_FILE: $SPEC_FILE"
echo "FEATURE_NUM: $FEATURE_NUM"
if [ "$DRY_RUN" != true ]; then
printf '# To persist in your shell: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")"
printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")"
fi
fi
+83
View File
@@ -0,0 +1,83 @@
#!/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
# Ensure the feature directory exists
mkdir -p "$FEATURE_DIR"
# Copy plan template if plan doesn't already exist
if [[ -f "$IMPL_PLAN" ]]; then
if $JSON_MODE; then
echo "Plan already exists at $IMPL_PLAN, skipping template copy" >&2
else
echo "Plan already exists at $IMPL_PLAN, skipping template copy"
fi
else
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
cp "$TEMPLATE" "$IMPL_PLAN"
if $JSON_MODE; then
echo "Copied plan template to $IMPL_PLAN" >&2
else
echo "Copied plan template to $IMPL_PLAN"
fi
else
if $JSON_MODE; then
echo "Warning: Plan template not found" >&2
else
echo "Warning: Plan template not found"
fi
# Create a basic plan file if template doesn't exist
touch "$IMPL_PLAN"
fi
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" \
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch}'
else
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")"
fi
else
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"
echo "SPECS_DIR: $FEATURE_DIR"
echo "BRANCH: $CURRENT_BRANCH"
fi
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -e
# Parse command line arguments
JSON_MODE=false
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
;;
*) echo "ERROR: Unknown option '$arg'" >&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
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
eval "$_paths_output"
unset _paths_output
# Validate required files
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
if [[ ! -f "$FEATURE_SPEC" ]]; then
echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
echo "Run /speckit-specify first to create the feature structure." >&2
exit 1
fi
# Build available docs list
docs=()
[[ -f "$RESEARCH" ]] && docs+=("research.md")
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
docs+=("contracts/")
fi
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
# Resolve tasks template through override stack
TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true
if [[ -z "$TASKS_TEMPLATE" ]] || [[ ! -f "$TASKS_TEMPLATE" ]]; then
echo "ERROR: Could not resolve required tasks-template from the template override stack for $REPO_ROOT" >&2
echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .specify/templates/overrides/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the core .specify/templates/tasks-template.md template." >&2
exit 1
fi
# Output results
if $JSON_MODE; then
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" \
--arg tasks_template "${TASKS_TEMPLATE:-}" \
'{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TASKS_TEMPLATE:$tasks_template}'
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,"TASKS_TEMPLATE":"%s"}\n' \
"$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "${TASKS_TEMPLATE:-}")"
fi
else
echo "FEATURE_DIR: $FEATURE_DIR"
echo "TASKS_TEMPLATE: ${TASKS_TEMPLATE:-not found}"
echo "AVAILABLE_DOCS:"
check_file "$RESEARCH" "research.md"
check_file "$DATA_MODEL" "data-model.md"
check_dir "$CONTRACTS_DIR" "contracts/"
check_file "$QUICKSTART" "quickstart.md"
fi
+40
View File
@@ -0,0 +1,40 @@
# [CHECKLIST TYPE] Checklist: [FEATURE NAME]
**Purpose**: [Brief description of what this checklist covers]
**Created**: [DATE]
**Feature**: [Link to spec.md or relevant documentation]
**Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements.
<!--
============================================================================
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
The /speckit-checklist command MUST replace these with actual items based on:
- User's specific checklist request
- Feature requirements from spec.md
- Technical context from plan.md
- Implementation details from tasks.md
DO NOT keep these sample items in the generated checklist file.
============================================================================
-->
## [Category 1]
- [ ] CHK001 First checklist item with clear action
- [ ] CHK002 Second checklist item
- [ ] CHK003 Third checklist item
## [Category 2]
- [ ] CHK004 Another category item
- [ ] CHK005 Item with specific criteria
- [ ] CHK006 Final item in this category
## Notes
- Check items off as completed: `[x]`
- Add comments or findings inline
- Link to relevant resources or documentation
- Items are numbered sequentially for easy reference
@@ -0,0 +1,50 @@
# [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 -->
+113
View File
@@ -0,0 +1,113 @@
# 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; its definition describes 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.*
[Gates determined based on constitution file]
## 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] |
+131
View File
@@ -0,0 +1,131 @@
# 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]
### 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]?
## 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]
### 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"]
+252
View File
@@ -0,0 +1,252 @@
---
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**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
**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
---
## 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
**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]
### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] 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
### 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
**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]
### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
- [ ] 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
### 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)
**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]
### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
- [ ] 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
### 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
**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
---
## 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
+78
View File
@@ -0,0 +1,78 @@
schema_version: "1.0"
workflow:
id: "speckit"
name: "Full SDD Cycle"
version: "1.0.0"
author: "GitHub"
description: "Runs specify → plan → tasks → implement with review gates"
requires:
# 0.8.5 is the first release with engine-side resolution of the
# ``integration: "auto"`` default. Older versions would treat "auto"
# as a literal integration key and fail at dispatch.
speckit_version: ">=0.8.5"
integrations:
# The four commands below (specify, plan, tasks, implement) are core
# spec-kit commands provided by every integration. The list here is an
# advisory, non-exhaustive compatibility hint following the documented
# ``any: [...]`` schema -- it is NOT a closed set. The workflow runs
# against any integration the project was initialized with, including
# ones not listed below, as long as that integration provides the four
# core commands referenced in ``steps``.
any:
- "alquimia"
- "claude"
- "copilot"
- "gemini"
- "opencode"
inputs:
spec:
type: string
required: true
prompt: "Describe what you want to build"
integration:
type: string
default: "auto"
prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)"
scope:
type: string
default: "full"
enum: ["full", "backend-only", "frontend-only"]
steps:
- id: specify
command: speckit.specify
integration: "{{ inputs.integration }}"
input:
args: "{{ inputs.spec }}"
- id: review-spec
type: gate
message: "Review the generated spec before planning."
options: [approve, reject]
on_reject: abort
- id: plan
command: speckit.plan
integration: "{{ inputs.integration }}"
input:
args: "{{ inputs.spec }}"
- id: review-plan
type: gate
message: "Review the plan before generating tasks."
options: [approve, reject]
on_reject: abort
- id: tasks
command: speckit.tasks
integration: "{{ inputs.integration }}"
input:
args: "{{ inputs.spec }}"
- id: implement
command: speckit.implement
integration: "{{ inputs.integration }}"
input:
args: "{{ inputs.spec }}"
+13
View File
@@ -0,0 +1,13 @@
{
"schema_version": "1.0",
"workflows": {
"speckit": {
"name": "Full SDD Cycle",
"version": "1.0.0",
"description": "Runs specify \u2192 plan \u2192 tasks \u2192 implement with review gates",
"source": "bundled",
"installed_at": "2026-08-09T08:59:06.497248+00:00",
"updated_at": "2026-08-09T08:59:06.497259+00:00"
}
}
}
+58
View File
@@ -0,0 +1,58 @@
# AGENTS.md — AXP_Resurection
How agents must work in **this** repository (AXP Magisk resurrection track).
Global project rules live in `.specify/memory/constitution.md`. Per-feature
requirements live in `specs/*/spec.md`. Do not duplicate those documents here.
Sibling standby repo/workspace `sunfish-a16-rom` keeps the KernelSU /
split-roadmap track; do not merge those ship intents back into this tree
unless the owner explicitly asks.
## Scope
- This project is **independent of SSYSTEMS**.
- Do **not** apply SSYSTEMS `AGENTS.md`, Redmine, `r{ticket}_*` branches, company
GitLab/MR workflows, or company Spec Kit conventions.
- Do **not** modify PVE / `pm-test01` / company Salt infrastructure from this
repository. `rom-dev01` is assumed to already exist when host work begins.
## Sources of truth
1. **Constitution** (`.specify/memory/constitution.md`) — global project rules
for this Magisk-resurrection track.
2. **Feature `spec.md`** — especially `specs/009-axp-los23-port/spec.md`.
3. Upstream / primary sources for LineageOS/AOSP, GrapheneOS, AXP.OS, DivestOS,
Magisk, microG, and related projects — prefer these over secondary blogs
or guesses. KernelSU docs are reference-only here (optional recycle).
## Working rules
- Do not invent unresolved technical decisions; investigate them in the relevant
feature spec (or create/clarify that spec first).
- `docs/axp-pro-parity.md` and `docs/hardening-backlog.md` are non-normative
until a row is promoted into a feature `spec.md`.
- Prefer small, attributable, reversible changes.
- Never commit secrets, private keys, proprietary Google APKs, phone dumps with
credentials/identifiers, `.repo`, `out/`, or ccache.
- Large Android trees and builds belong on `rom-dev01`, not on the laptop.
- On `rom-dev01`, never leave long `mka`/`ninja` jobs attached to laptop SSH;
start them detached (`/srv/rom/artifacts/bin/rom-detach`, prefers **tmux**) and
only poll status/logs with short connections.
- **Upstream freshness (mandatory):** for KernelSU/ReSukiSU, managers, modules,
toolchains helpers, and similar moving deps, always take the **newest stable
(or newest tagged pre-release the project currently ships)** that still fits
sunfish / non-GKI 4.14 / LOS 23.2 — **not** a stale laptop cache, shallow
clone, or “whatever was already on the host”. Before any long kernel/ROM
build: `git fetch --tags`, confirm non-shallow history when version codes
depend on `rev-list`, record tag/commit + version code, and ensure manager
APK and kernel driver versions match. Lesson from `007`: a depth=1 ReSukiSU
mirror produced bogus `KSU_VERSION=30701` after a full boot rebuild.
- Keep commits and patches source-attributed; do not dump opaque megapatches.
## Spec Kit workflow
Typical order: `/speckit-specify` → `/speckit-plan` → `/speckit-tasks` →
implement. Regenerate plan/tasks after meaningful `spec.md` edits.
- Do **not** use upstream `/speckit-taskstoissues` for this project; it is
GitHub-specific. Gitea issue integration may be added separately later.
+30
View File
@@ -0,0 +1,30 @@
# AXP_Resurection
Personal **AXP.OS resurrection** on LineageOS 23.2 / Android 16 for Google
Pixel 4a (`sunfish`): Magisk like GOLDEN, full AXP capability set, Spec Kit
feature `009-axp-los23-port`.
This tree is a **parallel product track** to `sunfish-a16-rom` (KernelSU /
split-roadmap standby). Same Spec Kit / docs home pattern; **not** a
SSYSTEMS / buildout-saar project.
## Paths
- Laptop workspace: `/home/aventura/Workspaces/AXP_Resurection`
- Sibling standby (KSU track): `/home/aventura/Workspaces/sunfish-a16-rom`
- Gitea (rom-dev01): `https://a16.ssystems.de/` — repo
`sunfish-admin/AXP-Resurection` (private)
- Builds stay on `rom-dev01`; laptop = Cursor + adb/fastboot
## Active feature
- Spec: `specs/009-axp-los23-port/spec.md`
- Plan: `specs/009-axp-los23-port/plan.md`
## Quick links
- Constitution: `.specify/memory/constitution.md`
- Agent rules: `AGENTS.md`
- Specs: `specs/README.md`
- Device roles (GOLDEN vs LAB): `docs/device-roles.md`
- AXP Pro parity matrix (non-normative): `docs/axp-pro-parity.md`
+56
View File
@@ -0,0 +1,56 @@
# Specification Analysis Report — 001-development-environment (re-run)
**Date**: 2026-08-09
**Artifacts**: spec.md, plan.md (I1/I2 synced), tasks.md (T001–T048)
**Constitution**: 0.1.5
**Mode**: Read-only analysis
## Findings
| ID | Category | Severity | Location(s) | Summary | Recommendation |
|----|----------|----------|-------------|---------|----------------|
| A1 | Ambiguity | LOW | tasks T037 | Semaphore↔canonical repo transport left to implement | Keep; document in runbook at install time |
| C1 | Coverage | LOW | tasks T024 | Laptop `adb`/`fastboot --version` without touching Pixel | Keep; useful non-blocking smoke |
**Resolved since prior analyze:** I1 (SSH vs `sudo -iu` smoke), I2 (P1 list includes pubkey + verify login).
No CRITICAL or HIGH findings. No constitution MUST violations. No 002/003 / PVE / LOS build / Pixel leakage.
## Coverage Summary
| Requirement Key | Has Task? | Task IDs |
|-----------------|-----------|----------|
| FR-001 … FR-010 | Yes | (unchanged; 100%) |
| SC-001 … SC-007 | Yes | (unchanged; 100%) |
## Constitution Alignment Issues
None.
## Unmapped Tasks
None material (setup / preflight / polish remain support tasks).
## Metrics
| Metric | Value |
|--------|-------|
| Total Requirements (FR+SC) | 17 |
| Total Tasks | 48 |
| Coverage % | **100%** |
| Ambiguity Count | 1 (LOW, intentional) |
| Duplication Count | 0 |
| Critical Issues Count | **0** |
| High Issues Count | **0** |
## Next Actions
**STOP — freeze documentary cycle for `001`.**
- Do **not** regenerate tasks or re-plan `002`.
- Do **not** `/speckit-implement` until `rom-dev01` has working SSH.
- When VM exists: implement T001–T048 in order (P1 then P2).
## Remediation
No further remediation recommended before freeze.
+186
View File
@@ -0,0 +1,186 @@
# 001 host runbook — `rom-dev01`
Living notes for feature `001-development-environment`.
## Access (laptop → host)
| Item | Value | Notes |
|------|-------|-------|
| Project name | `rom-dev01` | Guest hostname set 2026-09-13 |
| DNS / IP | **`a16.ssystems.de`** (preferred) / `a15.ssystems.de` → `49.13.104.83` | |
| SSH port | **37037** | `:22` closed |
| Admin | `root` (**default**) | `ssh a16` / `a16.ssystems.de` / `a16-root` |
| Build user | `android-build` (uid/gid **1000**) | `ssh a16-build` (also `rom-dev01`, `a15`) |
| Laptop private key | `~/.ssh/id_ed25519_rom_dev01` | also YubiKey PIV in `authorized_keys` |
| Client SSH | `docs/ssh/config` | Include from `~/.ssh/config` |
| Web UI | `https://a16.ssystems.de/` | nginx + wildcard + HTTP Basic |
### OpenSSH / Cursor
```sshconfig
Host a16 a16.ssystems.de a16-root
HostName 49.13.104.83
User root
Port 37037
IdentityFile ~/.ssh/id_ed25519_rom_dev01
IdentitiesOnly yes
Host a16-build
HostName 49.13.104.83
User android-build
Port 37037
IdentityFile ~/.ssh/id_ed25519_rom_dev01
IdentitiesOnly yes
```
Verify:
```bash
ssh a16 'whoami; hostname' # root @ rom-dev01
ssh a16-build 'whoami' # android-build
```
### SSH password auth
Effective `sshd -T`: **`passwordauthentication no`** (Ubuntu
`/etc/ssh/sshd_config.d/60-cloudimg-settings.conf` overrides
`PasswordAuthentication yes` in main `sshd_config`). Pubkey-only in practice.
Do **not** edit Salt-/cloud-managed sshd unless required; current effective
policy already matches the desired end state. YubiKey PIN ≠ SSH password.
## Coexistence with `saltenv=base`
If a Salt-/base-managed file conflicts with project needs: **stop and report**.
Do not silently replace Salt-managed settings.
Observed: `salt-minion` **inactive**; many company pubkeys already in
`/root/.ssh/authorized_keys` (base provision). Project added:
`id_ed25519_rom_dev01` + YubiKey PIV for `root` and `android-build`.
Rollback SSH key: remove the ed25519 line from `authorized_keys` on host;
delete laptop `~/.ssh/id_ed25519_rom_dev01` if retiring.
## Account model
| Account | Role |
|---------|------|
| `root` | packages, users, services, firewall observation |
| `android-build` | all Android builds, `/srv/rom` writes, Spec Kit meta tree |
| future Gitea/Semaphore | least privilege; **never** build Android as root |
## Paths
| Path | Purpose | Mode (approx) |
|------|---------|----------------|
| `/home/android-build/sunfish-a16-rom` | Meta Spec Kit tree | user-owned |
| `/srv/rom/src` | Android source trees | `android-build` |
| `/srv/rom/references` | Reference archives | `android-build` |
| `/srv/rom/tooling` | Host tooling — **not** Spec Kit checkout | `android-build` |
| `/srv/rom/ccache` | `CCACHE_DIR` (50G) | `android-build` |
| `/srv/rom/artifacts` | Artifact handoff to laptop | `android-build` |
| `/srv/rom/logs` | Preflight / toolchain / smoke logs | `android-build` |
| `/srv/rom/secrets` | Restricted secrets | `700 root:root` |
## Preflight summary (2026-09-13)
Full log: `/srv/rom/logs/preflight/preflight-latest.txt`
- Ubuntu 24.04.4 LTS / Linux 6.8.0-136-generic
- 8 vCPU (host CPU E3-1275 v5), **62 GiB** RAM, **32 GiB** swap (`/swapfile`)
- Disk: **1.9T** ext4 on `/` (~2T vda)
- NIC: `ens18` only — `49.13.104.83/29`, default via `49.13.104.81` (public-only)
- Firewall: ufw inactive; iptables ACCEPT (no flush)
- Listening: SSH `0.0.0.0:37037`, systemd-resolved localhost DNS
- apt: Ubuntu noble archives
## Toolchain (US2)
- Package log: `/srv/rom/logs/toolchain-packages.txt` (LOS wiki sunfish list +
`openjdk-17-jdk`, `python-is-python3`)
- `repo` in `~/bin/repo` (launcher 2.65)
- `CCACHE_DIR=/srv/rom/ccache`, `USE_CCACHE=1`, size **50G**, compression on
- Env in `android-build` `.profile` / `.bashrc`
- Smoke: `/srv/rom/logs/toolchain-smoke.txt`
- **FR-007 / build parallelism (use the HW):** guest has **8 vCPU / ~62 GiB /
32 GiB swap**. Default Android builds: **`mka … -j8`** (match vCPU count).
Do **not** leave conservative `-j4` unless diagnosing OOM. Watch `load` /
`available` RAM; if linking OOMs, drop to `-j6` temporarily — not below
without a written reason. Host-side: Proxmox CPU steal can waste cycles;
prefer dedicated/pinned vCPUs when practical (outside this repo).
Wrapper: `/srv/rom/logs/003-build/run-brunch.sh` (`-j8`).
- **nsjail / build sandbox (Ubuntu 24.04):** AppArmor
`kernel.apparmor_restrict_unprivileged_userns=1` put nsjail into profile
`unprivileged_userns` and DENY `mount`/`setgid` → Soong message
`Build sandboxing disabled due to nsjail error`. Fix (scoped, not global
sysctl): `/etc/apparmor.d/nsjail-android-build` (`userns` +
`flags=(unconfined)` for
`/srv/rom/src/lineage/prebuilts/build-tools/linux-{x86,arm64}/bin/nsjail`).
Evidence: `/srv/rom/logs/nsjail-apparmor-fix.txt`. Re-probe on next Soong
start (current `mka bacon` already started with sandbox off).
## Thin client / artifacts (US5)
- Cursor Remote SSH: Host `rom-dev01` →
`/home/android-build/sunfish-a16-rom` as `android-build`
- Dummy rsync: host `/srv/rom/artifacts/dummy-001.txt` → laptop
`Workspaces/sunfish-a16-rom/.local-artifacts/` (gitignored path preferred)
- Pixel USB: **adb/fastboot on laptop only**; no USB passthrough to VM
## Gitea / Ansible / Semaphore
Independent of SSYSTEMS Salt/GitLab. All on `rom-dev01`.
| Service | Bind | Public URL |
|---------|------|------------|
| nginx | `0.0.0.0:80/443` | TLS + HTTP Basic gate |
| Gitea 1.27.3 | `127.0.0.1:3000` | https://a16.ssystems.de/ |
| Semaphore 2.19.12 | `127.0.0.1:3001` | https://a16.ssystems.de/semaphore/ |
| Ansible | host CLI | core 2.16.3 |
- Site: `/etc/nginx/sites-available/a16.ssystems.de`
- TLS: company wildcard `*.ssystems.de` (Sectigo) at
`/etc/nginx/ssl/ssystems.de/{fullchain,privkey}.pem`
Source (laptop, not in this repo):
`buildout-saar-docker/salt/srv/files/etc/pki/tls/{certs,private}/wildcard_.ssystems.de-*`
- HTTP Basic: `/etc/nginx/.htpasswd` — credentials on host only:
`/srv/rom/secrets/web-htpasswd.txt` (user `sunfish`)
- Unit: `gitea.service` — data `/var/lib/gitea`, config `/etc/gitea/app.ini`
(`ROOT_URL=https://a16.ssystems.de/`)
- Unit: `semaphore.service` — data `/var/lib/semaphore`, config `/etc/semaphore/config.json`
(`web_host=https://a16.ssystems.de/semaphore`, `web_root=/semaphore`)
nginx: `location /semaphore/` → `proxy_pass` **without** trailing URI
(must preserve `/semaphore` prefix or UI assets 404 → blank page)
- App secrets (host only, never Git): `/srv/rom/secrets/gitea-admin.txt`,
`/srv/rom/secrets/semaphore-admin.txt`
- Admin user (both apps): `sunfish-admin` (after nginx Basic)
- Git transport: **HTTPS via nginx** (Gitea still `DISABLE_SSH=true`; no public `:2222`)
- Repos (private): `sunfish-admin/{sunfish-a16-rom,sunfish-rom-manifest,sunfish-rom-build,sunfish-rom-patches}`
- Automation tree: `/home/android-build/sunfish-rom-build` (not inside Spec Kit meta)
- Semaphore project `sunfish-rom` template `env-smoke` clones **canonical**
`sunfish-rom-build` from Gitea and runs `ansible/smoke.yml` unprivileged
- Evidence: `/srv/rom/logs/ansible-smoke.txt`, `/srv/rom/logs/semaphore-smoke.txt`
- Off-VM mirrors (laptop): `/home/aventura/Archives/sunfish-rom-mirrors/*.git`
- No SSH tunnels needed for UIs anymore
### Accounts (reminder)
- **root**: install packages/services only
- **android-build**: Android builds, meta + automation working trees
- **git** / **semaphore**: service users for Gitea / Semaphore
- Do **not** build Android as root (LOS/AXP practice)
## Isolation checklist
- [x] Public NIC only (`ens18` /29) — no internal SSYSTEMS NIC required
- [x] App backends remain loopback (`:3000` / `:3001`); only nginx is public
- [x] Workflow uses public DNS `a16.ssystems.de` + SSH port 37037 (no VPN)
- [x] Gitea/Semaphore behind nginx HTTPS + HTTP Basic (`htpasswd`)
## Off-VM mirror
- Laptop: `/home/aventura/Archives/sunfish-rom-mirrors/*.git`
- Host: `/srv/rom/artifacts/gitea-mirrors/` (token stays on host; laptop only rsyncs)
## Handoff
**`001` complete → proceed to `002` (AXP reference capture).**
Do **not** start LineageOS `repo sync` / build as part of `001`.
+781
View File
@@ -0,0 +1,781 @@
001 plan review pack — for ChatGPT
Generated: 2026-08-09T12:23:59+02:00
Constitution: 0.1.5 | Active: specs/001-development-environment
================================================================================
FILE: specs/001-development-environment/plan.md
================================================================================
# Implementation Plan: Development environment on rom-dev01
**Branch**: `001-development-environment` | **Date**: 2026-08-09 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-development-environment/spec.md`
**Mode**: Documentation/design only until `rom-dev01` exists with working SSH.
No implementation, host mutation, or Android sync/build in this planning phase.
## Summary
Feature `001` prepares an already-existing Ubuntu 24.04 host (`rom-dev01`) as the
ROM development environment: unprivileged `android-build` account, `/srv/rom`
layout, Lineage/AOSP host toolchain + ccache, Spec Kit meta-repo bootstrap,
private Gitea (four repos) with off-VM Git history mirror, Ansible + Semaphore
smoke orchestration, thin-client laptop workflow, and guest isolation posture.
It does **not** create the Proxmox VM, sync LineageOS, build Android, or touch
the Pixel. Those belong to human Salt/PVE prep (`rom-dev01` prerequisite) and
features `002`/`003`+.
**Unlocks**: after P1, host is usable for work; after P2, Gitea is canonical and
orchestration smoke works → ready for `002` then `003`.
## Technical Context
**Language/Version**: Bash / Ansible YAML; Ubuntu Server 24.04 LTS on host;
Spec Kit docs in Markdown (English).
**Primary Dependencies**: apt packages per current LineageOS/AOSP host build
docs at implementation time; `repo`; `git` + `git-lfs`; `ccache`; Gitea;
Ansible; Semaphore UI; OpenSSH.
**Storage**: Host filesystem paths under `/srv/rom/` (no project-owned LVM/fstab
changes). Secrets directory restricted/empty in `001`.
**Testing**: Observable shell/service checks (clean-login tool smoke, systemd
active, local curl, rsync SHA256, restart survival). No Android instrumentation.
**Target Platform**: Single VM `rom-dev01` (~6 vCPU / 52 GB RAM / 2 TB /
32 GB swap) + thin laptop (Cursor Remote SSH, adb/fastboot, rsync).
**Project Type**: Infrastructure / host bootstrap for Android ROM development.
**Performance Goals**: Toolchain available in a clean `android-build` login;
services survive restart; no public exposure of Gitea/Semaphore. Build `-j`
tuning is **out of scope** (owned by `003`).
**Constraints**:
- VM/PVE/IP/Salt `saltenv=base` provisioning is external; do not fight managed
config silently.
- Never build Android as root; never `chmod 777`.
- Never flush firewall / risk active SSH path.
- Do not bind Gitea/Semaphore to `0.0.0.0` without an explicit protected design
(default: loopback / non-public + SSH tunnel).
- No secrets in Git or build logs.
- Do not delete laptop staging tree before Gitea + mirror verified.
**Scale/Scope**: One developer; four Gitea repos; one smoke Ansible playbook;
no distributed CI / registry / Kubernetes.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Gate | Status | Notes |
|------|--------|-------|
| Independence from SSYSTEMS governance | PASS | No Redmine/GitLab/company MR workflow; no Salt VM creation from this repo |
| Spec-as-source | PASS | Plan derived from `001/spec.md` + constitution `0.1.5` |
| Parity/hardening docs non-normative | PASS | Not used as requirements for `001` |
| Host boundary: VM is prerequisite | PASS | Preconditions assume host exists; non-goals forbid PVE/qm/Salt states |
| Isolation posture (guest) | PASS | Public-NIC workflow; UIs not world-exposed; SSH preserved |
| No Android trees on laptop | PASS | Thin-client + `/srv/rom` on host |
| Secrets not in Git | PASS | Explicit secrets handling |
| Magisk/KSU/microG/signing out of `001` | PASS | Listed in DO NOT |
| Capacity honesty | PASS | Plan around 6/52/2T/32; parallelism deferred to `003` |
**Post-design re-check**: PASS — artifacts (`research.md`, `quickstart.md`,
operational `data-model.md`) stay inside `001` scope; no invented API contracts.
## Project Structure
### Documentation (this feature)
```text
specs/001-development-environment/
├── plan.md # This file
├── research.md # Phase 0
├── data-model.md # Operational entities only (no app domain model)
├── quickstart.md # Validation / cold-start guide
├── contracts/ # N/A for 001 (see contracts/README.md)
└── tasks.md # Created later by /speckit-tasks — NOT this command
```
### Host layout (target on rom-dev01)
```text
/srv/rom/
├── src/ # future Android trees (empty in 001; no repo sync)
├── references/ # future AXP/reference captures (empty OK)
├── tooling/ # project scripts / ansible / helper tooling
├── ccache/ # CCACHE_DIR for android-build
├── artifacts/ # handoff to laptop (dummy file for smoke)
├── logs/ # install/bootstrap logs
└── secrets/ # restricted; empty; no key generation in 001
# service data (exact paths chosen at implement time; document them)
# e.g. /var/lib/gitea, /etc/gitea, semaphore data dir
# meta Spec Kit tree (staging copy, then Gitea clone)
# e.g. /home/android-build/sunfish-a16-rom and/or /srv/rom/tooling/...
```
### Meta repository content (laptop staging → Gitea)
```text
sunfish-a16-rom/ # Spec Kit + docs (this tree)
├── .specify/
├── specs/
├── docs/
├── AGENTS.md
└── …
```
Companion empty repos on Gitea (no AOSP trees):
`sunfish-rom-manifest`, `sunfish-rom-build`, `sunfish-rom-patches`.
**Structure Decision**: Infrastructure feature — host paths + Git/service
bootstrap. No application `src/` tree inside Spec Kit for `001`.
## Complexity Tracking
> No constitution violations requiring justification.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|--------------------------------------|
| — | — | — |
---
## 1. Summary (expanded)
`001` makes `rom-dev01` a usable ROM workstation and private Git home without
performing ROM builds. Completion means: clean `android-build` toolchain smoke
passes; `/srv/rom` owned correctly; Spec Kit tree lives on host; Gitea holds
four repos and is canonical; laptop has an off-VM bare mirror; Ansible+Semaphore
run one smoke playbook; thin-client + isolation docs/checks pass; no PVE/Salt
fight, no Pixel/LOS work.
## 2. Constitution check / source of truth
Authoritative for this plan:
1. `.specify/memory/constitution.md` **v0.1.5**
2. `specs/001-development-environment/spec.md`
3. `AGENTS.md`
Non-normative (do not treat as `001` requirements):
`docs/axp-pro-parity.md`, `docs/hardening-backlog.md`.
## 3. Preconditions
Assume already true before `/speckit-implement`:
- `rom-dev01` exists (human + SSYSTEMS Salt/PVE)
- Ubuntu Server 24.04 LTS
- One reachable public IPv4 (public NIC only)
- SSH administration works
- `saltenv=base` already applied
- Capacity approximately: 6 vCPU / 52 GB RAM / 2 TB disk / 32 GB swap
Do **not** create, resize, or re-IP the VM from this project.
## 4. Non-goals / boundaries
**DO NOT** (plan / tasks / implement):
- create or configure the Proxmox VM; emit `qm`/PVE provisioning;
- choose or hard-code the public IPv4;
- write SSYSTEMS Salt states; modify `pm-test01`;
- assume company internal DNS/VPN/LDAP/GitLab/NFS/Zabbix is required;
- start LineageOS `repo sync` or any Android build;
- touch/backup/flash the Pixel (NeoBackup belongs to `002`);
- introduce microG, Phonesky, KernelSU, Magisk, SUSFS, or hardening;
- generate ROM/AVB/Phonesky signing keys;
- put `.repo`, `out/`, or ccache on the laptop;
- require Ansible on the laptop;
- expose Gitea or Semaphore to the public Internet;
- design GitLab/Jenkins/Kubernetes/registry/distributed runners;
- turn Semaphore into the Android build pipeline yet;
- invent an API/data model merely to satisfy a template;
- hard-code facts that must be measured on `rom-dev01`;
- duplicate requirements owned by `002`/`003`+.
**Implement-time DO NOT** (operational):
- run Android builds as root; `chmod 777` project trees;
- blindly dist-upgrade; replace apt sources without need;
- overwrite Salt/base-managed config silently;
- flush firewall/nftables; risk active SSH management path;
- bind Gitea/Semaphore to `0.0.0.0` without explicit protected design;
- commit passwords/tokens/private keys; plaintext secrets in Ansible in Git;
- delete laptop staging before Gitea + mirror verified;
- delete/overwrite working data only to fake idempotency;
- download full Lineage tree; compile Lineage; connect/flash Pixel;
- automate ROM builds through Semaphore yet.
**Conflict rule**: If base-managed configuration conflicts with project needs,
**stop and report** — do not silently replace or fight it; do not “fix”
unrelated infrastructure opportunistically.
## 5. Host preflight (before modifying the host)
First implement steps are **observational**. Capture and store under
`/srv/rom/logs/preflight/` (or create layout first then log):
- Ubuntu version / kernel
- CPU / RAM / swap
- filesystem free space (esp. for `/` and future `/srv/rom`)
- network interfaces / default route (confirm no required internal NIC)
- SSH reachability
- existing users / groups
- existing firewall (nft/ufw/iptables) summary — **do not flush**
- listening ports / services
- apt sources summary
- hints of Salt/base-managed files if obvious
Produce a short baseline note in the meta repo docs or `/srv/rom/logs` for later
diff. Especially important because `saltenv=base` already touched the machine.
## 6. Account and permission model
| Actor | Role |
|-------|------|
| Existing admin SSH user | packages, systemd, system config, firewall changes (carefully) |
| `android-build` | Android sources/builds (later), ccache, artifacts, build tooling; **never** build as root; **no sudo required to compile** |
| `gitea` (service user) | Gitea data/config ownership |
| `semaphore` (service user/context) | Semaphore data/config ownership |
Rules:
- No `chmod -R 777 /srv/rom`
- Group memberships only as needed (e.g. `android-build` owns `/srv/rom` trees
it writes; services own their state dirs)
- Admin elevates only for system packaging/services
## 7. `/srv/rom` filesystem layout
Create with explicit owners/modes (exact modes chosen at implement; document):
```text
/srv/rom/
├── src/
├── references/
├── tooling/
├── ccache/
├── artifacts/
├── logs/
└── secrets/ # empty, restricted; do not generate keys in 001
```
- Do **not** manage partitions/LVM/fstab from this project.
- Do not populate `references/` (that is `002`) or generate secrets.
- `artifacts/` must be writable by `android-build` for dummy + later builds.
## 8. Android host toolchain (no sync/build)
- Install host packages using **current primary LineageOS/AOSP build
documentation at implementation time** (do not freeze a stale 50-package list
in this plan).
- Record what was installed (log + short note in meta repo / `/srv/rom/logs`).
- Ensure `git`, `git-lfs`, `repo`, `python3`, and Java/toolchain prerequisites
required by that documentation.
- Configure `ccache`: location (`/srv/rom/ccache`), ownership (`android-build`),
size cap, environment (`CCACHE_DIR`, etc.). **Not** tied to inventing `-j`
from vCPU/RAM here.
- FR-007: document capacity (6/52/32) and that **conservative `-j` / OOM tuning
belongs to `003`**, with a one-line recommended starting point only if needed
for docs — no build execution in `001`.
- Smoke from a **clean login**: `sudo -iu android-build` → verify tools; **no
`repo sync`**.
## 9. Spec Kit staging bootstrap (before Gitea)
Egg/chicken sequence (mandatory):
```text
laptop staging:
/home/aventura/Workspaces/sunfish-a16-rom
↓ rsync/scp/git bundle (preserve .git)
rom-dev01 working copy
↓
install Gitea
↓
create empty Gitea repo sunfish-a16-rom
↓
push meta repo
↓
create sunfish-rom-manifest, sunfish-rom-build, sunfish-rom-patches
↓
Gitea becomes canonical remote
```
Do **not** assume `git clone https://gitea...` before Gitea exists.
Do **not** delete laptop staging until Gitea push + off-VM mirror verified.
## 10. Gitea
- Install on `rom-dev01` with persistent data dirs and systemd unit.
- Default access: **loopback / non-public bind + SSH tunnel** from laptop
(decision fixed for `001` unless implement discovers a hard conflict — then
stop/report).
- Create admin credentials with restricted local storage; never commit secrets.
- Verify: service active; local curl succeeds; direct public Internet access to
UI **not** required / not open.
## 11. Canonical Git transition
Repos:
1. `sunfish-a16-rom` (meta Spec Kit/docs) — push existing history
2. `sunfish-rom-manifest` — empty
3. `sunfish-rom-build` — empty (future Ansible/scripts home)
4. `sunfish-rom-patches` — empty
Document remotes for host + laptop clones. Company GitLab must not be used.
## 12. Off-VM Git mirror (P2)
First implementation (sufficient for `001` SC-007):
```text
Gitea repos → bare mirrors on laptop (or equivalent off-VM path)
```
Borg / full Gitea disaster recovery is **later**, not required to close `001`.
Requirement is **Git history** redundancy, not VM image backup.
## 13. Ansible bootstrap (P2)
- Install Ansible on `rom-dev01` for **project environment** automation.
- Keep playbooks in meta repo or `sunfish-rom-build` once created.
- No plaintext secrets in Git (use local ansible vault / restricted files outside
Git if needed in `001` — prefer deferred secrets).
## 14. Semaphore bootstrap (P2)
- Install Semaphore as UI over Ansible.
- Same exposure model as Gitea (loopback/tunnel).
- First integration: **small localhost/environment smoke playbook only** —
prove Semaphore can run Ansible. **Not** Android build pipeline.
## 15. Isolation / firewall / service exposure
Keep guest isolation posture from constitution/`001` spec:
- Public NIC only for project workflow
- Gitea/Semaphore not world-exposed
- Document firewall/DNS expectations
Implementation safety:
- never flush/replace existing firewall blindly;
- never close current SSH path before a tested replacement;
- keep a rollback path;
- prefer additive allow rules.
## 16. Laptop thin-client workflow
Document and smoke:
1. Cursor Remote SSH → host workspace as `android-build` (or documented path)
2. Dummy artifact in `/srv/rom/artifacts` → `rsync` to laptop → SHA256 match
3. adb/fastboot remain on laptop; Pixel USB on laptop; no USB passthrough design
## 17. Verification matrix (FR/SC → evidence)
| ID | Implementation focus | Observable verification |
|----|----------------------|-------------------------|
| FR-001 | Create `android-build` + home/workspace docs | `id android-build`; login shell works |
| FR-002 | LOS/AOSP host deps + ccache | clean-shell: `git`, `repo`, `ccache -s`, etc. |
| FR-003 | `/srv/rom` tree + ownership | `namei -l` / `stat` shows owners; writable artifacts |
| FR-004 | Gitea + four repos | local UI/API; four repos listed |
| FR-005 | Ansible + Semaphore smoke | Semaphore runs smoke playbook successfully |
| FR-006 | Thin-client docs | doc exists; Remote SSH + rsync smoke done |
| FR-007 | Capacity / defer `-j` to `003` | short doc records 6/52/32 and defers build jobs |
| FR-008 | Spec Kit on host | tree present; after P2, cloned/pushed via Gitea |
| FR-009 | Isolation posture | checklist: NIC, binds, firewall notes; SSH still works |
| FR-010 | Off-VM Git mirror | laptop bare mirror of four repos once |
| SC-001 | Toolchain smoke | clean `android-build` session has tools for later `003` |
| SC-002 | Artifacts handoff | dummy file rsync SHA256 match |
| SC-003 | Four Gitea repos | listed on Gitea; not company GitLab |
| SC-004 | Cold-start workflow doc | quickstart path SSH→workspace→(later build)→rsync→flash |
| SC-005 | No PVE management | no qm/PVE playbooks in repo/tasks evidence |
| SC-006 | Isolation checklist | passes without requiring VPN/internal DNS |
| SC-007 | Mirror demonstrated | off-VM mirror exists and was verified once |
## 18. Handoff
**`001` complete** means environment ready for `002` (phone capture) and later
`003` (LOS vanilla build) — **not** “start building Lineage now”.
Do not begin `/speckit-plan` for `002`/`003` until `001` doc cycle + implement
are done (avoid document saturation).
## P1 / P2 sequencing
### P1 — host usable without Gitea
1. Host preflight baseline
2. Create `android-build`
3. Create `/srv/rom` + permissions
4. Install build prerequisites (current LOS/AOSP docs)
5. `repo` / `git` / `git-lfs` / ccache config
6. Copy laptop staging meta repo to host (preserve `.git`)
7. Clean-shell toolchain smoke
8. Cursor Remote SSH smoke
9. Dummy artifact → rsync → laptop (SHA256)
### P2 — reproducible / comfortable
1. Install Gitea (loopback/tunnel)
2. Create four repos
3. Push meta repo; set Gitea canonical
4. Off-VM bare mirrors on laptop
5. Install Ansible
6. First smoke playbook
7. Install Semaphore
8. Run smoke playbook from Semaphore
9. Isolation / access / firewall validation (SSH-safe)
10. Service restart (+ reboot survival where practical)
Finish P2 before formally starting `002`.
## Phase outputs
| Artifact | Role |
|----------|------|
| [research.md](./research.md) | Decisions: Gitea/Semaphore access, mirror, deps source, ccache |
| [data-model.md](./data-model.md) | Operational entities only |
| [quickstart.md](./quickstart.md) | Cold-start validation |
| [contracts/](./contracts/) | N/A |
Next command after human review of these files: `/speckit-tasks` (still no VM).
Then `/speckit-analyze`. Stop before `/speckit-implement` until SSH to
`rom-dev01` works.
================================================================================
FILE: specs/001-development-environment/research.md
================================================================================
# Research: 001-development-environment
Phase 0 decisions for planning. Implementation may refine versions using live
primary docs on `rom-dev01`.
## R1 — Gitea access model
**Decision**: Bind Gitea to loopback (or other non-public address) and access via
SSH local port forward from the laptop. Do not require public HTTPS exposure.
**Rationale**: Constitution/`001` require private project Git without unnecessary
Internet exposure; single developer; SSH already required for admin/Remote SSH.
**Alternatives considered**:
- Public HTTPS + firewall allowlist — rejected for `001` (more moving parts;
accidental exposure risk).
- VPN-only bind — rejected as a *required* path (may exist externally; project
must not depend on company VPN).
## R2 — Semaphore access model
**Decision**: Same as Gitea — loopback/non-public + SSH tunnel. First job is a
localhost/environment smoke playbook only.
**Rationale**: Semaphore is orchestration UI, not a public portal; Android build
pipeline is explicitly out of scope for `001`.
**Alternatives considered**: Public Semaphore — rejected. Full ROM pipeline in
Semaphore — deferred past `003` manual first build.
## R3 — Off-VM Git history backup
**Decision**: Laptop-side **bare mirrors** of the four Gitea repos for `001`
(SC-007). Borg/full Gitea DR later.
**Rationale**: Spec requires off-VM copy of project Git history, not a complete
backup platform. Simplest path that meets SC-007.
**Alternatives considered**: Borg-only — optional later. Second server — not
needed for `001`.
## R4 — Lineage/AOSP host dependencies
**Decision**: At implement time, follow **current primary LineageOS / AOSP host
build documentation** for Ubuntu; record installed package set in logs. Do not
freeze a long package list in the plan.
**Rationale**: Dependency lists drift; stale pins cause false confidence.
**Alternatives considered**: Copy a historical package list from blogs —
rejected.
## R5 — `repo` installation source
**Decision**: Install Google's `repo` launcher via the method recommended by
current Lineage/AOSP docs (typically curl to `~/bin/repo` or distro package if
still adequate); verify `repo --version` as `android-build`.
**Rationale**: Align with upstream bring-up docs used for `003`.
## R6 — ccache policy vs build parallelism
**Decision**: `001` configures `CCACHE_DIR=/srv/rom/ccache` (or equivalent),
ownership for `android-build`, and a disk size cap. Document host capacity
(6 vCPU / 52 GB / 32 GB swap). **Defer `-j` / OOM tuning to `003`.**
**Rationale**: ccache is storage/env; parallel link/compile jobs belong to the
first real build feature.
**Alternatives considered**: Encoding `-j$(nproc)` defaults in `001` — rejected
as premature and confusing with FR-007 (document capacity; don't run builds).
## R7 — Meta-repo bootstrap before Gitea
**Decision**: Copy laptop staging Git tree to host first (rsync/scp/bundle
preserving `.git`), install Gitea, create repos, push, then mark Gitea
canonical. Keep laptop staging until mirror verified.
**Rationale**: Avoid chicken/egg `git clone` against non-existent Gitea.
## R8 — Coexistence with `saltenv=base`
**Decision**: Preflight + stop/report on conflicts with base-managed config.
Project Ansible may manage **project** paths/services; must not silently rewrite
Salt-owned system policy.
**Rationale**: Host is dual-touched (SSYSTEMS base then project). Fighting Salt
causes flapping.
## R9 — data-model / contracts
**Decision**: No application domain model or HTTP API contracts for `001`.
Operational entities only in `data-model.md`. `contracts/` marked N/A.
**Rationale**: Infrastructure bootstrap feature; inventing APIs would be template
noise.
================================================================================
FILE: specs/001-development-environment/data-model.md
================================================================================
# Data model (operational): 001-development-environment
This is **not** an application domain model. It records host/Git/service
entities for planning clarity. No ORM, no API schemas.
## Entities
### Host (`rom-dev01`)
| Field | Notes |
|-------|-------|
| OS | Ubuntu 24.04 LTS (precondition) |
| Capacity | ~6 vCPU / 52 GB / 2 TB / 32 GB swap |
| Network | Public IPv4 NIC; no required internal NIC |
| Management | External Salt `saltenv=base` + project config |
### Account `android-build`
| Field | Notes |
|-------|-------|
| Type | Unprivileged login user |
| Owns | `/srv/rom/{src,references,tooling,ccache,artifacts,logs}` (as documented) |
| Must not | Need root to compile; own Gitea/Semaphore system dirs |
### Path root `/srv/rom`
| Child | Purpose | Populated in 001? |
|-------|---------|-------------------|
| `src/` | Future Android trees | Empty (no sync) |
| `references/` | Future AXP/reference | Empty |
| `tooling/` | Scripts/ansible helpers | Partial OK |
| `ccache/` | CCACHE_DIR | Yes (dir + env) |
| `artifacts/` | Laptop handoff | Dummy file for smoke |
| `logs/` | Bootstrap/preflight logs | Yes |
| `secrets/` | Restricted placeholder | Empty; no keygen |
### GiteaInstance
| Field | Notes |
|-------|-------|
| Bind | Loopback / non-public |
| Repos | `sunfish-a16-rom`, `sunfish-rom-manifest`, `sunfish-rom-build`, `sunfish-rom-patches` |
| Canonical | Yes after first successful push of meta repo |
### GitMirror (off-VM)
| Field | Notes |
|-------|-------|
| Location | Laptop bare mirrors (initial) |
| Scope | All four project repos |
| Success | At least one verified mirror cycle |
### AnsibleController / SemaphoreInstance
| Field | Notes |
|-------|-------|
| Host | `rom-dev01` |
| First job | Environment smoke playbook |
| Exposure | Same private access model as Gitea |
## Relationships
```text
AdminSSH ──manages──► Host packages/systemd/firewall
android-build ──writes──► /srv/rom (build-related trees)
GiteaInstance ──hosts──► four Repositories
Laptop ──mirrors──► Repositories (bare)
SemaphoreInstance ──runs──► Ansible smoke playbook on Host
Laptop Cursor ──RemoteSSH──► Host workspace
Laptop ──rsync──► /srv/rom/artifacts (pull)
```
## State transitions (meta Git)
1. `laptop-staging-only`
2. `copied-to-host` (pre-Gitea)
3. `gitea-canonical` (push complete)
4. `mirrored-off-vm` (SC-007)
Do not destroy state (1) until (3) and (4) succeed.
================================================================================
FILE: specs/001-development-environment/quickstart.md
================================================================================
# Quickstart / validation: 001-development-environment
Use after `/speckit-implement` on a real `rom-dev01`. Until the VM exists, treat
this as the acceptance script shape.
## Prerequisites
- SSH to `rom-dev01` as admin works
- You are ready to create `android-build` and `/srv/rom` (P1)
- Laptop has `rsync`, `ssh`, Cursor Remote SSH
## P1 validation path
### 1. Preflight captured
```bash
# On host (admin) — exact commands filled at implement time
# Expect: Ubuntu 24.04, disk/RAM/swap notes, firewall summary saved
```
### 2. Switch to build user
```bash
sudo -iu android-build
pwd # documented workspace
```
### 3. Toolchain smoke (clean shell)
```bash
git --version
git lfs version
repo --version
python3 --version
ccache -s
# plus java/other tools required by current LOS/AOSP host docs
```
Expected: all commands succeed without sudo.
### 4. Layout
```bash
namei -l /srv/rom /srv/rom/artifacts /srv/rom/ccache
touch /srv/rom/artifacts/.write_test && rm /srv/rom/artifacts/.write_test
```
### 5. Meta repo present on host (pre-Gitea OK)
```bash
# path documented at implement — must contain .git and specs/
test -d /path/to/sunfish-a16-rom/.git
test -f /path/to/sunfish-a16-rom/specs/001-development-environment/spec.md
```
### 6. Cursor Remote SSH
From laptop: open host workspace; edit a trivial file; confirm it is on the VM
disk (not only local).
### 7. Artifact rsync smoke
On host:
```bash
echo '001-smoke' > /srv/rom/artifacts/dummy-001.txt
sha256sum /srv/rom/artifacts/dummy-001.txt
```
On laptop:
```bash
rsync -avz USER@ROM_DEV01:/srv/rom/artifacts/dummy-001.txt ./
sha256sum ./dummy-001.txt # must match
```
## P2 validation path
### 8. Gitea private access
```bash
# SSH tunnel example (ports chosen at implement):
# ssh -L 3000:127.0.0.1:3000 USER@ROM_DEV01
# curl -fsS http://127.0.0.1:3000/ → success via tunnel
```
Confirm UI not relied upon as public Internet service.
### 9. Four repositories
In Gitea: `sunfish-a16-rom`, `sunfish-rom-manifest`, `sunfish-rom-build`,
`sunfish-rom-patches`. Meta repo history visible after push.
### 10. Off-VM mirror
On laptop: bare clones/mirrors of all four; `git fsck` or fetch succeeds.
### 11. Semaphore smoke
Semaphore runs the environment smoke playbook successfully (localhost/host
check only — not Android build).
### 12. Restart survival
```bash
sudo systemctl restart gitea # unit names as installed
sudo systemctl restart <semaphore>
# optional: reboot when safe; confirm SSH + services return
```
### 13. Isolation checklist
- No required internal NIC for workflow
- Gitea/Semaphore binds documented as non-public
- SSH still works after firewall changes
- Day-to-day docs do not require company VPN/internal DNS
## Done means
`001` complete → start **`002`** (AXP capture / NeoBackup).
Do **not** start Lineage `repo sync` / build as part of `001` success.
================================================================================
FILE: specs/001-development-environment/contracts/README.md
================================================================================
# Contracts: N/A for 001
Feature `001-development-environment` is host/Git/service bootstrap.
It does **not** expose a project-owned HTTP API, CLI contract for external
consumers, or IPC schema that needs `contracts/` files.
Service access is operational (SSH tunnel to Gitea/Semaphore UIs) and is
documented in `plan.md` / `quickstart.md` instead.
+222
View File
@@ -0,0 +1,222 @@
001 tasks review pack — for ChatGPT
Generated: 2026-08-09T12:30:10+02:00
================================================================================
FILE: specs/001-development-environment/tasks.md
================================================================================
# Tasks: Development environment on rom-dev01
**Input**: Design documents from `/specs/001-development-environment/`
**Prerequisites**: plan.md (required), spec.md, research.md, data-model.md,
quickstart.md, contracts/ (N/A)
**Tests**: Observational verify steps embedded in tasks (no separate TDD suite).
**Organization**: Setup → Foundational (preflight) → User stories by priority →
Polish. **Do not run these tasks until `rom-dev01` SSH works.** Generating this
file is documentation-only.
**Canonical paths**:
- Meta Spec Kit tree: `/home/android-build/sunfish-a16-rom`
- Data root: `/srv/rom/`
- Automation repo (after Gitea): `sunfish-rom-build` (not the meta repo)
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no incomplete dependencies)
- **[Story]**: US1…US5 from spec.md
- Every task includes a path and an observable verify hint
## Path Conventions
Host paths are absolute on `rom-dev01`. Docs/logs also land under
`/home/android-build/sunfish-a16-rom/docs/` or `/srv/rom/logs/` as noted.
---
## Phase 1: Setup (docs readiness — can be done on laptop before VM)
- [ ] T001 Confirm active feature is `specs/001-development-environment` via `.specify/feature.json` and that constitution is `0.1.5` in `.specify/memory/constitution.md`
- [ ] T002 [P] Create host-facing doc stub `docs/001-host-runbook.md` in meta repo (preflight results, paths, Gitea tunnel notes — fill during implement)
- [ ] T003 [P] Create FR/SC evidence checklist stub `docs/001-verification-matrix.md` mapping each FR-*/SC-* from `specs/001-development-environment/spec.md` to blank evidence fields
---
## Phase 2: Foundational (blocking — host preflight before stories)
**Goal**: Observe `rom-dev01` without fighting `saltenv=base`; abort on conflict.
- [ ] T004 On `rom-dev01` as admin, capture preflight baseline into `/srv/rom/logs/preflight/` (create `/srv/rom/logs` with safe perms if needed): Ubuntu/kernel, CPU/RAM/swap, disk free, interfaces/routes, users, firewall summary (**do not flush**), listening ports, apt sources → also summarize in `docs/001-host-runbook.md`
- [ ] T005 Record coexistence rule in `docs/001-host-runbook.md`: if base-managed config conflicts with project needs, **stop and report**; do not silently replace Salt-managed settings
- [ ] T006 Verify SSH admin path still works after any read-only inspection; note rollback contacts/commands in `docs/001-host-runbook.md`
---
## Phase 3: User Story 1 — Reachable build workspace (P1)
**Story goal**: `android-build` exists with documented workspace; `/srv/rom` owned correctly; meta tree at fixed path.
**Independent test**: `sudo -iu android-build` works; `/srv/rom/artifacts` writable; `/home/android-build/sunfish-a16-rom` contains Spec Kit tree (after T012).
### Implementation
- [ ] T007 [US1] Create unprivileged user `android-build` with home `/home/android-build` (admin); document uid/gid in `docs/001-host-runbook.md`
- [ ] T008 [US1] Create `/srv/rom/{src,references,tooling,ccache,artifacts,logs,secrets}` with ownership for `android-build` on writable trees; `secrets/` restricted/empty — document modes in `docs/001-host-runbook.md`
- [ ] T009 [US1] Verify: `namei -l /srv/rom /srv/rom/artifacts` and `sudo -iu android-build touch /srv/rom/artifacts/.write_test` succeeds; no `chmod 777`
- [ ] T010 [US1] Document account model (admin vs `android-build` vs service users) in `docs/001-host-runbook.md` — never build Android as root
- [ ] T011 [US1] Copy laptop staging Git tree to `/home/android-build/sunfish-a16-rom` preserving `.git` (rsync/scp/bundle); verify `test -f /home/android-build/sunfish-a16-rom/specs/001-development-environment/spec.md`
- [ ] T012 [US1] Ensure `/srv/rom/tooling` is **not** used as Spec Kit checkout; note distinction in `docs/001-host-runbook.md`
---
## Phase 4: User Story 2 — Android host toolchain (P1)
**Story goal**: Clean-login toolchain ready for later `003` (no `repo sync` / build).
**Independent test**: `sudo -iu android-build` → `git`, `git lfs`, `repo`, `python3`, `ccache -s`, and LOS-doc Java/prereqs succeed.
### Implementation
- [ ] T013 [US2] As admin, install host packages per **current** primary LineageOS/AOSP Ubuntu build documentation; record exact package list in `/srv/rom/logs/toolchain-packages.txt`
- [ ] T014 [US2] Install/configure `repo` for `android-build` per current docs; verify `sudo -iu android-build repo --version`
- [ ] T015 [US2] Ensure `git` + `git-lfs` for `android-build`; verify `sudo -iu android-build git lfs version`
- [ ] T016 [US2] Configure ccache: `CCACHE_DIR=/srv/rom/ccache`, ownership `android-build`, size cap; document env in `/home/android-build/.bashrc` or `/home/android-build/.profile` and `docs/001-host-runbook.md`
- [ ] T017 [US2] Verify: `sudo -iu android-build ccache -s` shows configured cache dir
- [ ] T018 [US2] Document FR-007 capacity (6 vCPU / 52 GB / 32 GB swap) and **defer `-j`/OOM tuning to `003`** in `docs/001-host-runbook.md` — do not run a Lineage build
- [ ] T019 [US2] Clean-shell toolchain smoke checklist executed; paste command outputs into `/srv/rom/logs/toolchain-smoke.txt` and tick FR-002/SC-001 in `docs/001-verification-matrix.md`
---
## Phase 5: User Story 5 — Laptop thin client (P1/P2 overlap — do after US1–US2)
**Story goal**: Cursor Remote SSH + artifact rsync proven; Pixel stays on laptop USB.
**Independent test**: Remote SSH edit on host; dummy artifact SHA256 matches after rsync.
### Implementation
- [ ] T020 [US5] Document Cursor Remote SSH → `/home/android-build/sunfish-a16-rom` in `docs/001-host-runbook.md` and `specs/001-development-environment/quickstart.md` (update if needed)
- [ ] T021 [US5] Create `/srv/rom/artifacts/dummy-001.txt` as `android-build`; record `sha256sum` in `/srv/rom/logs/rsync-smoke.txt`
- [ ] T022 [US5] From laptop, `rsync` dummy artifact; verify SHA256 match; record laptop path in `/srv/rom/logs/rsync-smoke.txt` and `docs/001-verification-matrix.md` (SC-002)
- [ ] T023 [US5] Document adb/fastboot-on-laptop / no USB passthrough rule in `docs/001-host-runbook.md` (SC-004 partial)
---
## Phase 6: User Story 3 — Gitea, Ansible, Semaphore, off-VM mirror (P2)
**Story goal**: Private Gitea with four repos; canonical meta remote; laptop pull mirrors; unprivileged Semaphore smoke via Ansible in `sunfish-rom-build`.
**Independent test**: Four repos on Gitea; meta history pushed; laptop `git clone --mirror` works; Semaphore runs unprivileged smoke.
### Implementation
- [ ] T024 [US3] Install Gitea on `rom-dev01` with systemd + persistent data; bind UI to loopback/non-public; document unit/paths in `docs/001-host-runbook.md`
- [ ] T025 [US3] Store Gitea admin credentials only in restricted host location under `/srv/rom/secrets/` or service-native secret path — **never** commit to Git; document location (not secrets) in `docs/001-host-runbook.md`
- [ ] T026 [US3] Verify: local `curl` to Gitea UI succeeds; confirm not relying on public Internet exposure; record bind address in `docs/001-host-runbook.md`
- [ ] T027 [US3] Document Git transport chosen after host inspection (HTTP via SSH tunnel and/or tunneled Git SSH); **do not** open public `:2222` or blindly edit `saltenv=base` sshd — write choice in `docs/001-host-runbook.md`
- [ ] T028 [US3] Create Gitea repos: `sunfish-a16-rom`, `sunfish-rom-manifest`, `sunfish-rom-build`, `sunfish-rom-patches`
- [ ] T029 [US3] Push `/home/android-build/sunfish-a16-rom` history to Gitea `sunfish-a16-rom`; set `origin` to Gitea; verify remote in that directory
- [ ] T030 [US3] Seed minimal structure in `sunfish-rom-build` for Ansible (e.g. `ansible/` playbooks path) — **not** inside Spec Kit meta repo
- [ ] T031 [US3] Install Ansible on `rom-dev01`; document version in `docs/001-host-runbook.md`
- [ ] T032 [US3] Add unprivileged smoke playbook in `sunfish-rom-build` (gather_facts + asserts for `/srv/rom`, `android-build`, toolchain); **no become/sudo secrets**
- [ ] T033 [US3] Run smoke playbook successfully via `ansible-playbook` CLI as documented non-root/project user; log output to `/srv/rom/logs/ansible-smoke.txt`
- [ ] T034 [US3] Install Semaphore; bind like Gitea (loopback/tunnel); no public exposure; document in `docs/001-host-runbook.md`
- [ ] T035 [US3] Wire Semaphore to run the **same unprivileged** smoke playbook only; execute once successfully; evidence in `/srv/rom/logs/semaphore-smoke.txt` (FR-005)
- [ ] T036 [US3] From **laptop**, create bare mirrors with `git clone --mirror` for all four repos (laptop initiates connection); run `git remote update` + `git fsck`; document mirror paths in `docs/001-host-runbook.md` (FR-010/SC-007)
- [ ] T037 [US3] Confirm laptop staging `/home/aventura/Workspaces/sunfish-a16-rom` is **retained** until T029+T036 verified; only then optionally mark staging as non-canonical in `docs/001-host-runbook.md`
---
## Phase 7: User Story 4 — Guest isolation (P2)
**Story goal**: Isolation checklist passes without breaking SSH.
**Independent test**: Public-NIC-only workflow; UIs non-public; SSH still works; no VPN/internal DNS required in docs.
### Implementation
- [ ] T038 [US4] Document interfaces/routes: project workflow requires only public NIC — `docs/001-host-runbook.md` + evidence in `docs/001-verification-matrix.md` (SC-006)
- [ ] T039 [US4] Apply isolation/firewall guidance **additively** if needed: never flush nftables/iptables; never close active SSH before tested alternative; record before/after in `/srv/rom/logs/firewall-notes.txt`
- [ ] T040 [US4] Re-verify Gitea/Semaphore binds remain non-public; SSH admin + tunnel still work — update `docs/001-verification-matrix.md` FR-009/SC-006
- [ ] T041 [US4] Confirm day-to-day runbook does not require SSYSTEMS VPN/internal DNS/LDAP/GitLab/NFS/Zabbix — `docs/001-host-runbook.md`
---
## Phase 8: Polish & cross-cutting
- [ ] T042 [P] Fill all FR-*/SC-* evidence rows in `docs/001-verification-matrix.md` (including SC-005: no PVE/qm/Salt VM tasks were performed)
- [ ] T043 [P] Align `specs/001-development-environment/quickstart.md` with final ports/paths/tunnel commands actually used
- [ ] T044 Restart Gitea (+ Semaphore) via systemd; confirm recovery; optional reboot when safe; note results in `/srv/rom/logs/restart-smoke.txt`
- [ ] T045 Declare handoff in `docs/001-host-runbook.md`: **`001` complete → proceed to `002`**, do not start Lineage sync/build
- [ ] T046 Ensure no secrets/tokens landed in `/home/android-build/sunfish-a16-rom` or `sunfish-rom-build` Git history (`git grep` / review)
---
## Dependencies & story order
```text
Phase 1 (laptop docs)
↓
Phase 2 preflight (host)
↓
US1 workspace + /srv/rom + meta copy
↓
US2 toolchain ──────────────┐
↓ │
US5 thin-client smokes ←───┘ (needs artifacts + meta path)
↓
US3 Gitea → repos → Ansible in sunfish-rom-build → Semaphore → laptop mirrors
↓
US4 isolation hardening/validation
↓
Polish / handoff → STOP (wait was already for implement; next feature 002)
```
**MVP (minimum useful host)**: complete through US2 + US5 (T007–T023) even if Gitea delayed — but **formal `001` done** requires P2 (US3+US4+polish) per plan.
## Parallel opportunities
- T002/T003 on laptop before VM
- After T008: T010 docs can parallel T011 copy if disk ready
- T013–T015 package/`repo`/`git-lfs` partly serial (apt) then T016 ccache
- T042/T043 polish docs in parallel after evidence exists
## FR/SC coverage map
| ID | Tasks |
|----|-------|
| FR-001 | T007–T010 |
| FR-002 | T013–T017, T019 |
| FR-003 | T008–T009, T012 |
| FR-004 | T024–T029 |
| FR-005 | T030–T035 |
| FR-006 | T020–T023 |
| FR-007 | T018 |
| FR-008 | T011, T029 |
| FR-009 | T038–T041 |
| FR-010 | T036–T037 |
| SC-001 | T019 |
| SC-002 | T021–T022 |
| SC-003 | T028–T029 |
| SC-004 | T020, T023, T045 |
| SC-005 | T005, T042 |
| SC-006 | T038–T041 |
| SC-007 | T036 |
## Implementation strategy
1. Do **not** execute host tasks until SSH to `rom-dev01` works.
2. After this `tasks.md` is approved: `/speckit-analyze` → STOP.
3. `/speckit-implement` follows P1 then P2; stop/report on Salt conflicts.
4. Do not plan `002` until `001` implement + verification complete.
================================================================================
Micro-adjustments applied to plan (summary)
================================================================================
- Meta path fixed: /home/android-build/sunfish-a16-rom
- Ansible in sunfish-rom-build only
- Semaphore smoke unprivileged (no sudo secrets)
- Off-VM mirror: laptop pull only
- No public :2222 / no blind sshd edits; transport documented at implement
+38
View File
@@ -0,0 +1,38 @@
# 001 verification matrix — FR / SC evidence
Map each requirement from `specs/001-development-environment/spec.md` to
observable evidence. Leave blank until verified on `rom-dev01`.
## Functional requirements
| ID | Requirement (short) | Evidence (command/path/doc) | Status |
|----|---------------------|-----------------------------|--------|
| FR-001 | `android-build` + documented workspace | uid 1000; `/home/android-build/sunfish-a16-rom`; runbook | pass |
| FR-002 | Android/LOS prereqs + ccache | `/srv/rom/logs/toolchain-packages.txt` + `toolchain-smoke.txt` | pass |
| FR-003 | `/srv/rom` layout | namei + write test artifacts | pass |
| FR-004 | Private Gitea for project repos | loopback :3000; 4 private repos; secrets under /srv/rom/secrets/ | pass |
| FR-005 | Ansible + Semaphore UI | ansible 2.16.3; Semaphore task env-smoke success | pass |
| FR-006 | Cursor Remote SSH + rsync + laptop flash workflow doc | runbook Host `rom-dev01`; rsync smoke; adb on laptop | pass |
| FR-007 | Conservative parallelism defaults (~6 vCPU) | measured 8/62/32; `-j` deferred to 003 | pass |
| FR-008 | Spec Kit tree on host | `/home/android-build/sunfish-a16-rom` SPEC_OK | pass |
| FR-009 | Guest isolation posture | public NIC only; UIs on 127.0.0.1 | pass |
| FR-010 | Off-VM backup/mirror of Gitea repos | laptop Archives/sunfish-rom-mirrors/*.git fsck OK | pass |
## Success criteria
| ID | Outcome (short) | Evidence | Status |
|----|-----------------|----------|--------|
| SC-001 | Clean `android-build` SSH has toolchain for `003` | `toolchain-smoke.txt` (git/lfs/repo/ccache/java) | pass |
| SC-002 | `/srv/rom/artifacts` writable handoff | dummy-001 SHA256 match laptop `.local-artifacts/` | pass |
| SC-003 | Four Gitea repos ready (no company GitLab) | sunfish-admin/* on local Gitea | pass |
| SC-004 | Cold-start workflow doc | quickstart.md + host-runbook | pass |
| SC-005 | No PVE/VM create/reconfig in this feature | no qm/PVE/Salt VM work in 001 | pass |
| SC-006 | Isolation checklist passes | runbook isolation checklist | pass |
| SC-007 | Off-VM mirror demonstrated once | rsync bare mirrors + fsck | pass |
Status values: `pending` | `pass` | `fail` | `blocked`
## Task cross-check
Track completion in `specs/001-development-environment/tasks.md`. This matrix
is evidence, not a second task list.
+15
View File
@@ -0,0 +1,15 @@
# 002 gate checklist (handoff → 003)
Date: 2026-09-13
| Gate | Status | Evidence |
|------|--------|----------|
| GOLDEN labeled; do-not-flash | OK | `docs/device-roles.md` — serial `11071JEC203839`; agents must not modify |
| LAB labeled | OK | serial `11231JEC207477` (iodé baseline) |
| LAB factory rescue doc + verified zip | OK | `docs/002-lab-stock-rescue.md`; host SHA-256 **VERIFY_OK** |
| Essential GOLDEN inventory + banking layout | OK | `docs/002-golden-inventory.md` (gaps = re-query; Yuh FAIL documented) |
| NeoBackup / wipe GOLDEN | N/A | Not a gate |
| FLAG_SECURE / call-record source | Deferred | T021 — re-query GOLDEN later; not blocking `003` vanilla |
| Offline AXP archive rsync | In progress | ~18G on host of ~38G laptop; not a flash gate |
**Handoff:** `002` P1 complete for `003` purposes → flash/build **LAB only**.
+108
View File
@@ -0,0 +1,108 @@
# GOLDEN inventory (AXP.OS oracle) — redacted
**Device:** GOLDEN sunfish (main phone) — **DO NOT FLASH / wipe / modify**.
**Serial:** `11071JEC203839`
**Role:** Live **AXP.OS Pro 22.2 / Android 15** compatibility oracle.
**Access:** Non-destructive `adb` only (sampled 2026-09-13).
**Raw artifacts:** `/srv/rom/references/axp/golden/2026-09-13/` on `rom-dev01`
(includes Magia DenyList screenshots, Yuh logcat; Git = this summary only).
## Identity (measured)
| Field | Value |
|-------|--------|
| Serial | `11071JEC203839` |
| Owner user | `Andres` (user 0) |
| `ro.build.display.id` | `BP1A.250505.005` |
| `ro.build.version.release` | `15` |
| `ro.build.version.security_patch` | `2026-03-03` |
| AXP / Lineage identity | `ro.lineage.version=22.2-2026.003-sunfish-pro` (`releasetype=PRO`) |
| Bootloader | **unlocked** (`flash.locked=0`, `verifiedbootstate=orange`) |
| Magisk | Binary **29.0** (`29000`) at `/system_ext/bin/magisk`; UI app named **Magia** |
## Users / work profile
| User | Name | Notes |
|------|------|-------|
| 0 | Andres | Owner |
| 10 | Island | Profile owner = `com.oasisfeng.island.fdroid` 6.4.2 |
After owner’s 2026-09-13 reclone experiment: `com.yuh` and
`com.swissquote.android` present on **both** user 0 and user 10
(Island reinstall of Yuh `firstInstallTime` user10 ≈ 21:48).
## Essential packages (measured)
| Area | Package / path | Version | Notes |
|------|----------------|---------|--------|
| microG GmsCore | `com.google.android.gms` | **0.3.15.250932** | System base MicrogGmsCore_GH; sig short `73aaa390` |
| Phonesky (AXP) | `/system/priv-app/Phonesky_AXP-OS` | **50.2.12-24** | Sig short `e05f779d` |
| GSF Proxy | `/system_ext/app/GsfProxy_GH` | `v0.1.0` | |
| Island | `com.oasisfeng.island.fdroid` | `6.4.2-d46911c9` | Spec text said “Insular”; measured = **Island** |
| Aurora Store | `com.aurora.store` | `4.8.4` | |
| Yuh | `com.yuh` | `1.45.11` (vc 535) | See attestation section |
| Swissquote | `com.swissquote.android` | `9.9.0` | Owner reports still OK |
| Aosmium (AXP) | `org.axpos.aosmium` | `148.0.7778.178` | Not on DenyList |
| Magisk modules list | `/data/adb/modules` | — | Not readable without root shell — **re-query** if needed |
## Magia DenyList (from UI screenshots 2026-09-13)
Checked packages (package-level; process expand not fully visible in shots):
| App | Package |
|-----|---------|
| BBVA Spain | `com.bbva.bbvacontigo` |
| BOOM | `com.boom.android.mobile2` |
| CieID | `it.ipzs.cieid` |
| Device configuration | `android.autoinstalls.config.google.nexus` |
| DKB | `com.dkbcodefactory.banking` |
| Identifica | `it.sielte.sielteidentify` |
| N26 | `de.number26.android` |
| Scalable Capital | `capital.scalable.droid` |
| Swissquote | `com.swissquote.android` |
| Webank | `com.opentecheng.android.webank` |
| Wise | `com.transferwise.android` |
| Yuh | `com.yuh` |
Visible unchecked (partial): Aegis, Amarok, Amaze, Aosmium.
Owner notes: most DenyList banking apps still pass; **Yuh fails** for ~weeks.
Extra Magia DenyList/host checks + Island uninstall/reclone did **not** fix Yuh.
## Compatibility / attestation (observational)
| Check | Status | Evidence |
|-------|--------|----------|
| Swissquote | **PASS** (owner report) | Still used; on DenyList |
| Other DenyList banks (N26, DKB, …) | **PASS** (owner: “casi todos”) | UI DenyList |
| Yuh | **FAIL** — hardware attestation / “certificate is invalid” | Owner UI + logcat ~21:51–21:52 |
| Play Integrity token path | Client path still runs | `Finsky requestIntegrityToken() finished for com.yuh`; DroidGuard `pia_attest_e1` for `com.yuh` |
| KeyMint / TEE | Errors concurrent with Yuh attempt | `keystore2`: `HARDWARE_TYPE_UNAVAILABLE` (IRPC / `TRUSTED_ENVIRONMENT`); `addRngEntropy` → `DEAD_OBJECT` / KeyMint `-1000` |
Interpretation for later `006`/`007` (not a GOLDEN fix): DenyList alone is
insufficient for Yuh’s hardware-attestation path; KeyMint/TEE remote-provisioning
errors correlate with the failure window. Project production root path remains
**KernelSU-Next (+ SusFS) or ReSukiSU evaluation** — Magisk/Magia is oracle-only.
Call recording: GOLDEN Secure dump shows native EXTENDROM
`extendrom_call_recording=1` (see `008` — port `er.sh` F_CALLREC, not Magisk
RRO). FLAG_SECURE override: not identified as AXP catalog on GOLDEN; project
`009` is native DO.
## Banking layout (oracle)
Historical target order (AXP workflow; Magisk DenyList → Island):
1. Aurora Store → install
2. Do not open
3. Magia DenyList (incl. processes) before first process
4. Clone into Island
5. First launch in Island
6. Phonesky/Play in Island as needed
Observed: Yuh+Swissquote intended in Island; after 2026-09-13 debug, both also
on owner user 0 — clean up is owner’s choice; agents must not uninstall.
## Policy
No flash, wipe, Magisk/Magia changes, or app uninstalls by agents — sample only.
+42
View File
@@ -0,0 +1,42 @@
# LAB baseline (iodé) — measured 2026-09-13
**Role:** LAB — second sunfish for flashing our ROM.
**Serial:** `11231JEC207477`
**As found:** iodé / Lineage `7.8-20260902-sunfish`, Android **16**, patch `2026-08-01`.
**Bootloader:** locked (`ro.boot.flash.locked=1`).
**Policy:** OK to flash/wipe for experiments after factory rescue is in place.
This is **not** the AXP GOLDEN oracle. Banking apps / Island / microG here are
LAB-side copies or leftovers — do not treat as AXP Pro parity evidence.
## Snapshot (text-only)
| Item | Value |
|------|--------|
| `ro.lineage.version` | `7.8-20260902-sunfish` |
| `ro.build.version.release` | `16` |
| microG GmsCore | `0.3.16.252432` |
| FakeStore / vending | `0.3.16.40226` |
| GsfProxy | `v0.1.0` |
| Island | `6.4.2-d46911c9` (user 10 `myisla`) |
| Magisk/KSU | not found |
Raw logs (moved/copied from early mislabel): host
`/srv/rom/references/axp/lab/2026-09-13-iodes-baseline/` when present; else
`/srv/rom/references/axp/golden/2026-09-13/` (same content — LAB).
Factory rescue: [`002-lab-stock-rescue.md`](002-lab-stock-rescue.md).
## Yuh on LAB (log peek 2026-09-13 ~22:15)
Launched on **user 0** (not Island). No Magisk. Bootloader **locked**.
Same core error as GOLDEN within ~4s of open:
`keystore2`: RKP/IRPC for `TRUSTED_ENVIRONMENT` → `Error::Km(HARDWARE_TYPE_UNAVAILABLE)`
Also: Yuh loads `libts.so`; SELinux denials probing `getenforce` / `ps` / `/cache` (root-detect style).
No Magisk DenyList involved on LAB → failure is **not** “only Magisk visible”.
Raw: host `…/lab/2026-09-13-iodes-baseline/yuh-2026-09-13/`.
+63
View File
@@ -0,0 +1,63 @@
# LAB stock factory rescue — Pixel 4a (`sunfish`)
**Scope: LAB only.** Never use these steps on GOLDEN.
## Source (measured 2026-09-13)
Google’s public factory listing for sunfish is pruned. The former CDN URL now
returns **HTTP 404**:
`https://dl.google.com/dl/android/aosp/sunfish-tq3a.230805.001.s1-factory-82902ea9.zip`
Canonical build identity (final Android 13 / TQ3A, widely cross-checked):
| Field | Value |
|-------|--------|
| Build | `TQ3A.230805.001.S1` |
| File | `sunfish-tq3a.230805.001.s1-factory-82902ea9.zip` |
| Size | `2276267484` bytes |
| SHA-256 | `82902ea98c80f1a22706279a30d2bc46aa55cbe60703c9a02ba043bd052c6d99` |
| Primary index | https://developers.google.com/android/images (sunfish may be empty) |
| Fallback (measured) | Internet Archive item [`sunfish-tq3a.230805.001.s1`](https://archive.org/details/sunfish-tq3a.230805.001.s1) |
| Cross-check | CalyxOS `vars/sunfish` cites the same URL/hash historically |
Host storage: `/srv/rom/references/axp/lab/`
Download log: `/srv/rom/references/axp/logs/lab-factory-download.txt`
## Before flashing
1. Confirm the USB phone is **LAB** (serial/label) — not GOLDEN.
2. Backup anything you care about on LAB (full wipe).
3. Unlock bootloader only if required and already accepted for LAB.
4. Verify zip SHA-256 before `flash-all`.
```bash
sha256sum -c <<<'82902ea98c80f1a22706279a30d2bc46aa55cbe60703c9a02ba043bd052c6d99 sunfish-tq3a.230805.001.s1-factory-82902ea9.zip'
```
## Flash (factory package)
From an unpacked factory directory on the **laptop** (USB to LAB):
```bash
# device in fastboot; verify serial matches LAB
fastboot devices
cd /path/to/sunfish-tq3a.230805.001.s1
./flash-all.sh # Linux; erases userdata
```
Windows: `flash-all.bat`.
`-w` is typically included in Google’s script (data wipe).
Alternative: https://flash.android.com only if that build remains selectable (LAB only).
## After flash
- Confirm LAB boots stock.
- Re-unlock / re-setup only as needed for later `003` LOS experiments.
- Do **not** relock unless you intend stock-only LAB.
## Gate
FR-002 / SC-002: this doc + **SHA-256-verified** zip on host are required before
experimental `003` flashes on LAB.
+93
View File
@@ -0,0 +1,93 @@
# 003 — Build notes (LOS 23.2 sunfish vanilla)
**Host:** `rom-dev01` as `android-build`
**Tree:** `/srv/rom/src/lineage`
**Artifacts:** `/srv/rom/artifacts/003-…`
**Logs:** `/srv/rom/logs/003-…`
**ccache:** `CCACHE_DIR=/srv/rom/ccache` (export in build shell)
## Parallelism — squeeze the HW
Guest is **8 vCPU / ~62 GiB**. Policy for this host:
| Job | Default | Notes |
|-----|---------|--------|
| `mka bacon` / Soong ninja | **`-j8`** | Match `nproc`; do not leave `-j4` idle |
| `repo sync` | `-j8` OK | Network-bound; `-j4` fine if flaky |
| OOM during link | try `-j6` | Only then; document why |
Do **not** pass `-j` to `brunch` (it eats extra args as variant). Use
`breakfast` + `mka bacon -j8`, or `/srv/rom/logs/003-build/run-brunch.sh`.
If `mpstat` shows high **%steal**, the Proxmox host is oversubscribed — fix
outside this repo (pin/dedicate vCPUs); guest governors usually unavailable.
## Manifest (wiki-verified 2026-09-13)
```bash
mkdir -p /srv/rom/src/lineage && cd /srv/rom/src/lineage
repo init -u https://github.com/LineageOS/android.git -b lineage-23.2 --git-lfs --no-clone-bundle
repo sync -c -j8
```
Primary: https://wiki.lineageos.org/devices/sunfish/build/
Branch `lineage-23.2` listed for sunfish.
## Build (after sync + proprietary blobs)
```bash
export CCACHE_DIR=/srv/rom/ccache
export USE_CCACHE=1
cd /srv/rom/src/lineage
source build/envsetup.sh
breakfast sunfish userdebug
# extract proprietary blobs: TheMuppets lineage-23.2 → vendor/google/sunfish
mka bacon -j8 # do NOT pass -j to brunch; wrapper: /srv/rom/logs/003-build/run-brunch.sh
```
Target expected: `lineage_sunfish-userdebug` (confirm after `breakfast`).
## Metadata to capture on success
- `repo manifest -r` → artifacts dir
- SHA-256 of zip/images
- compressed build log
- `-j` used, wall time, host `nproc`/RAM note
## Status
- [x] repo init (`/srv/rom/src/lineage`, `lineage-23.2`, 2026-09-13)
- [x] repo sync (`finished 2026-09-13T21:27:26Z exit=0`; ~180G; log `/srv/rom/logs/003-sync/sync.log`)
- [x] proprietary blobs policy documented (factory base + AXP/GOS diffs later)
- [x] TheMuppets `lineage-23.2` synced → `vendor/google/sunfish`
- [x] first successful build — `#### build completed successfully` /
`END … exit=0` (2026-09-14T13:52:46Z); j8 segment **06:45:43**
- [x] artifacts + `repo manifest -r` + SHA-256 + `brunch.log.gz` →
`/srv/rom/artifacts/003-vanilla/20260914-lineage-23.2-sunfish/`
(rsync → `.local-artifacts/…` on laptop)
- [x] flash LAB + smoke (`docs/003-flash-lab.md`) — done 2026-09-14;
gate → `004` (`docs/003-gate-handoff.md`)
## R7 note (vanilla `003`)
For the first vanilla build: **factory-equivalent blobs only**. AXP/Graphene
diffs land in later hardening/compat features — not in the clean baseline.
## Proprietary blobs (qué son)
Android open source **no** incluye todo lo que el Pixel necesita para radio,
cámara, firmware, etc. Esos binarios cerrados de Google/Qualcomm se llaman
**proprietary blobs** (o “vendor blobs”). Sin ellos, `brunch sunfish` no
arranca hardware real.
### Decision (owner 2026-09-13) — no overthink
| Layer | Approach |
|-------|----------|
| **Base blobs** | Factory Google / Lineage-equivalent extract (e.g. TheMuppets or `extract-files` from factory) — same hardware binaries stock uses |
| **AXP diffs** | Only sunfish-relevant proprietary/packaging diffs from AXP A15 reference |
| **GrapheneOS diffs** | Historical **sunfish A13** where still applicable; plus ideas from a **currently supported Pixel** when code is shared and still relevant (sunfish is EOL — do not assume every modern GOS change ports) |
Do **not** chase exotic blob sets for location quality; NLP/services matter more than swapping vendor GPS blobs.
+90
View File
@@ -0,0 +1,90 @@
# 003 — Flash LAB (vanilla LOS only)
**LAB serial:** `11231JEC207477`
**Never flash:** GOLDEN `11071JEC203839`
## Artifacts (this build)
Host: `/srv/rom/artifacts/003-vanilla/20260914-lineage-23.2-sunfish/`
Laptop: `.local-artifacts/003-vanilla/20260914-lineage-23.2-sunfish/` (rsynced)
| File | Role | SHA-256 (prefix) |
|------|------|------------------|
| `lineage-23.2-20260914-UNOFFICIAL-sunfish.zip` | sideload ROM | `f89edfe1…5852` |
| `boot.img` | Lineage recovery (flash to `boot`) | `aaf52d20…aad1` |
| `dtbo.img` | required before recovery | `a400e6ae…6327` |
| `vbmeta.img` | copied for reference (wiki path uses dtbo+boot) | `b3223b5d…0735` |
Full digests: `SHA256SUMS` in the same directory. Metadata: `build-metadata.txt`.
## Owner decisions (2026-09-13)
- **Unlock / wipe LAB:** allowed when needed for first custom flash.
- **Stock factory on LAB:** not desired as a planned OS (rescue zip stays on
disk for emergencies only).
- **Banking hard-gate focus:** **Yuh** (+ Swissquote); Revolut is **not** a
hard-gate.
- Vanilla `003` only — no microG / KSU / GApps add-ons on first flash.
## Wiki note (firmware)
Upstream LOS install wiki asks for **stock Android 13** firmware before first
install. LAB currently runs custom A16 (iodé). For this project we flash
vanilla LOS over LAB as approved; if radio/modem misbehaves after smoke, use
`docs/002-lab-stock-rescue.md` then re-flash — **never GOLDEN**.
## Before every fastboot/adb command
```bash
adb devices -l
fastboot devices
# serial must be 11231JEC207477 only
```
```bash
ART="$HOME/Workspaces/sunfish-a16-rom/.local-artifacts/003-vanilla/20260914-lineage-23.2-sunfish"
cd "$ART"
sha256sum -c SHA256SUMS
```
## Unlock (first custom flash)
Unlocking **wipes** LAB — owner-approved.
```bash
adb -s 11231JEC207477 reboot bootloader
fastboot -s 11231JEC207477 devices
fastboot -s 11231JEC207477 flashing unlock # confirm on device
```
## Flash (LOS wiki sunfish path)
```bash
cd "$ART"
fastboot -s 11231JEC207477 flash dtbo dtbo.img
fastboot -s 11231JEC207477 reboot bootloader
fastboot -s 11231JEC207477 flash boot boot.img
# device menu → Recovery
# Recovery → Factory reset → Format data
# Apply update → Apply from ADB
adb -s 11231JEC207477 sideload lineage-23.2-20260914-UNOFFICIAL-sunfish.zip
# Reboot system now (no GApps / no add-ons for vanilla 003)
```
Unofficial zip: recovery may warn about signature — expected; proceed only if
hashes match `SHA256SUMS`.
## After first boot
```bash
adb -s 11231JEC207477 shell getprop ro.lineage.version
adb -s 11231JEC207477 shell getprop ro.build.display.id
# expect lineage 23.2 / sunfish UNOFFICIAL — not AXP, not iodé
```
Then complete `docs/003-smoke-lab.md`.
## Failure
Use [`002-lab-stock-rescue.md`](002-lab-stock-rescue.md) only as **emergency**
brick recovery. Do not touch GOLDEN.
+33
View File
@@ -0,0 +1,33 @@
# 003 gate checklist (handoff → 004)
**Date:** 2026-09-14
**Device:** LAB `11231JEC207477` only (GOLDEN untouched)
## Naming (owner clarification)
- **LAB target ROM:** Android **16** / LineageOS **23.2**
- **GOLDEN:** AXP Pro on Android **15** — reference/read only
- Do **not** treat “A15” as a synonym for Android 16
## Build / flash
| Item | Status | Evidence |
|------|--------|----------|
| Vanilla LOS build | pass | `#### build completed successfully` / `exit=0` 2026-09-14T13:52:46Z |
| Artifacts + SHA-256 + manifest -r | pass | `/srv/rom/artifacts/003-vanilla/20260914-lineage-23.2-sunfish/` + laptop `.local-artifacts/…` |
| Unlock + flash LAB | pass | `unlocked: yes`; sideload `Total xfer: 1.00x` |
| Identity | pass | `ro.lineage.version=23.2-20260914-UNOFFICIAL-sunfish`; no iodé/AXP props |
| SELinux / encrypt | pass | Enforcing; `encrypted` |
| P1 hardware smoke | pass | adb + owner confirmation of main functions 2026-09-14 |
## Notes (non-blocking)
- Public **test-keys** banner — expected until `011`
- `verifiedbootstate=orange` / unlocked — expected
- Vendor security patch **2023-09-05** — EOL Pixel; document further in `013`
- Titan M `libnos_datagram` during recovery wipe — ignore; install status 0
- Platform ASB for this baseline: trust Lineage merges (`ro.build.version.security_patch=2026-09-01`); no separate CVE audit in `003`
## Gate
**`003` complete → proceed to `004` (microG + GSF Proxy + patched Phonesky) on LAB only.**
+67
View File
@@ -0,0 +1,67 @@
# 003 — LAB smoke checklist (vanilla LOS 23.2)
Device serial: `11231JEC207477`
Build: `lineage-23.2-20260914-UNOFFICIAL-sunfish`
Zip SHA-256: `f89edfe1fba7062eb8f8e3cea5012f209fefe587bf447cf4f83a7017812e5852`
Date: 2026-09-14
Status: `pass` / `fail` / `skip` + short evidence.
## Identity (adb 2026-09-14)
| Prop | Value |
|------|--------|
| `ro.lineage.version` | `23.2-20260914-UNOFFICIAL-sunfish` |
| `ro.build.flavor` | `lineage_sunfish-userdebug` |
| `ro.build.display.id` | `BP4A.251205.006 test-keys` |
| Slot | `_a` (install was on `b`; post-update active `_a`) |
| Prior ROM props | none (no iodé / AXP / microG) |
## P1
| Item | Status | Notes |
|------|--------|-------|
| Boots to UI | pass | owner + adb up ~7 min |
| Encryption / PIN | pass | `ro.crypto.state=encrypted` (set PIN in setup) |
| SELinux Enforcing | pass | `getenforce` → Enforcing |
| Android security patch | pass | `2026-09-01` |
| Vendor security patch / baseline | pass | `2023-09-05` (EOL Pixel vendor train — expected) |
| SIM detection | pass | `gsm.sim.state=LOADED`, operator Movistar |
| Mobile data | pass | owner 2026-09-14 — main functions OK |
| Phone calls | pass | owner 2026-09-14 |
| SMS | pass | owner 2026-09-14 |
| Wi-Fi | pass | connected `Pixel_1216` |
| Bluetooth | pass | owner 2026-09-14 |
| GPS | pass | owner 2026-09-14 |
| Camera(s) | pass | owner 2026-09-14 |
| Audio (speaker/mic) | pass | owner 2026-09-14 |
| NFC | pass | owner 2026-09-14 |
| Sensors (basic) | pass | accel/gyro/mag/light/prox listed + active |
| Fingerprint | pass | owner 2026-09-14 |
| USB data / ADB | pass | adb device authorized |
| Charging | pass | USB powered, level 99 |
## Extended
| Item | Status | Notes |
|------|--------|-------|
| Airplane mode | skip | not run |
| Flashlight | _user_ | |
| Vibration | _user_ | |
| Screen rotation | _user_ | |
| USB OTG | skip | |
| Headphone jack | n/a | sunfish has none |
| WebView basic page | pass | `com.android.webview` present (`/product/app/webview`) |
| Sideload non-Google APK | skip | |
| Widevine / DRM level | skip | check later if needed |
| eSIM state | skip | |
| WireGuard kernel baseline | skip | |
| Bootloader version | pass | `s5-0.5-10252351` |
| Baseband / modem | pass | `g7150-00112-230505-B-10075601` |
| AVB / lock state | note | `verifiedbootstate=orange`, `flash.locked=0` (unlocked — expected) |
| Public test-keys banner | note | expected for UNOFFICIAL userdebug until `011` |
## Gate to `004`
P1 blockers unresolved? **no** — owner confirmed main functions 2026-09-14.
Handoff: `docs/003-gate-handoff.md`. Proceed to `004`.
+105
View File
@@ -0,0 +1,105 @@
# 004 / 005 — LAB microG Self-Check
Device: LAB `11231JEC207477` (never GOLDEN)
Build: `23.2-20260915-UNOFFICIAL-sunfish`
Date: 2026-09-15
Evidence: `/srv/rom/artifacts/005-spoofing/20260915-us6-validation/LAB-US6.txt`
## Packages
| Package | `pm path` | Notes |
|---------|-----------|-------|
| `com.google.android.gms` | `/system_ext/app/GmsCore/GmsCore.apk` | PRESIGNED; from zip |
| `com.google.android.gsf` | `/system_ext/app/GsfProxy/GsfProxy.apk` | PRESIGNED; from zip |
| `com.android.vending` | system priv-app from zip; LAB currently `/data/app/…` after exact-alarm `adb install -r` | Next bacon restores `/system/priv-app/Phonesky/` |
| FakeStore | absent | `pm list packages` — no FakeStore |
## Spoofing (`005`)
| Check | Result |
|-------|--------|
| `settings get secure allow_signature_fake` | `1` |
| `FAKE_PACKAGE_SIGNATURE` user 0 gms + vending | `granted=true` |
| `FAKE_PACKAGE_SIGNATURE` user 10 (Island) gms + vending | `granted=true` |
| Self-Check correct signature (gms + Play) | pass (operator) |
| Magisk / KSU / `su` | absent |
## Self-Check
| Item | pass/fail | Notes |
|------|-----------|-------|
| Package / ROM signature rows | pass | after spoof grant |
| Permission rows incl. spoof | pass | Developer Options toggle + grant |
| Play companion / Phonesky sig | pass | project JKS allow-listed in `005` |
## Play Store
| Check | Result | Notes |
|-------|--------|-------|
| Play opens | pass | |
| Login | pass | no `UNREGISTERED_ON_API_CONSOLE` in logcat |
| Install app (owner + work) | pass | after exact-alarm Phonesky (`USE_EXACT_ALARM`) |
| Version | `52.9.22-24` | |
| In-app Play self-update | may fail | AXP-like; acceptable |
## Modern location (BeaconDB / Positon)
Mozilla Location Service backends are dead. Prefer an Ichnaea-compatible URL:
**BeaconDB** (`https://api.beacondb.net`) or **Positon** (`https://api.positon.xyz/?key=…`).
Do **not** reintroduce UnifiedNlp Déjà Vu / Nominatim backend APKs on 0.3.x.
1. Open **microG Settings → Location**.
2. Enable network / Wi‑Fi / cell location as shown in Self-Check.
3. Enable the in-GmsCore toggles that still exist in 0.3.x (e.g. remember from
GPS, request from online service / hotspot) — these are **not** the old
UnifiedNlp plugin list.
4. Set (or confirm) the service URL (BeaconDB or Positon).
5. Do **not** install UnifiedNlp backend APKs from F-Droid.
| Check | Result |
|-------|--------|
| BeaconDB / Positon URL documented | yes |
| UnifiedNlp backends required | no |
### LAB observation (follow-up — 2026-09-16) — **fixed (prebuilt)**
Root cause: staged GmsCore was official **`*-hw`** (`versionName=…-hw`). That
flavor omits `NetworkLocationService` /
`NetworkLocationProviderService` from the manifest, so Location UI hides
Wi‑Fi/cell NLP (only Timeline-style bits remain). **GOLDEN** / abu ship the
**default** flavor (`versionName` without `-hw`) and show the full Location
options.
**Fix (host staged 2026-09-16):** replace with
`com.google.android.gms-252432032.apk` → `versionName=0.3.16.252432` (non-hw),
synced to `vendor/sunfish-a16/prebuilt/GmsCore.apk`. Quick LAB check without
bacon: `adb install -r` the default APK (same microG signer), then:
```bash
adb shell dumpsys package com.google.android.gms | grep -iE 'NetworkLocationProvider|NetworkLocationService'
adb shell dumpsys package com.google.android.gms | grep versionName=
# expect versionName=0.3.16.252432 (no -hw) + both NetworkLocation* components
```
Permanent: next `bacon` picks up the new prebuilt (~12.5 MiB larger than hw).
Positon remains a valid URL if preferred over BeaconDB.
After `adb install -r` of the default APK, `dumpsys` may still list both
`versionName=0.3.16.252432` and `…-hw` until the next full image flash replaces
`/system_ext/app/GmsCore` (data overlay + old system base). NLP services already
come from the default update.
## microG F-Droid updates
| Check | Result | Notes |
|-------|--------|-------|
| Add [microG F-Droid repo](https://microg.org/fdroid/repo) | optional | one-time |
| F-Droid sees GmsCore update (when newer) | expected | official `PRESIGNED` on system_ext |
| Do **not** install Companion/FakeStore from F-Droid | — | conflicts with Phonesky |
## AXP layout sanity
| Check | Result | Notes |
|-------|--------|-------|
| Phonesky | priv-app in image | LAB data path only after hotfix upgrade |
| GmsCore / GsfProxy | `/system_ext/…` | matches AXP GOLDEN layout intent |
+65
View File
@@ -0,0 +1,65 @@
# 004 — Phonesky / microG runbook (rom-dev01)
Proprietary Google/Play APKs **never** go in Git. Binaries live on `rom-dev01`
under `/srv/rom/prebuilts/`.
## Owner policy (2026-09-15)
- **Copy AXP’s packaging scheme** (GOLDEN layout), modernize versions (LOS 23.2 /
A16, latest microG + patched latest Play).
- Root later = **KSU-Next + SusFS** (`007`), not Magisk — do not design `004`
around Magisk modules.
- Prefer AXP behaviour over ad-hoc shortcuts (no “everything on product”, no
ABI-strip that forces re-signing official microG).
## AXP GOLDEN layout (mirror this)
| Package | Partition | Privileged? |
|---------|-----------|-------------|
| Phonesky | `system` priv-app | yes |
| GmsCore | `system_ext` app | no (AXP: `MicrogGmsCore_GH_presigned`) |
| GsfProxy | `system_ext` app | no |
Host packaging: `vendor/sunfish-a16/{Android.mk,gms.mk,BoardConfig.mk}`.
## Layout on host
| Path | Contents |
|------|----------|
| `/srv/rom/prebuilts/microg/` | official `GmsCore.apk`, `GsfProxy.apk`, `VERSIONS.txt` |
| `/srv/rom/prebuilts/phonesky/` | patched `Phonesky.apk`, XMLs, `VERSIONS.txt`, `upstream/` |
| `/srv/rom/secrets/phonesky-signing/` | project JKS for Phonesky |
| `/srv/rom/tools/phonesky/` | AXP `packages_apps_phonesky` worktree + tools |
## Stage microG (latest official — F-Droid-updatable)
1. Download latest **default** `com.google.android.gms-NNNNNNN.apk` (no `-hw`) +
`GsfProxy.apk` from microG GitHub releases. Match GOLDEN: `versionName`
must **not** end in `-hw` (Huawei flavor drops NLP Location services /
UI). Example: `v0.3.16.252432` → `com.google.android.gms-252432032.apk`.
2. Keep **official signature** (`LOCAL_CERTIFICATE := PRESIGNED`). **Never**
re-sign / ABI-strip GmsCore for size — that breaks F-Droid updates.
3. Do **not** install microG’s `com.android.vending-*.apk` (FakeStore).
## Stage Phonesky (patch latest Play — AXP procedure)
1. Tools: apktool, aapt, zipalign, `apksigner` via `java -jar …/apksigner.jar`.
2. Project keystore once under `/srv/rom/secrets/phonesky-signing/`.
3. `apkmd` + AXP `apkmirror.json` → `/srv/rom/prebuilts/phonesky/upstream/`.
4. Checkout AXP `packages_apps_phonesky`; run `DEBUG=yes ANDROIDSDK=35 ./patch-playstore …`
(host adaptations: apktool 2.12 build without `-t`; **zipalign then sign**).
5. Stage fat patched APK as system priv-app + AXP `etc/` XMLs to **system**.
## Partition headroom
If `check_partition_sizes` fails after adding the AXP-sized set, raise
`BOARD_SUPER_PARTITION_ERROR_LIMIT` in `vendor/sunfish-a16/BoardConfig.mk`
(keep under `BOARD_GOOGLE_DYNAMIC_PARTITIONS_SIZE`). Do **not** “fix” by
destroying the microG signer.
## Signing (FR-010)
| Package | Signer | Updates |
|---------|--------|---------|
| GmsCore / GsfProxy | Official microG | F-Droid microG repo |
| Phonesky | Project JKS | Same-signer patched APKs |
+92
View File
@@ -0,0 +1,92 @@
# 005 — Restricted signature spoofing runbook
**Feature**: `005-restricted-signature-spoofing`
**LAB only**: serial `11231JEC207477` — never GOLDEN
**Host**: `rom-dev01` under `/srv/rom/`
## What we discovered (2026-09-15)
LineageOS 23.2 **already** ships restricted microG spoofing
(`frameworks/base` commit `23c0a5c19844`, LuK1337):
| Gate | Stock LOS behaviour |
|------|---------------------|
| Build | `userdebug`/`eng` only (`isDebuggable()` JNI) |
| Packages | `com.google.android.gms`, `com.android.vending` only |
| Signer | Must match **microG release** cert (`MICROG_REAL_SIGNATURE`) |
| Fake value | Must equal platform Google cert (`MICROG_FAKE_SIGNATURE`) |
| Surfaces | Spoofs both `PackageInfo.signatures` and `signingInfo` |
Gaps vs our stack / AXP UX:
1. **Our Phonesky** is signed with the **project JKS**, not microG — stock
gate rejected it → Play `UNREGISTERED_ON_API_CONSOLE`.
2. No `FAKE_PACKAGE_SIGNATURE` permission → microG Self-Check permission rows
empty.
3. No Developer Options master switch (`allow_signature_fake`).
## What we applied
Script (idempotent, marker `SUNFISH_A16_005_SIGSPOOF`):
- Meta: `vendor/sunfish-a16/patches/005-sigspoof/apply_005_sigspoof.py`
- Host copy: `/srv/rom/src/lineage/vendor/sunfish-a16/patches/005-sigspoof/`
- References: `/srv/rom/references/extendrom` @ `f129253c…`
- Cert hex: `/srv/rom/artifacts/005-spoofing/phonesky-cert.hex`
(SHA-256 `31:31:86:CB:…:BD:56`)
Changes on the LOS tree:
1. Extend `isMicrogSigned()` to accept **project Phonesky** signer for
`com.android.vending`.
2. Gate spoofing on `Settings.Secure.allow_signature_fake` (default **0**).
3. Require grant of `android.permission.FAKE_PACKAGE_SIGNATURE`.
4. Port EXTENDROM permission + Settings Developer Options toggle + strings.
5. Keep packaging leftovers: `LOCAL_REPLACE_PREBUILT_APK_INSTALLED` + Play FGS
privapp XML (already in `vendor/sunfish-a16/`).
**LAB 2026-09-15 post-flash:** Self-Check / spoofing / no
`UNREGISTERED_ON_API_CONSOLE` OK. Play install failures / ANR: `DownloadService`
/`VpaService` FGS `systemExempted` needs **any of** `SCHEDULE_EXACT_ALARM` /
`USE_EXACT_ALARM` / VPN — privapp alone is not enough if the APK does not
**request** those permissions.
**Fix (2026-09-15):** Phonesky rebuild adds `USE_EXACT_ALARM` +
`SCHEDULE_EXACT_ALARM` to the manifest; artifact
`/srv/rom/artifacts/005-spoofing/Phonesky_exact-alarm.apk`; pushed to LAB via
`adb install -r` (same project JKS); vendor prebuilt updated under
`/srv/rom/src/lineage/vendor/sunfish-a16/prebuilt/Phonesky.apk` for next bacon.
`USE_EXACT_ALARM: granted=true` on LAB after upgrade.
Re-apply after `repo sync` that resets frameworks/Settings:
```bash
python3 /srv/rom/src/lineage/vendor/sunfish-a16/patches/005-sigspoof/apply_005_sigspoof.py
```
Backups: `/srv/rom/artifacts/005-spoofing/bak-snapshots/` (never leave
`*.005.bak` under `res/` — aapt2 will fail).
## LAB validation (US6 — 2026-09-15)
Accepted on LAB `11231JEC207477` / build `23.2-20260915-UNOFFICIAL-sunfish`.
Evidence: `/srv/rom/artifacts/005-spoofing/20260915-us6-validation/LAB-US6.txt`.
1. ~~Flash LAB; `pm path` gms / gsf / vending~~ — pass
2. ~~Allow signature spoofing~~ — `allow_signature_fake=1`
3. ~~Self-Check correct signature~~ — pass (grant on user 0 + Island 10)
4. ~~Play sign-in / installs~~ — pass after exact-alarm Phonesky; no `UNREGISTERED_ON_API_CONSOLE`
5. Location: configure BeaconDB `https://api.beacondb.net` (no UnifiedNlp) — steps in
`docs/004-lab-selfcheck.md` + `quickstart.md`
```bash
adb -s 11231JEC207477 shell settings get secure allow_signature_fake
# expect 1 when enabled
```
**Handoff:** first stack done → next feature **`006` banking rootless**. Do **not**
add KernelSU/`007` until after `006`.
## Out of scope
- Banking apps (`006`), KSU/root (`007`), Magisk/LSPosed.
+80
View File
@@ -0,0 +1,80 @@
# abu (KernelSU-Next) vs ours (ReSukiSU) — SusFS integration checklist
> **Historical hypothesis checklist:** The [2026-09-16 source audit](007-susfs-source-audit.md)
> supersedes causal claims here. ReSukiSU intentionally supports SUSFS as an
> alternative inline hook provider; forcing MANUAL+SUSFS is a local porting
> choice, not an established universal fix. Check module ABI and command wire
> formats as well as Kconfig. The reference checkout is not proven identical
> to the owner's working Abu L22 binary.
Oracle: `/srv/rom/references/abu-msm-4.14-ksu` (`drivers/kernelsu` → `KernelSU-Next/kernel`).
Ours: ReSukiSU under `msm-4.14/drivers/kernelsu` + LOS 23.2 A16.
**Do not copy abu ROM/boot**; copy *model* only. Forks differ (Next ≠ ReSukiSU).
## Architectural model (abu — correct)
| Topic | abu KernelSU-Next | ReSukiSU upstream (broken for us) | Ours after patches |
|-------|-------------------|-------------------------------------|--------------------|
| SusFS | **Addon** `bool` under `menu "KernelSU - SUSFS"`, `depends on KSU` | Was a **choice** entry XOR with MANUAL/TRACEPOINT | Kconfig: SusFS outside choice (abu-like) |
| Hooks | `KSU_KPROBES_HOOK` default y; or manual (disable kprobes) | `choice` MANUAL / TRACEPOINT / (was SUSFS) | MANUAL_HOOK + AUTO_* for 4.14 |
| `susfs_init()` | `#ifdef CONFIG_KSU_SUSFS` always in `ksu.c`, **independent** of hook method | Often behind `#elif CONFIG_KSU_SUSFS` only | Fixed `init.c`: MANUAL branch also calls `susfs_init()` |
| Hook enable keys | N/A (different code) | `#elif SUSFS` before `#elif MANUAL` → no `ksu_init_rc_hook` when both | Fixed `ksud_integration.c`: SusFS-only branch if `!MANUAL_HOOK` |
## Failures we already hit (XOR leftovers)
1. Defconfig SusFS on → choice flipped → **no MANUAL_HOOK** → vibrator hang / broken hooks (`T004038Z`).
2. MANUAL+SusFS without **AUTO_*** → early logo reboot (`T091211Z`).
3. AUTO_* + SusFS → link error `ksu_init_rc_hook` (SUSFS `#elif` won).
4. Even after link fix: without `susfs_init()` in MANUAL path, SusFS would be dead code.
## Remaining `#elif CONFIG_KSU_SUSFS` in our tree (review)
| File | Pattern | Risk when MANUAL+SUSFS | Action |
|------|---------|------------------------|--------|
| `runtime/ksud_integration.c` | SusFS iff `!MANUAL` | OK | done |
| `core/init.c` | MANUAL then `ifdef SUSFS` init; elif SUSFS legacy | OK | done |
| `feature/sucompat.h` | `#elif SUSFS` before else MANUAL macros | Uses SusFS TIF/helpers — **intended** for addon | leave; verify LAB |
| `supercall/dispatch.c:527` | mark get: SUSFS then else | SusFS path when addon — OK | leave |
| `supercall/dispatch.c:902` | MANUAL before SUSFS for hook_type string | Reports "Manual" when both — OK | leave |
| `feature/sucompat.c` | `SUSFS \|\| MANUAL` | OK compose | leave |
**Rule:** any `#elif CONFIG_KSU_SUSFS` that **defines symbols or inits** needed by MANUAL/AUTO must become either `&& !MANUAL` or `#ifdef SUSFS` *nested inside* MANUAL (abu: always `#ifdef SUSFS`).
## Feature flag delta (Kconfig)
abu Next has extras we lack (Next-specific / different SusFS port):
- `KSU_SUSFS_HAS_MAGIC_MOUNT`
- `KSU_SUSFS_AUTO_ADD_SUS_*` / `TRY_UMOUNT*`
- `KSU_SUSFS_SUS_SU` (needs KPROBES)
Our ReSukiSU SusFS set: PATH/MOUNT/KSTAT/LOG/HIDE_SYMBOLS/SPOOF_CMDLINE/OPEN_REDIRECT/SUS_MAP (+ SPOOF_UNAME optional).
Bring-up: keep **SPOOF_UNAME off** (already commented in defconfig); consider SPOOF_CMDLINE off until boot stable.
abu `sunfish_defconfig` in this reference has **0** `CONFIG_KSU*` lines — enablement is elsewhere / not this file; do not treat that defconfig as SusFS pin list.
## Hook stack (do not mix blindly)
| | abu | ours |
|--|-----|------|
| Primary | KPROBES (`CONFIG_KSU_KPROBES_HOOK`) | MANUAL + AUTO_SETUID/INITRC/INPUT |
| Non-GKI docs | kernelsu.org manual integrate | resukisu.org manual integrate |
Porting abu **kprobe** assumptions into ReSukiSU MANUAL builds = future footgun.
## Pre-build gate (mandatory)
Before each SusFS `bootimage`:
```bash
CFG=out/target/product/sunfish/obj/KERNEL_OBJ/.config
grep -E "CONFIG_KSU_MANUAL_HOOK=y|CONFIG_KSU_MANUAL_HOOK_AUTO_.*=y|CONFIG_KSU_SUSFS=y" "$CFG"
# must have MANUAL=y, all three AUTO=y, SUSFS=y
# must NOT have "# CONFIG_KSU_MANUAL_HOOK is not set"
nm …/ksud_integration.o | grep ksu_init_rc_hook # after CC
```
After Image: extract-ikconfig and confirm same.
## Flash discipline
Flash SusFS to one slot only; keep penúltimo on the other; `--set-active`; both-slot rollback after failure.
+60
View File
@@ -0,0 +1,60 @@
# Debug boot — freeze on panic (photo / dump)
Normal sunfish kernel: `CONFIG_PANIC_TIMEOUT=5` + often
`CONFIG_QCOM_FORCE_WDOG_BITE_ON_PANIC` → reboot in seconds. Console is
**UART only** (`console=ttyMSM0`); `# CONFIG_VT is not set` → **no Oops text
on the phone display** (you only see the Google logo loop).
## What a “debug boot” must include (owner)
For a useful photo, **both** are required — freeze alone is not enough:
1. **Freeze:** `CONFIG_PANIC_TIMEOUT=0` + cmdline `panic=0`; disable
`CONFIG_QCOM_FORCE_WDOG_BITE_ON_PANIC` so it does not reboot in 5s.
2. **On-screen console:** `CONFIG_VT` + `CONFIG_FRAMEBUFFER_CONSOLE` (+
`console=tty0` in cmdline). Without this, you only see the Google logo.
| Goal | Mechanism | Photo-able? |
|------|-----------|-------------|
| Stop reboot loop | `panic=0` / `PANIC_TIMEOUT=0`, no wdog-bite-on-panic | Logo freezes only |
| Oops on screen | VT + FRAMEBUFFER_CONSOLE + `console=tty0` | Yes if panic after FB; early may still be logo |
| Dump after hang | Freeze + penúltimo + immediate `klog`/`logfs` dd | Not a photo |
Use **only on LAB**, SusFS/debug trials. Never ship as daily boot.
Next SusFS debug build after the sigfix trial must enable **both**.
## Host recipe (LAB SusFS debug boot)
1. Defconfig fragment (or temporary edits) on `rom-dev01`:
```text
CONFIG_PANIC_TIMEOUT=0
# CONFIG_QCOM_FORCE_WDOG_BITE_ON_PANIC is not set
# optional on-screen console (experimental on Pixel 4.14):
CONFIG_VT=y
CONFIG_VT_CONSOLE=y
CONFIG_FRAMEBUFFER_CONSOLE=y
CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY=y
```
2. Cmdline (device tree or unpack/repack boot): add `panic=0` and optionally
`console=tty0`. Prefer a **vendor_sunfish / BoardConfig** override gated by
a make var (e.g. `SUNFISH_KERNEL_PANIC_DEBUG=1`) so normal bacon stays clean.
3. Build `bootimage`, name artifact
`boot-resukisu-susfs-DEBUG-YYYYMMDDTHHMMSSZ.img`.
4. Flash only to `boot_b`; keep penúltimo on `boot_a`. On hang: photo if text
appears; Vol−+Power → `--set-active=a`.
## Capture order (always)
1. Panic / hang on SusFS (debug) slot
2. Fastboot **before** Android success on good slot
3. Then penúltimo + immediate `dd` of `klog`/`logfs` (4a has no `fastboot fetch`)
## Status
Documented 2026-09-16 after SusFS early `kernel_panic` with empty Oops in klog.
Sigfix rebuild may already address root cause; keep this recipe for the next
opaque bootloop.
+198
View File
@@ -0,0 +1,198 @@
# 007 — KernelSU-Next on sunfish (LAB runbook)
**LAB only**: serial `11231JEC207477` — never GOLDEN `11071JEC203839`
**Host**: `rom-dev01` — artifacts `/srv/rom/artifacts/007-kernelsu/`, logs
`/srv/rom/logs/007-build/`
**Managed profile**: **Island** (current LAB/GOLDEN vehicle; historical
“Insular” docs are workflow reference only)
## Latest LAB results
**2026-09-16:** SusFS DEBUG bring-up **PASS** (matched vendor); banks still
**FAIL** (Talsec `-69` / root lock). Full write-up:
[`docs/007-lab-results-20260916.md`](007-lab-results-20260916.md).
## Hard-gates
| Gate | Rule |
|------|------|
| Yuh (`com.yuh`) | **PASS** usable session in Island on the tested stack |
| Swissquote (`com.swissquote.android`) | **PASS** usable session in Island on the **same** stack |
| Root | Banks **never** receive root; hide/umount **verified** (not toggle-only) |
| Magisk | Not production |
If either bank fails after SusFS/attestation experiments → **cancel root**
(Phase 9): keep incomplete / withdraw KSU; pivot to native `008`/`009` + optional
stock phone. Do **not** ship “root without both banks”.
## Sequencing (constitution 0.2.1)
- `006` rootless PASS gate is **deferred**.
- Before first rooted flash: **T039** rootless snapshot of *this* LAB ROM
(`pre-root/`). FAIL allowed; record facts.
- Then KSU → verified hide → SusFS → attestation → both-bank PASS.
- **Pins:** always refresh to newest compatible ReSukiSU/manager (non-shallow);
record tag + `KSU_VERSION` before `mka`. Never rebuild from a stale host copy.
## `002` KeyMint / Yuh evidence (index)
| Source | What | Path / doc |
|--------|------|------------|
| GOLDEN AXP+Magisk | Yuh FAIL — HA / “certificate invalid”; KeyMint `HARDWARE_TYPE_UNAVAILABLE` | `docs/002-golden-inventory.md` §Compatibility; host refs under `/srv/rom/references/axp/golden/…` when present |
| LAB iodé A16 (no Magisk, BL locked) | Same KeyMint/RKP class on Yuh open | `docs/002-lab-iodes-baseline.md`; host `…/lab/2026-09-13-iodes-baseline/yuh-…` |
| Contracts | Hide + both-bank gate | `specs/007-kernelsu-sunfish/contracts/` |
## Manager UI is **not** in the ROM zip
Kernel root (KSU-Next / ReSukiSU) does **not** ship a visible Settings tile.
After flash: install the matching **spoofed manager APK** (version ≤ kernel
`KSU_VERSION`), then modules (TEESimulator-RS / Zygisk / PIF, etc.). Same on
torukk’s MicroG zip — ResukiSU/KSU is kernel-only until the manager is installed
(see XDA #41; our pin: `.local-artifacts/007-kernelsu/ReSukiSU_*spoofed_*.apk`).
## Host policy
**All heavy work on `rom-dev01` only** (kernel clone, integrate, `mka`, image
staging). Laptop = thin client (adb/fastboot, Spec Kit, docs). Do not run kernel
builds or multi‑GB clones on the laptop.
**Long jobs must be detached on the host** — never leave `mka`/`ninja` on a
laptop SSH pipe (Wi‑Fi drops kill the build). Prefer **tmux** via
`/srv/rom/artifacts/bin/rom-detach` (falls back to screen / nohup):
```bash
/srv/rom/artifacts/bin/rom-detach 007-boot -- bash -lc \
'cd /srv/rom/src/lineage && source build/envsetup.sh && lunch lineage_sunfish-bp4a-userdebug && mka bootimage -j$(nproc)'
cat /srv/rom/artifacts/detach-007-boot.status
tmux ls # attach: tmux attach -t 007-boot
```
Agents: start detached, then only poll `detach-*.status` / logs with short SSH.
| Path on host | Role |
|--------------|------|
| `/srv/rom/src/lineage/kernel/google/msm-4.14` | Our LOS kernel (integrate here) |
| `/srv/rom/src/lineage/kernel/google/msm-4.14/KernelSU-Next` | setup.sh checkout @ `a54e4fa…` |
| `/srv/rom/references/kernelsu-next/` | KernelSU-Next `legacy` mirror (setup source) |
| `/srv/rom/references/abu-msm-4.14-ksu/` | abu-adm `ksu-susfs-23.0` **reference only** |
| `/srv/rom/artifacts/007-kernelsu/abu-reference/` | Owner-supplied abu `boot-abu.img` + `dtbo-abu.img` + SHA256 |
| `/srv/rom/artifacts/007-kernelsu/` | Our outs / evidence |
| `/srv/rom/logs/007-build/` | Build logs |
### Integration status (2026-09-16)
- **Production root fork for LAB:** **ReSukiSU** (symlink `drivers/kernelsu` →
`/srv/rom/references/resukisu/kernel`, full non-shallow clone). KernelSU-Next
remains historical learning only.
- Manual hooks (`CONFIG_KSU_MANUAL_HOOK`): `fs/open.c`, `fs/exec.c`, `fs/stat.c`,
`fs/read_write.c`, `drivers/input/input.c`, `kernel/sys.c` (`setresuid`),
`kernel/reboot.c`
- `sunfish_defconfig`: `CONFIG_KSU=y`, `CONFIG_KSU_MANUAL_HOOK=y`, all three
`AUTO_*` hooks **on** for 4.14, SusFS options as addon (not XOR). See
`docs/007-abu-vs-ours-susfs.md`.
- **Our boots (use these — not Downloads):**
| Role | Artifact |
|------|----------|
| Penúltimo (ReSukiSU, no SusFS) | `boot-resukisu-our-20260915T183729Z.img` / `boot-resukisu-our-latest.img` |
| Último (ReSukiSU + SusFS) | `boot-resukisu-susfs-20260916T004038Z.img` / `…-susfs-latest.img` (sha `455a77a6…`) |
- SusFS: **built** (API bridge 4.14 ↔ ReSukiSU ioctl). **Not claimed** on LAB —
trial only; US2 hide not finished before build. See
`docs/session-20260916-handoff.md`.
## Community / abu — **reference archive only**
Same problem space (sunfish + microG + hide + banks). **Not** our flash path.
Do not install abu/torukk ROMs or `~/Downloads/boot-sunfish-resukisu*.img` as
“our” kernel.
| Topic | Useful reference | Our action |
|-------|------------------|------------|
| Root fork | ReSukiSU for hard banks (XDA) | We already build **our** ReSukiSU boot |
| Attestation | TEESimulator-RS | Prefer over TrickyStore when modules resume |
| Modules | Zygisk Next + PIF inject | Documented exception if bank gate needs it |
| SusFS / kernel | abu `ksu-susfs` trees, `abu-reference/` imgs | Diff / port ideas only |
| Their full ROM | torukk 22.2 MicroG / abu A15 | **Ignore for day-to-day** — restore always = our 23.2 bacon |
Host refs: `/srv/rom/references/abu-msm-4.14-ksu/`,
`/srv/rom/artifacts/007-kernelsu/abu-reference/` (`boot-abu` / `dtbo-abu`).
```bash
# LAB — our penúltimo (preferred after ROM sideload)
# Always flash BOTH slots after a failed SusFS/bootloop trial — bootloader may
# mark the other slot unbootable and leave current-slot on the bad one.
fastboot flash boot_a .local-artifacts/007-kernelsu/boot-resukisu-our-20260915T183729Z.img
fastboot flash boot_b .local-artifacts/007-kernelsu/boot-resukisu-our-20260915T183729Z.img
fastboot --set-active=a # or the slot that last ran our 23.2 bacon successfully
fastboot reboot
# host: /srv/rom/artifacts/007-kernelsu/boot-resukisu-our-20260915T183729Z.img
# Emergency (no KSU): bacon stock boot
# fastboot flash boot_a|boot_b .local-artifacts/007-kernelsu/boot-005-known-good.img
```
**Slot lesson (2026-09-16):** SusFS bootloop left `current-slot:b` and
`slot-unbootable:a:yes`. Flashing only `boot` (→ active slot) could not revive
LAB; `boot-005` on **both** slots + `--set-active=a` recovered. Before any
kernel trial: `fastboot getvar current-slot` / `slot-unbootable:*`. After
failure: flash rollback to **boot_a and boot_b**, then set the known-good slot.
## Pin table (2026-09-16)
| Component | Revision | Status | Notes |
|-----------|----------|--------|-------|
| ReSukiSU | host `/srv/rom/references/resukisu` (non-shallow; match manager) | **active** | record tag + `KSU_VERSION` before each `mka` |
| KernelSU-Next | `a54e4fa…` (`legacy`) | archived | learning only |
| LOS sunfish kernel | `28f9290ae067…` + dirty SusFS/hooks | tree | `msm-4.14` |
| SusFS | bridged in-tree (trial) | **rebuild** | `T004038Z` hung (no MANUAL_HOOK); `T091211Z` logo-reboot (MANUAL+SUSFS but **AUTO_* hooks off**); next: AUTO_*=y + SusFS |
| Attestation stack | — | pending | TEESimulator-RS first candidate |
## Kernel inventory (T005)
| Item | Value |
|------|--------|
| Source | `/srv/rom/src/lineage/kernel/google/msm-4.14` |
| Device makefile | `device/google/sunfish/BoardConfigLineage.mk` → `TARGET_KERNEL_SOURCE := kernel/google/msm-4.14` |
| Defconfig | `arch/arm64/configs/sunfish_defconfig` |
| Boot image name | `Image.lz4` |
## KernelSU-Next choice (T006–T007) — amended by community 2026-08
**Earlier decision**: KernelSU-Next `legacy` + manual hooks for first in-tree POC.
**Amended (owner + torukk XDA 2026-08-22)**: For **hard-bank** detection
(Revolut-class; our gate is Yuh/Swissquote), community evidence favors
**ReSukiSU** + **TEESimulator-RS** (+ Zygisk Next + PIF inject) over Next+TrickyStore.
Keep Next work as learning / optional A/B; **do not** treat Next-only as the
banking solution if ResukiSU PoC is available.
**LAB PoC order (current, 2026-09-16):**
1. ~~Foreign ResukiSU / abu boots~~ — reference only; wrong for our A16 tree
2. **Done**: our ReSukiSU → `boot-resukisu-our-20260915T183729Z.img` (penúltimo)
3. **Done (build)**: SusFS trial → `boot-resukisu-susfs-20260916T004038Z.img` —
flash/claim **after** US2; prefer penúltimo on LAB until then
4. Manager + TEESimulator-RS ± Zygisk/PIF → bank gate
## Integration notes (T008)
Work on host tree; flash only our `boot-resukisu-our-*` / `boot-resukisu-susfs-*`
artifacts (never `~/Downloads/boot-sunfish-resukisu*`):
1. Wire ReSukiSU as **`drivers/kernelsu` → `/srv/rom/references/resukisu/kernel`** (full git clone lives *outside* the msm tree). Do **not** keep a `KernelSU/` directory inside `msm-4.14`: this tree’s `scripts/setlocalversion` walks nested `.git` dirs and appends `KernelSU-gHASH` into `uname -r` (easy bank detection).
2. Always use a **non-shallow** ReSukiSU clone so `KSU_VERSION = 30000 + rev-list + 700` matches the manager (≥35080). Shallow depth=1 produced bogus **30701**.
3. Enable `CONFIG_KSU=y` + `CONFIG_KSU_MANUAL_HOOK` (+ AUTO_* on 4.14).
4. Patch syscall hook sites per https://resukisu.org/guide/manual-integrate.html.
5. Build `mka bootimage`; verify `uname -r` has **no** `KernelSU` string before bank testing.
6. SusFS: built as trial; **do not claim** until US1–US2 green and T019 boots clean.
References: ReSukiSU manual integrate; abu/XDA only as SusFS/module oracle
(`docs/reference-sources.md` §8; `docs/session-20260916-handoff.md`).
## Rootless snapshot (T039)
See `/srv/rom/artifacts/007-kernelsu/pre-root/OUTCOMES.txt` and `preflight.txt`.
Recorded on `23.2-20260915-UNOFFICIAL-sunfish`: microG + Phonesky + spoof ON;
Island user 10 present; **no su**. Yuh/Swissquote **not installed** at snapshot
time (N/A outcomes). Reinstall via Aurora→Island before US5 banking gate.
+220
View File
@@ -0,0 +1,220 @@
# 007 LAB results — 2026-09-16
LAB only: serial `11231JEC207477`. GOLDEN not used.
Build base: `23.2-20260915-UNOFFICIAL-sunfish`.
Kernel under test: ReSukiSU **35153** + SusFS **v1.5.5**, DEBUG boot
`#8 … Wed Sep 16 12:09:18 UTC 2026`
(`boot-resukisu-susfs-DEBUG-20260916T115115Z.img`).
Related: [source audit](007-susfs-source-audit.md),
[attempts brief](007-susfs-attempts-chatgpt-brief.md),
[runbook](007-kernelsu-runbook.md).
## Executive summary
| Area | Result |
|------|--------|
| SusFS + ReSukiSU **boot** (matched vendor) | **PASS** |
| Manager 35153 + `susfs_init` | **PASS** |
| Play downloads (system Phonesky alone) | **FAIL** → fixed with exact-alarm APK hotfix |
| TEESimulator-RS + ReZygisk + PIF inject | Installed and running |
| Yuh / Swissquote usable session | **FAIL** (Talsec root detect / exit **-69**) |
| SusFS as banking hide by itself | **Not sufficient** (expected; US2/attestation still open) |
**Bottom line:** The SusFS bring-up *pipeline* works end-to-end (build →
matched `boot`+`vendor` → modules load → Manager sees SusFS). Banking PASS is
still blocked by root/environment detection; SusFS did not clear that gate.
---
## 1. Boot / SusFS bring-up
### What failed earlier (corrected causality)
1. **Boot-only SusFS flashes** left LAB on an old `/vendor/lib/modules` set.
With `CONFIG_MODVERSIONS=y`, `drv2624` (and peers) disagreed on CRCs
(**31/56** incl. `module_layout`). That matches the T004038Z-class hang in
`VibratorManagerService` when the module never loads — **not** proof of the
later early-panic series.
2. First matched-module attempt used a **hand-repacked** `vendor.img` with
wrong SELinux labels (`vendor_file` instead of
`vendor_kernel_modules`). `init-insmod-sh` got `avc: denied { module_load }`
→ empty `lsmod` → same vibrator / bootanim stall. **ADB was alive.**
Reproduce ABI check:
```bash
python3 scripts/check-module-abi.py <Module.symvers> <module.ko>
```
### What worked
| Step | Detail |
|------|--------|
| DEBUG boot | `panic=0`, VT/fbcon; artifact `.local-artifacts/007-kernelsu/boot-resukisu-susfs-DEBUG-20260916T115115Z.img` |
| Vendor | Repacked modules from same build; flash via **fastbootd** (`vendor_a` sparse); vbmeta `--disable-verity --disable-verification` |
| SELinux fix | Relabel `.ko` → `u:object_r:vendor_kernel_modules:s0` (`incrementalfs` → `vendor_incremental_module`) |
| Post-boot | `sys.boot_completed=1`, `drv2624` + ~40 modules loaded |
| SusFS | `dmesg`: `susfs_init … version: v1.5.5` |
| Manager | Spoofed `weogax.atbfxb.skiggr` **35153**; UI: driver 35153, SusFS v1.5.5, hook **Manual**, SELinux Enforcing |
`su` from adb: `uid=0` `u:r:ksu:s0`. No `/sys/fs/susfs` (normal for 1.5.5;
ioctl/supercall path).
### Architecture note (open)
MANUAL_HOOK + `CONFIG_KSU_SUSFS` remains a **local hybrid** vs upstream ReSukiSU
hook `choice`. This DEBUG build **boots** with that hybrid + matched vendor.
ChatGPT’s “hook ABI collision” theory is **not** required to explain the
vibrator hang; it remains a hypothesis for earlier logo→reboot trials that
were not re-run with matched vendor. Prefer a controlled **SusFS-inline,
no MANUAL**, min-features experiment later — do not treat hybrid as final
architecture.
Prepared but not on this boot: execve post-hook UAF patch
`vendor/sunfish/patches/007-kernelsu/0001-exec-avoid-post-hook-filename-uaf.patch`
(real bug; also present on root-only path → not SusFS-only panic proof).
---
## 2. Play Store “pending”
**Cause:** `DownloadService` crash loop:
```text
SecurityException: FGS type systemExempted requires
FOREGROUND_SERVICE_SYSTEM_EXEMPTED + (SCHEDULE_EXACT_ALARM | USE_EXACT_ALARM | VPN)
```
System `/system/priv-app/Phonesky` lacked `USE_EXACT_ALARM` in the **APK
manifest** (privapp XML alone is insufficient). Documented hotfix:
| Item | Path |
|------|------|
| Host | `/srv/rom/artifacts/005-spoofing/Phonesky_exact-alarm.apk` |
| Laptop | `.local-artifacts/004-microg/Phonesky_exact-alarm.apk` |
```bash
adb install -r .local-artifacts/004-microg/Phonesky_exact-alarm.apk
```
Not in the bacon image yet (deferred with other API warnings). Next bacon must
ship the exact-alarm Phonesky prebuilt.
---
## 3. Modules reinstall
Installed with `su -M` (global mount ns; plain `su` often cannot see
`/data/adb` under SusFS) via `/data/adb/ksud module install …`:
| Module | id | Version |
|--------|-----|---------|
| ReZygisk | `rezygisk` | v1.0.0 (515) |
| TEESimulator-RS | `tricky_store` | v6.0.1-307 |
| Play Integrity Fix [INJECT] | `playintegrityfix` | v4.7-1-inject-s |
Zips staged under `.local-artifacts/007-kernelsu/modules/`. Versions matched
upstream `update.json` at install time. Reboot required after install; user
confirmed activated.
**TEESimulator targets:** default `target.txt` had Play/GMS + test apps only.
`com.yuh` and `com.swissquote.android` must be listed (GUI may not expose this;
edit `/data/adb/tricky_store/target.txt` or module action). Adding them was
necessary for TEEsim to bind those UIDs; it did **not** by itself clear Talsec.
Observed TEEsim noise: `StrongBox op limit reached (hw=4 sw=0 max=4)`,
`Skip patching short certificate chain of length null`.
**PIF note:** `pif.prop` spoofed **Pixel 6** (`oriole`) on sunfish — device
mismatch risk for banks; revisit fingerprint before next bank trial.
---
## 4. Banking (Yuh / Swissquote)
### Observed behaviour
- Both load **Talsec** `libts.so`.
- Swissquote (and Yuh after retries): `VM exiting with result code **-69**`
then process death — intentional anti-root exit, **not** an uncaught Java
crash. Same signature as 2026-09-15 captures (with/without TEEsim).
- Probes in logcat: `getenforce`, `/proc`, serial props, `qemu.*`, `ps` /
`pid_max`, cache symlink, etc.
- Operator report: after many attempts Yuh **locked the app** citing **root**.
### Profile / policy gaps (spec US2 still open)
- Contract: banks **never** receive root; umount/hide must be **verified**.
- `.allowlist` contained both bank packages (binary profile); confirm in
Manager UI: **Deny / Forbidden** + **Umount modules** for
`com.yuh` and `com.swissquote.android` (all userIds tested).
- SusFS initialised ≠ verified path hide for banking UIDs.
- Island / work profile not re-validated in this session.
### Verdict on SusFS vs banks
SusFS **does not** replace: deny-root profiles, verified umount, coherent
attestation (TEEsim keybox/StrongBox), and PIF props matching the device.
Pipeline works; **both-bank PASS remains FAIL**.
---
## 5. Operational recipes (LAB)
### Flash matched DEBUG (fastbootd)
```bash
adb reboot fastboot # userspace fastbootd
fastboot flash vendor_a .local-artifacts/007-kernelsu/vendor-DEBUG-modules.sparse.img
fastboot --disable-verity --disable-verification flash vbmeta_a .local-artifacts/007-kernelsu/vbmeta.img
fastboot flash boot_a .local-artifacts/007-kernelsu/boot-resukisu-susfs-DEBUG-20260916T115115Z.img
fastboot flash boot_b .local-artifacts/007-kernelsu/boot-resukisu-susfs-DEBUG-20260916T115115Z.img
fastboot reboot
```
Rollback vendor (host): `/srv/rom/artifacts/007-kernelsu/vendor-pre-debug-20260915.img`.
### Module install (after SusFS boot)
```bash
adb push <zip> /data/local/tmp/ksu-mods/
adb shell 'su -M -c "/data/adb/ksud module install /data/local/tmp/ksu-mods/<zip>"'
adb reboot
```
### TEESimulator bank targets
```bash
adb shell 'su -M -c "grep -E \"yuh|swissquote\" /data/adb/tricky_store/target.txt ||
printf \"%s\\n\" com.yuh com.swissquote.android >> /data/adb/tricky_store/target.txt"'
```
---
## 6. Artifacts index
| Artifact | Location |
|----------|----------|
| DEBUG boot | `.local-artifacts/007-kernelsu/boot-resukisu-susfs-DEBUG-20260916T115115Z.img` |
| Vendor sparse (correct SELinux) | `.local-artifacts/007-kernelsu/vendor-DEBUG-modules.sparse.img` (+ host `…/vendor-repack/`) |
| Experiment dir | `a16-build:/srv/rom/artifacts/007-kernelsu/debug-20260916T115115Z/` |
| Module zips | `.local-artifacts/007-kernelsu/modules/` |
| Phonesky exact-alarm | `.local-artifacts/004-microg/Phonesky_exact-alarm.apk` |
| ABI checker | `scripts/check-module-abi.py` |
| RCA CRCs | `.local-artifacts/007-kernelsu/rca-20260916/` |
| Manager pin | `.local-artifacts/007-kernelsu/SPOOFED_MANAGER_PIN.txt` (35153) |
---
## 7. Open follow-ups (non-ordering commitment)
1. Bacon: ship exact-alarm Phonesky + always package `boot` with matching
vendor modules (and correct `vendor_kernel_modules` labels).
2. Confirm Manager Deny + Umount for both banks; prove with `su` from bank UID
and mount evidence (US2).
3. TEEsim StrongBox limit / keybox quality; PIF fingerprint → sunfish-coherent.
4. Optional control: SusFS-inline without MANUAL, min features, matched vendor.
5. Apply execve UAF patch on next kernel rebuild.
6. Do **not** claim banking PASS until Yuh + Swissquote usable sessions on the
same stack (runbook hard-gate).
+312
View File
@@ -0,0 +1,312 @@
# SusFS + ReSukiSU on sunfish LOS 23.2 — full attempt log (for external review)
> **Audit correction (2026-09-16):** See [source and artifact audit](007-susfs-source-audit.md).
> The claims below that missing MANUAL_HOOK caused the vibrator hang, or that
> the upstream hook choice is intrinsically broken, are hypotheses, not proven
> causes. The audit confirms incompatible installed/vendor module CRCs, an
> exec post-hook use-after-free, and incomplete/mismatched SUSFS API wrappers.
> The exact early-panic cause still requires a trace or controlled experiment.
**Date compiled:** 2026-09-16
**Project:** `sunfish-a16-rom` (independent; not SSYSTEMS)
**Device (LAB only):** Pixel 4a `sunfish`, serial `11231JEC207477`
**ROM base:** LineageOS **23.2** / Android **16** (`lineage-23.2-20260915-UNOFFICIAL-sunfish`)
**Goal:** ReSukiSU root **plus** SusFS hide addon on **our** `msm-4.14` tree (A16), using abu/GOLDEN as **behavioural oracles**, not as flash images.
**Ask for help:** Why do MANUAL_HOOK + AUTO_* + CONFIG_KSU_SUSFS builds still `kernel_panic` before userspace on this tree, while (1) penúltimo ReSukiSU-without-SusFS boots, and (2) an earlier SusFS-without-MANUAL build reached Android (then hung on vibrator)? What remaining XOR / composition bugs exist in ReSukiSU when treating SusFS as an addon like KernelSU-Next?
---
## 1. Concepts (do not confuse)
| Name | What it is |
|------|------------|
| **KernelSU (KSU)** | Generic root-in-kernel project family |
| **ReSukiSU** | Fork we ship (kernel driver + Manager). Version code **35153**, `v4.2.0-rc2-…@ReSukiSU` |
| **KernelSU-Next** | Different fork; what **abu** wires in their reference tree |
| **SusFS** | Kernel **addon** (simonpunk) for path/mount/uname/cmdline hide. **Not** a replacement for ReSukiSU. Should sit **on top** of a KSU-family driver |
**Intended product stack:**
```
apps / banks
↑
SusFS ← optional hide (compile into boot)
↑
ReSukiSU ← root
↑
msm-4.14 + LOS 23.2 system
```
**abu product habit:** two boots (root-only vs root+SusFS). Same idea: SusFS on/off at **compile time** in the boot image, not a runtime “activate SusFS” without the code in the kernel.
---
## 2. Important paths (host `rom-dev01` + laptop)
### Host
| Path | Role |
|------|------|
| `/srv/rom/src/lineage/` | LOS 23.2 tree |
| `/srv/rom/src/lineage/kernel/google/msm-4.14/` | Our kernel (dirty: ReSukiSU + SusFS + patches) |
| `/srv/rom/src/lineage/kernel/google/msm-4.14/drivers/kernelsu/` | ReSukiSU kernel module sources (in-tree) |
| `/srv/rom/src/lineage/kernel/google/msm-4.14/fs/susfs.c` | SusFS implementation |
| `/srv/rom/src/lineage/kernel/google/msm-4.14/arch/arm64/configs/sunfish_defconfig` | Defconfig (KSU/SusFS/debug knobs) |
| `/srv/rom/src/lineage/device/google/sunfish/BoardConfig-common.mk` | `BOARD_KERNEL_CMDLINE`; optional `SUNFISH_DEBUG_PANIC_SCREEN=1` → `panic=0 console=tty0` |
| `/srv/rom/artifacts/007-kernelsu/` | Boot images + managers + RCA |
| `/srv/rom/artifacts/007-kernelsu/susfs-fail-rca/` | ikconfig diffs, notes, abu checklist |
| `/srv/rom/artifacts/007-kernelsu/run-susfs-bootimage.sh` | Normal SusFS bootimage builder |
| `/srv/rom/artifacts/007-kernelsu/run-susfs-bootimage-debug.sh` | DEBUG builder (`SUNFISH_DEBUG_PANIC_SCREEN=1`) |
| `/srv/rom/logs/007-build/susfs-bootimage.log` | Cumulative build log (script tee) |
| `/srv/rom/logs/007-build/susfs-bootimage-debug.log` | Debug build log |
| `/srv/rom/artifacts/detach-*.log` | Per-session `rom-detach` logs |
| `/srv/rom/references/abu-msm-4.14-ksu/` | **abu reference kernel** (A15-era). `drivers/kernelsu` → symlink `KernelSU-Next/kernel` |
| `/srv/rom/references/resukisu/` | ReSukiSU git reference (non-shallow; version from `rev-list`) |
| `/srv/rom/references/susfs4ksu/` | Upstream SusFS project reference |
| `/srv/rom/artifacts/007-kernelsu/abu-reference/` | Owner-supplied abu `boot-abu` / `dtbo-abu` (reference only; **do not flash as ours**) |
### Laptop workspace
| Path | Role |
|------|------|
| `/home/aventura/Workspaces/sunfish-a16-rom/` | Git project |
| `.local-artifacts/007-kernelsu/` | Copied boots, klog dumps, Manager APKs |
| `docs/007-kernelsu-runbook.md` | Runbook |
| `docs/007-abu-vs-ours-susfs.md` | abu vs ours checklist |
| `docs/007-debug-boot-panic-screen.md` | Debug boot / photo Oops notes |
| `specs/007-kernelsu-sunfish/` | Feature spec/tasks |
**SSH:** `ssh a16-build` → `android-build@rom-dev01` (see `docs/ssh/config`).
---
## 3. Boot image changelog (chronological)
All under `/srv/rom/artifacts/007-kernelsu/` (and laptop `.local-artifacts/007-kernelsu/` when copied).
| Stamp / file | uname / notes | Config essence | LAB result |
|--------------|---------------|----------------|------------|
| **Penúltimo** `boot-resukisu-our-20260915T183729Z.img` | `#1 … Tue Sep 15 18:06:17 UTC 2026` | `MANUAL_HOOK=y` + all **AUTO_***=y; **SusFS off** | **GOOD** — daily LAB root |
| `boot-005-known-good.img` | Stock bacon kernel (no KSU in practice for root) | LOS boot from zip | Emergency recovery when slots broken |
| **SusFS #1** `…-susfs-20260916T004038Z.img` | `#3 … Wed Sep 16 00:43:54` | `CONFIG_KSU_SUSFS=y`, **`MANUAL_HOOK` unset** (choice XOR) | Kernel up, **adb alive**, bootanim forever: `system_server` stuck in **VibratorManagerService.nativeGetVibratorIds** (~66s watchdog) |
| **SusFS #2** `…-susfs-20260916T091211Z.img` | `#4 … Wed Sep 16 09:21:41` | After Kconfig patch: `MANUAL_HOOK=y` + SusFS; **AUTO_* all unset** | **Google logo → reboot loop** (earlier than #1) |
| **SusFS #3** `…-susfs-20260916T104649Z.img` | `#6 … Wed Sep 16 10:51:09` | `MANUAL` + **AUTO_***=y + SusFS; `susfs_init` in MANUAL path; link fix for `ksu_init_rc_hook` | Still logo→reboot. Proven on **slot a** (same system as penúltimo) → **not A/B system_b** |
| **SusFS #4 sigfix** `…-susfs-20260916T112839Z.img` | `#7 … Wed Sep 16 11:32:09` sha `472a32d8…` | + sucompat **faccessat/stat** signatures: SusFS `struct filename**` only if `SUSFS && !MANUAL` | **Worse** — returns to fastboot **faster** |
| **SusFS #5 DEBUG** (building at doc time) | `susfs-boot-debug` session | #4 macros fix (`susfs_*` privilege macros only if `!MANUAL`) + `PANIC_TIMEOUT=0`, no `QCOM_FORCE_WDOG_BITE_ON_PANIC`, `VT`+`FRAMEBUFFER_CONSOLE`, cmdline `panic=0 console=tty0`, `SPOOF_CMDLINE` off | Pending |
**Known-good rollback always:**
`boot-resukisu-our-20260915T183729Z.img` on `boot_a` + `--set-active=a`.
**Manager matching 35153:**
`ReSukiSU_v4.2.0-rc2-spoofed_35153-arm64-v8a-release.apk`
(without Manager, `su` missing even if kernel has KSU).
---
## 4. Root-cause chain (what we learned)
### 4.1 ReSukiSU upstream treats SusFS as a **hooking method** (XOR)
Kconfig `choice "KernelSU Hooking Method"` originally included `KSU_SUSFS` alongside `KSU_MANUAL_HOOK` / `KSU_TRACEPOINT_HOOK`.
Enabling SusFS in defconfig → warning `override: KSU_SUSFS changes choice state` → **MANUAL_HOOK off**.
**ikconfig proof (T004038Z vs penúltimo):** see
`/srv/rom/artifacts/007-kernelsu/susfs-fail-rca/ikconfig.diff`.
**abu / KernelSU-Next model (correct for addon):** SusFS is a separate `bool` menu under KSU, **not** in the hook choice. Hooks = KPROBES (or manual). `susfs_init()` under `#ifdef CONFIG_KSU_SUSFS` always.
### 4.2 First LAB symptom (T004038Z) — vibrator watchdog
- Not an early panic: Android mostly up, adb works.
- `WATCHDOG KILLING SYSTEM PROCESS` blocked in `VibratorManagerService.nativeGetVibratorIds`.
- Interpreted as fallout of **missing MANUAL_HOOK** (broken input/LSM hook path / module vermagic issues discussed; drv2624 is vendor module).
### 4.3 After forcing MANUAL+SusFS — early `kernel_panic`
Builds with MANUAL (+ later AUTO) + SusFS die at **Google logo**, reboot, often land in **fastboot** after retries.
**Logging reality on Pixel 4a:**
- Bootloader **does not support** `fastboot fetch`.
- `/dev/block/by-name/klog` and `logfs` dumps show many `PM: Reason: kernel_panic` / `HARD RESET by PS_HOLD` but **no Oops / Call trace**.
- `/sys/fs/pstore` empty after successful penúltimo boot; Pixel ramoops often encrypted (`/vendor/bin/ramoops`, keys under CE — missing/unusable in our tries).
- Console default: `console=ttyMSM0` only; `# CONFIG_VT is not set` historically → **no Oops text on panel** (only logo).
### 4.4 Composition bugs fixed in-tree (still not enough for a clean boot)
Patches applied under `drivers/kernelsu/` (and defconfig / BoardConfig):
| # | File(s) | Change |
|---|---------|--------|
| 1 | `Kconfig` + `Kbuild` | SusFS **out of** hook `choice`; SusFS addon + MANUAL together (abu-like). Snapshot: `susfs-fail-rca/kconfig-fix-20260916T090322Z/` |
| 2 | `arch/.../sunfish_defconfig` | Re-enable `AUTO_SETUID/INITRC/INPUT=y` (T091211Z had them explicitly **unset**) |
| 3 | `runtime/ksud_integration.c` | `#elif CONFIG_KSU_SUSFS` → only if `&& !CONFIG_KSU_MANUAL_HOOK` so `ksu_init_rc_hook` is defined under MANUAL+AUTO (else **link error** `undefined symbol: ksu_init_rc_hook`) |
| 4 | `core/init.c` | In `MANUAL_HOOK` branch, also `#ifdef CONFIG_KSU_SUSFS susfs_init()` (abu always inits SusFS) |
| 5 | `feature/sucompat.h` + `.c` | `faccessat`/`stat`: SusFS `struct filename **` API **only** if `SUSFS && !MANUAL`. MANUAL call sites in `fs/open.c` / `fs/stat.c` pass `const char __user **` — with SusFS signatures → **ABI mismatch / panic** |
| 6 | `feature/sucompat.h` | Privilege macros: SusFS `susfs_is_current_proc_no_su` etc. **only** if `SUSFS && !MANUAL` (was still SusFS when both set — likely early panic) |
| 7 | defconfig + BoardConfig (DEBUG) | `PANIC_TIMEOUT=0`, disable `QCOM_FORCE_WDOG_BITE_ON_PANIC`, enable `VT`/`FRAMEBUFFER_CONSOLE`, cmdline `panic=0 console=tty0` when `SUNFISH_DEBUG_PANIC_SCREEN=1`; `SPOOF_CMDLINE` off |
** empirically:** fix #5 (sigfix T112839Z) made LAB failure **faster** to fastboot → either wrong direction for some other call path, or #6 was the dominant bug and #5 alone left broken macros. #6 is in the DEBUG build, **not yet LAB-proven**.
### 4.5 Slot / A/B lesson (not the SusFS root cause)
- After failed trials, `current-slot:b`, `slot-unbootable:a:yes` left LAB unbootable until **both** slots got `boot-005` + `--set-active=a`.
- SusFS on **slot a** (same `system_a` as working penúltimo) **still** bootlooped → failure is **SusFS/MANUAL kernel composition**, not stale `system_b`.
**Flash discipline now:** penúltimo on `boot_a`, experiment on `boot_b`, `--set-active`; rollback = `--set-active=a` (and reflash `a` if needed).
---
## 5. Critical comparison: why T004038Z got further than MANUAL+SusFS
| | T004038Z (SusFS as hook choice) | T091211Z+ (MANUAL+SusFS addon attempts) |
|--|----------------------------------|----------------------------------------|
| MANUAL_HOOK | **off** | **on** (+ AUTO on later builds) |
| Early boot | Survives to userspace / adb | `kernel_panic`, logo loop |
| Later failure | Vibrator HAL / system_server watchdog | Never gets there |
**Hypothesis for reviewers:** ReSukiSU still has multiple `#ifdef CONFIG_KSU_SUSFS` paths written for **XOR** (SusFS *instead of* MANUAL). Turning both on without auditing every `elif` leaves:
- Wrong sucompat prototypes vs `fs/*.c` hooks
- Wrong privilege helpers (`susfs_*` vs TIF flags)
- Possibly more (execveat_sucompat, dispatch mark, input hooks, etc.)
abu avoids this by using **Next + KPROBES** and SusFS as a pure addon with `#ifdef CONFIG_KSU_SUSFS` init, not ReSukiSU’s choice XOR.
---
## 6. abu reference details (oracle, not flash path)
**Tree:** `/srv/rom/references/abu-msm-4.14-ksu/`
**KSU:** `drivers/kernelsu` → `../KernelSU-Next/kernel`
**Kconfig pattern (excerpt behaviour):**
- `CONFIG_KSU` tristate
- `CONFIG_KSU_KPROBES_HOOK` (default y) — not ReSukiSU MANUAL choice
- `menu "KernelSU - SUSFS"` → `CONFIG_KSU_SUSFS` bool addon + many `KSU_SUSFS_*` features
- `ksu.c`: `#ifdef CONFIG_KSU_SUSFS` → `susfs_init();` **unconditional on SusFS**, independent of kprobes
**Feature flags abu has that our ReSukiSU SusFS Kconfig may lack:**
`HAS_MAGIC_MOUNT`, `AUTO_ADD_SUS_*`, `TRY_UMOUNT*`, `SUS_SU` (kprobes), etc.
**abu `sunfish_defconfig` in this mirror:** **0** `CONFIG_KSU*` lines — do not treat as the SusFS pin list.
**Owner note:** abu also ships separate ReSukiSU-oriented boots / managers under artifacts; still **reference**. Our flash path is only `/srv/rom/artifacts/007-kernelsu/boot-resukisu-*` built from **our** tree.
---
## 7. Parallel work same day (context, not SusFS)
- microG Location UI missing on LAB: we had staged official **`*-hw.apk`** (`versionName=…-hw`) which **omits** `NetworkLocationService` / `NetworkLocationProviderService`.
- GOLDEN/abu show full Location options with **default** (non-hw) GmsCore.
- Fixed: stage `com.google.android.gms-252432032.apk` (non-hw); LAB `adb install -r` restored NLP UI.
- GOLDEN/abu = **A15 scheme oracle**; we keep **newer** packages on A16.
---
## 8. How we build / logs
```bash
# normal SusFS bootimage (host)
/srv/rom/artifacts/bin/rom-detach susfs-boot-NAME -- \
bash -lc /srv/rom/artifacts/007-kernelsu/run-susfs-bootimage.sh
# DEBUG (panic freeze + tty0)
/srv/rom/artifacts/bin/rom-detach susfs-boot-debug -- \
bash -lc /srv/rom/artifacts/007-kernelsu/run-susfs-bootimage-debug.sh
```
Always record:
- `/srv/rom/artifacts/detach-<session>-<TS>.log`
- `/srv/rom/logs/007-build/susfs-bootimage.log` (or `-debug.log`)
- `detach-<session>.status` (`exit=0` / `exit=1`)
Verify image before flash:
```bash
unpack_bootimg + lz4 -dc kernel + scripts/extract-ikconfig
# expect MANUAL_HOOK=y, three AUTO_*=y, KSU_SUSFS=y
```
---
## 9. LAB recovery / log capture protocol (4a)
```text
1. SusFS on boot_b, penúltimo on boot_a, --set-active=b, reboot
2. On logo loop: Vol- + Power → fastboot
3. fastboot fetch klog → FAILS on this device (unsupported)
4. fastboot flash boot_a <penúltimo>; --set-active=a; reboot
5. IMMEDIATELY on adb:
su -c 'dd if=/dev/block/by-name/klog …; dd if=/dev/block/by-name/logfs …'
adb pull → lab-klog-*.bin / lab-logfs-*.bin
6. strings | grep kernel_panic|Oops|Call trace|susfs
```
Observed: step 6 usually only `PM: Reason: kernel_panic` — **insufficient for stack**. Hence DEBUG on-screen build.
---
## 10. Current tree intent (defconfig snapshot at doc time)
```
CONFIG_KSU=y
CONFIG_KSU_MANUAL_HOOK=y
CONFIG_KSU_MANUAL_HOOK_AUTO_SETUID_HOOK=y
CONFIG_KSU_MANUAL_HOOK_AUTO_INITRC_HOOK=y
CONFIG_KSU_MANUAL_HOOK_AUTO_INPUT_HOOK=y
CONFIG_KSU_SUSFS=y
CONFIG_KSU_SUSFS_SUS_PATH=y
CONFIG_KSU_SUSFS_SUS_MOUNT=y
CONFIG_KSU_SUSFS_SUS_KSTAT=y
# CONFIG_KSU_SUSFS_SPOOF_UNAME is not set
CONFIG_KSU_SUSFS_ENABLE_LOG=y
CONFIG_KSU_SUSFS_HIDE_KSU_SUSFS_SYMBOLS=y
# CONFIG_KSU_SUSFS_SPOOF_CMDLINE_OR_BOOTCONFIG is not set
CONFIG_KSU_SUSFS_OPEN_REDIRECT=y
CONFIG_KSU_SUSFS_SUS_MAP=y
# DEBUG bring-up:
CONFIG_PANIC_TIMEOUT=0
# CONFIG_QCOM_FORCE_WDOG_BITE_ON_PANIC is not set
CONFIG_VT=y
CONFIG_VT_CONSOLE=y
CONFIG_FRAMEBUFFER_CONSOLE=y
…
```
---
## 11. Open questions for a second opinion
1. Is ReSukiSU **fundamentally** unsafe to run with `CONFIG_KSU_MANUAL_HOOK=y` **and** `CONFIG_KSU_SUSFS=y` without a larger port from KernelSU-Next’s addon model (more than Kconfig)?
2. Given T004038Z reached userspace with SusFS-as-choice (no MANUAL), should bring-up prefer that path and fix vibrator separately, vs insisting on MANUAL+AUTO+SusFS?
3. Remaining `#ifdef CONFIG_KSU_SUSFS` sites to audit in ReSukiSU when MANUAL is also on (`sucompat.c` execveat_sucompat, `dispatch.c`, input, etc.)?
4. Best way to get a **panic backtrace** on Pixel 4a 4.14 without UART cable (fbcon reliability? pstore decrypt? `CONFIG_PSTORE_CONSOLE`?)?
5. Any known ReSukiSU + susfs4ksu commit/tag combo that officially supports “SusFS addon + manual hooks” on non-GKI 4.14?
---
## 12. Related docs in-repo
- `docs/007-abu-vs-ours-susfs.md`
- `docs/007-debug-boot-panic-screen.md`
- `docs/007-kernelsu-runbook.md`
- `docs/session-20260916-handoff.md`
- Host: `/srv/rom/artifacts/007-kernelsu/susfs-fail-rca/SUMMARY.txt` and `NOTE-*.txt`
---
## 13. Non-goals / constraints
- No Magisk as production root.
- Do not flash abu/torukk/Downloads boots as product.
- LAB only; never GOLDEN.
- Prefer small attributable patches; large Android builds on `rom-dev01` only, detached (`rom-detach`).
---
*End of brief — 2026-09-16 afternoon LAB session after T112839Z failure; DEBUG build in flight.*
+139
View File
@@ -0,0 +1,139 @@
# SUSFS bring-up: source and artifact audit, 2026-09-16
Status: **superseded for boot outcome** by
[007-lab-results-20260916.md](007-lab-results-20260916.md) — matched
DEBUG boot + vendor (correct SELinux) **booted**; SusFS v1.5.5 + Manager
35153 verified. This audit retains the ABI / stub / UAF analysis. Banking
PASS still FAIL. LAB only; GOLDEN was not accessed.
## Verified baseline and build
LAB reports `23.2-20260915-UNOFFICIAL-sunfish`, slot `_a`, kernel
`4.14.357-openela-g28f9290ae067-dirty #1 ... Sep 15 18:06:17 UTC 2026`.
Approved shell root works. The installed vibrator module probes successfully
on this kernel. The DEBUG build started at `2026-09-16T11:51:15Z` and was
still linking with `ld.lld` using a full CPU at 12:18Z. Ninja's idle-output
warning alone is not a stalled-build diagnosis. Its inputs were not edited
during this audit.
## Confirmed incompatible vendor module
The installed `/vendor/lib/modules/drv2624.ko` imports `module_layout` with
CRC `0xbce8f094`. The SUSFS sigfix build's `Module.symvers` exports it as
`0xf9adc508`. There are **31 mismatches among 56 imports**. The matching
module in the host output passes all 56 checks against that same symvers.
Evidence was captured before DEBUG replaced these outputs:
- Host symvers mtime: `2026-09-16 11:44:39 UTC`.
- Host vendor module mtime: `2026-09-16 11:45:48 UTC`.
- Kernel config: `CONFIG_MODVERSIONS=y`, module force loading disabled.
- Device module loading list: `device/google/sunfish/init.insmod.sunfish.cfg`
includes `drv2624.ko` alongside audio, touch and WLAN modules.
- Local evidence: `.local-artifacts/007-kernelsu/rca-20260916/`, including
`Module.symvers-sigfix`, `drv2624-installed.ko`, `drv2624-built.ko` and CRCs.
Reproduce with `python3 scripts/check-module-abi.py <Module.symvers> <module.ko>`.
Exit 1 means incompatible or unverified; exit 0 verifies CRCs only, not full
ABI/CFI/firmware compatibility or device behaviour.
**Implication:** flashing a SUSFS boot while retaining these installed vendor
modules is an invalid release combination. Rebuilding modules on the host
does not update the phone's vendor partition. The older vibrator watchdog
is consistent with this defect, but historical dmesg or a controlled test is
still needed to prove that particular attempt's cause. This result does not
prove the cause of the subsequent early panics.
SUSFS currently changes `task_struct` and `vfsmount`; those changes can affect
layouts and transitive exported symbol CRCs. Do not bypass version checks.
Package a consistent kernel/modules set and verify all imports before testing.
After vendor changes, the old boot alone is not necessarily a valid rollback;
preserve the corresponding old vendor/ROM and slot state too.
## Confirmed filename lifetime defect
Our `fs/exec.c:do_execveat_common()` calls `__do_execve_file()`, which calls
`putname(filename)` on both successful and failed exec paths. The wrapper
then calls `ksu_handle_post_execveat()`; ReSukiSU dereferences
`filename->name`. This is a use-after-free in the integration. It also exists
in the root-only path, so its existence does not establish why only SUSFS
panics in the recorded trials.
Prepared patch:
`vendor/sunfish/patches/007-kernelsu/0001-exec-avoid-post-hook-filename-uaf.patch`.
It calls `ksu_handle_post_execve()` with NULL filename, which that handler
explicitly supports for its LSM adapter. The patch is staged locally, not
applied to the running build, and still needs compilation and LAB validation.
## Confirmed incomplete and mismatched SUSFS API
The active kernel advertises SUSFS **v1.5.5**, based on upstream
`kernel-4.14` at `77905b5a071e6f3669e3b1814cea30147c0801da` (February 2025),
plus local compatibility wrappers. It is not a complete modern backport.
The active `fs/susfs.c` contains:
- `susfs_add_sus_map()`: reports success without implementing map hiding.
- `susfs_set_avc_log_spoofing()`: reports success without the SELinux hook.
- `susfs_add_sus_path_loop()`: aliases a one-time addition, with no loop list.
- `susfs_run_extra_works()` and `susfs_start_sdcard_monitor_fn()`: no-op.
- `susfs_get_enabled_features()`: returns only the generic KSU_SUSFS string.
The local `st_susfs_sus_path` starts with `err`, then `target_ino`, then a
pathname. Modern upstream v2.3.0 starts with the pathname, then `err`.
Sharing command numbers or converting a pointer type does not make those
wire formats compatible. Do not interpret compile/link success or a manager
feature badge as proof that the commands work.
[ReSukiSU integration documentation](https://resukisu.org/guide/build.html)
explicitly directs non-GKI SUSFS integrators to backport the 5.10 patches.
Its separate SUSFS hook mode is deliberate; the Kconfig choice is not in
itself an upstream bug. Missing manual hooks when selecting SUSFS means
the SUSFS integration must provide its own complete hook path.
## Upstream freshness checked without changing live inputs
Remote refs were queried directly on 2026-09-16:
- Latest ReSukiSU tag seen: `v4.2.0-rc2`,
`3576e6a5255fc880c00f6a3175d89268a6d942be`.
- Active ReSukiSU: `6d674e50a022a85076dcfe4498af9a8bfead2cf9`, nine commits
after that tag, non-shallow, rev-list count 4453, computed version 35153,
with local patches. Manager binary matching still requires independent
verification; the filename is not sufficient.
- Latest observed SUSFS 5.10 branch:
`e466df60670eab2847ea76940be2f8a125c8adc9`, advertising **v2.3.0**.
- Remote 4.14 branch still points to the old v1.5.5 commit above.
- Abu reference checkout `ksu-susfs-23.0` is
`8c604af9d3d97a8b1b2da1b663a4e39b32697e5a`, with SUSFS **v1.5.12**.
This checkout is not proof of the exact source/configuration of the
owner's working L22 binary. Do not conflate the two.
Before a new build, fetch tags, record full revisions and version codes, and
use a matching manager and userspace tool. A v2.3.0 port must include actual
implementations, correct struct layouts and all call-site adaptations.
## Panic evidence remains missing
The known-good dmesg shows the ramoops script exits 0, but inspection shows
that the script returns the final key-generation command's status, not the
earlier decrypt status. LAB has `vendor.ramoops.decryption.error=1`,
`use_alt=0`, and empty pstore. No private key contents were read or copied.
Therefore neither successful script exit nor an empty directory proves that
the failed kernel produced no backtrace.
Framebuffer console config does not guarantee a visible panic: display
registration and panic timing must also permit output. `panic=0` does not
by itself disable every independent hardware watchdog.
## Next diagnostic work
1. Preserve DEBUG as a named experiment with hashes/config/source snapshot.
2. Fix the lifetime defect in a subsequent isolated change.
3. Obtain panic trace or instrument milestones; distinguish kernel init,
module load, init and zygote failures rather than infer timing from logo.
4. Use a matched kernel/vendor-module deployment and rollback. Never compare
different system slots as though only boot changed.
5. Complete a modern SUSFS backport with the intended hook model, then verify
real command responses, root denial, per-UID hiding and hardware behaviour.
Boot success alone does not meet the requested result or feature gates.
+115
View File
@@ -0,0 +1,115 @@
# Design: native call-recording unlock + FLAG_SECURE override (`008`/`009`)
**Date**: 2026-09-16
**Status**: Approved by owner (requirements); Spec Kit feature
`specs/008-call-rec-flag-secure/`
**LAB only**: `11231JEC207477`. No Magisk/LSPosed/Zygisk for these features.
## Problem
1. Lineage Dialer can refuse call recording based on **country / carrier**
rules even when the user wants to record.
2. Apps set `FLAG_SECURE` so screenshots / screen capture fail; we want an
optional **global** override (project-specific; not AXP Pro catalog).
## Goals (MVP)
Two **Developer Options** switches (same UX pattern as `005`
`allow_signature_fake`):
| Toggle (plain language) | Secure setting | Default | Effect when ON |
|-------------------------|----------------|---------|----------------|
| Disable call recording restrictions | `extendrom_call_recording` (GOLDEN / EXTENDROM) | `0` | Country/region limits ignored; Dialer may record |
| Allow screenshots of secure windows | `disable_secure_windows` (AOSP; DO pref `allow_flag_secure_override`) | `0` | Global: ignore `FLAG_SECURE` for screenshots / screen capture (debuggable builds) |
- **Global only** — product requirement; no per-app lists (not planned).
- OFF = stock Android / LOS behaviour.
- No root modules required at runtime.
## Non-goals
- Full AXP call-recording policy UI (auto-record rules, etc.).
- Per-app FLAG_SECURE lists (out of scope permanently for this project).
- Banking / KernelSU (`007` incomplete).
- Shipping Magisk zips.
## Approaches considered
### Call recording
| Approach | Pros | Cons |
|----------|------|------|
| **A. EXTENDROM / AXP GOLDEN native F_CALLREC** (chosen) | Same Secure key as GOLDEN (`extendrom_call_recording=1` observed); DO + Dialer gate; attributable | Re-validate paths on LOS 23.2 / A16 |
| B. Magisk Dialer RRO (`call_record_states.xml`) | Tiny | Not native; no GOLDEN parity; rejected |
| C. Blind-copy A11 overlays only | Fast | May no-op without Dialer Java gate |
**Choice: A.** Primary sources:
`/srv/rom/references/extendrom/er.sh` (F_CALLREC),
`overlays/call-rec`, `config/call-rec/…/CallRecPreferenceController.java`.
Secure string: `Settings.Secure.ER_ALLOW_ANY_CALL_REC = "extendrom_call_recording"`.
Also enable Dialer `call_recording_enabled` / audio source as needed for
sunfish (EXTENDROM sets `call_recording_enabled=true`,
`call_recording_audio_source=4`), after verifying LOS 23.2 Dialer still uses
those resources.
### FLAG_SECURE
| Approach | Pros | Cons |
|----------|------|------|
| **A. DO → AOSP `DISABLE_SECURE_WINDOWS`** (chosen) | Already in WMS on A16; tiny surface | Debuggable-only gate in stock WMS |
| B. Magisk `services.jar` patcher | Works on `user` | Not native ROM |
| C. Duplicate frameworks hooks under another Secure key | — | Redundant with AOSP |
**Choice: A.** Wire Settings DO to `disable_secure_windows`.
## Design
### Settings / UI
- Category under Developer Options (new or shared “Privacy overrides” /
EXTENDROM-style categories).
- Two `SwitchPreference`s + short legal/risk summaries (call recording:
user responsible for local law — EXTENDROM wording as reference).
- Controllers mirror `005` / EXTENDROM `DeveloperOptionsPreferenceController`:
write `Settings.Secure` 0/1; reset to 0 when Developer Options master is
disabled.
### Call recording path
1. Inventory LOS 23.2 Dialer: where country/carrier recording is blocked
(`isCountryAllowed` or successor).
2. When `extendrom_call_recording=1`, treat as allowed (GOLDEN).
3. Ensure recording feature is compiled/enabled for Dialer on sunfish
(`config.xml` / product overlay as required).
4. LAB: enable toggle → place/receive call → record → file exists; disable →
region block behaviour restored (or stock deny if in restricted region).
### FLAG_SECURE path
1. Confirmed A16: `WindowManagerService` + `WindowState.isSecureLocked()` already
honour `Settings.Secure.DISABLE_SECURE_WINDOWS`.
2. DO switch writes that key (UI title: Allow screenshots of secure windows).
3. LAB userdebug: OFF blocked / ON capturable; document DRM exceptions.
### Packaging
- Patches under `vendor/sunfish-a16/patches/008-…` (idempotent apply script
like `005-sigspoof`), attributed to EXTENDROM/`er.sh` F_CALLREC (call-rec)
and native frameworks work (FLAG_SECURE).
- No Magisk in the bacon zip; no Magisk zips in the meta-repo.
### Validation
- LAB serial only; document settings get/put and screenshots/logs under
`/srv/rom/artifacts/008-…`.
- Does not reopen `007` banking gates.
## Relationship to roadmap numbers
| Roadmap # | Theme | This feature |
|-----------|--------|--------------|
| 008 | Call recording | Included |
| 009 | FLAG_SECURE | Included (global only) |
Single Spec Kit directory: `specs/008-call-rec-flag-secure/`.
+68
View File
@@ -0,0 +1,68 @@
# Runbook: `008` call-rec + FLAG_SECURE (LAB)
**LAB serial**: `11231JEC207477` only — never GOLDEN.
**Apply** (on `rom-dev01`):
```bash
python3 /srv/rom/src/lineage/vendor/sunfish-a16/patches/008-call-rec-flag-secure/apply_008_call_rec_flag_secure.py
```
Idempotent (second run = all “already”). Marker:
`SUNFISH_A16_008_CALL_REC_FLAG_SECURE`.
**Evidence**: `/srv/rom/artifacts/008-call-rec-flag-secure/`
**Logs**: `/srv/rom/logs/008-build/`
**Backups**: `/srv/rom/artifacts/008-call-rec-flag-secure/bak-snapshots/`
## Secure keys
| Key | Default | Source |
|-----|---------|--------|
| `extendrom_call_recording` | `0` | AXP GOLDEN / EXTENDROM (`ER_ALLOW_ANY_CALL_REC`) |
| `disable_secure_windows` | `0` | AOSP A16 `Settings.Secure.DISABLE_SECURE_WINDOWS` (DO UI key `allow_flag_secure_override`) |
GOLDEN dump: `extendrom_call_recording=1` in
`.local-artifacts/002-golden/…/settings/secure-*.txt`.
## Call-rec — EXTENDROM `F_CALLREC` (applied)
| Step | LOS 23.2 path | Status |
|------|---------------|--------|
| Dialer config | `packages/apps/Dialer/.../callrecord/res/values/config.xml` | already `enabled=true`, audio `4` |
| Controllers | `Settings/.../ER_CallRecPreferenceController.java` + `ER_CallRecInfo.java` | applied |
| Dashboard | `DevelopmentSettingsDashboardFragment.java` | applied |
| DO XML | `Settings/res/xml/development_settings.xml` | applied |
| Strings | `Settings/res/values/strings.xml` | applied |
| Secure const | `frameworks/base/.../Settings.java` `ER_ALLOW_ANY_CALL_REC` | applied |
| Dialer gate | `Dialer/.../CallRecorder.java` `canRecordInCurrentCountry` | applied |
## FLAG_SECURE — A16 inventory (applied via DO only)
AOSP already implements the capture path:
- `WindowState.isSecureLocked()` → early false if
`mWmService.getDisableSecureWindows()`
- `WindowManagerService.updateDisableSecureWindows()` reads
`Settings.Secure.DISABLE_SECURE_WINDOWS`
- **Only when** `ro.debuggable` / `SYSTEM_DEBUGGABLE` is true (LAB
`userdebug` OK; `user` needs retest / possible gate lift later)
Project work: Settings DO writes that Secure key (no Magisk / no jar patcher).
## Validation
See `specs/008-call-rec-flag-secure/quickstart.md`.
After bacon + flash Magisk-free:
```bash
adb -s 11231JEC207477 shell settings get secure extendrom_call_recording
adb -s 11231JEC207477 shell settings get secure disable_secure_windows
```
## `user` vs `userdebug`
LAB = `userdebug` (DO + WMS secure-windows disable active). Before release
(`011`), re-check on `user`: DO visibility (like `005`) and whether
`updateDisableSecureWindows` still no-ops when not debuggable — document
outcome; do not silently claim `user` PASS.
+55
View File
@@ -0,0 +1,55 @@
# Documentation
Project rules: `.specify/memory/constitution.md`
Agent rules: `../AGENTS.md`
Feature specs: `../specs/`
Conversation continuity: [`chatgpt-context.md`](chatgpt-context.md) — context
from the shared ChatGPT discussion, reconciled with later repository records;
non-normative.
## Host boundary
`rom-dev01` (Ubuntu 24.04, public IPv4, SSH) is provisioned **outside** this
project (SSYSTEMS Salt `saltenv=base` / PVE). This repository must not specify
or automate Proxmox VM creation, public IP selection, or company Salt states.
Feature `001-development-environment` starts from an already-available host.
## Planned Gitea repositories (on rom-dev01)
- `sunfish-a16-rom` — Spec Kit / constitution / docs (meta)
- `sunfish-rom-manifest`
- `sunfish-rom-build`
- `sunfish-rom-patches`
Plus forks only when modification requires them (kernel, frameworks/base,
Settings, Dialer, vendor/config).
## Reference sources (Lineage / AXP / GrapheneOS)
[`reference-sources.md`](reference-sources.md) — roles, URLs, local vs later
`rom-dev01` archive work. **Sunfish only.**
## Device roles (two sunfish)
[`device-roles.md`](device-roles.md) — GOLDEN (AXP, do not flash) vs LAB (experiments).
## AXP sunfish preservation (detail)
[`axp-sunfish-preservation.md`](axp-sunfish-preservation.md) — Disroot/Codeberg/
downloads/Tor cookbook for sunfish-scoped AXP archive. Run after `001` on
`rom-dev01`. **Not** a full AXP org clone.
## AXP Pro parity
Living matrix (non-normative): [`axp-pro-parity.md`](axp-pro-parity.md).
## Android 15 → 16 developer delta
Living notes for porting AXP/GrapheneOS ideas onto LOS 23.2 without blind copy:
[`a15-a16-delta.md`](a15-a16-delta.md).
## Hardening backlog
Non-normative candidates: [`hardening-backlog.md`](hardening-backlog.md).
+129
View File
@@ -0,0 +1,129 @@
# Android 15 (AXP GOLDEN) → Android 16 (LOS 23.2) — developer delta
Living notes for porting **AXP / GrapheneOS ideas** onto our base without
blind copy. Non-normative until a row is promoted into a feature `spec.md`.
| Ours (LAB target) | GOLDEN (oracle) |
|-------------------|-----------------|
| LineageOS **23.2** / Android **16** | AXP Pro / LOS-derived **22.2** / Android **15** |
| `BUILD_ID=BP4A.251205.006`, SDK **36** tree | SDK **35**, `ro.build.version.release=15` |
| Device: sunfish (same silicon) | sunfish |
**Working rule (owner):** prefer AXP A15 scheme when still valid; if A16 is
easier, use it; if harder, redesign the *path* to the **same end state**.
---
## 1. Already measured on this project
| Area | A15 / AXP behaviour | A16 / LOS 23.2 reality | Adaptation |
|------|---------------------|------------------------|------------|
| Phonesky on `/system/priv-app` | Works on GOLDEN | `artifact_path_requirements` / `generic_system.mk` rejects device-specific `system/` installs unless allow-listed | Keep AXP path; `PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST` (`004`) — do **not** move to `product` only to silence the check |
| GmsCore / Gsf on `system_ext` app | GOLDEN layout | Builds clean (no artifact hit) | Keep |
| Priv-app XML for GmsCore | GOLDEN: not priv-app | Inventing `privapp-permissions-microg` diverges | Removed; add only if Self-Check proves need |
| Super partition headroom | AXP fuller image fits their budgets | Vanilla LOS ERROR_LIMIT too tight for fat microG+Play | Modest `BOARD_SUPER_PARTITION_ERROR_LIMIT` bump — not ABI-strip of official microG |
| Play targetSdk / apktool | Older Play on GOLDEN (50.x) | Latest Play ~52.x needs apktool 2.12 + `ANDROIDSDK=35+`, zipalign-before-sign | Documented in `004` runbook |
| PRESIGNED prebuilt APKs | AXP ships Play/microG with intact v2/v3 | `BUILD_PREBUILT` uncompresses JNI / realigns → strips APK Sig v2/v3; PM skips Phonesky (`pm path` empty) | `LOCAL_REPLACE_PREBUILT_APK_INSTALLED` for GmsCore/Gsf/Phonesky (`004`, measured 2026-09-15) |
| Phonesky privapp allowlist | AXP XML for older Play | Play 52.x on A16 requests `HIDE_STATUS_BAR_NOTIFICATION` + `READ_SYSTEM_GRAMMATICAL_GENDER`; missing → boot abort on userdebug | Extend `privapp-permissions-com.android.vending.xml` |
| Signature spoofing | EXTENDROM: `allow_signature_fake` + `FAKE_PACKAGE_SIGNATURE` + Settings toggle; grants on gms+vending; A15 STABLE | LOS 23.2 **already** has LuK1337 allow-list spoof (`ComputerEngine` `23c0a5c`): userdebug + microG signer + Google fake-sig + `signingInfo`. Missing vs AXP: no `FAKE_PACKAGE` perm/UI; **rejects project-signed Phonesky** | Feature **`005`**: extend signer allow-list for our Phonesky JKS; port EXTENDROM toggle + `FAKE_PACKAGE_SIGNATURE`; see `docs/005-spoofing-runbook.md` |
---
## 2. Partition / packaging (porter mindset)
- AOSP has been tightening **system vs system_ext vs product** for years; A16
builds (especially with `generic_system.mk`) make “drop OEM APKs on system”
a **build-time** problem, not only a policy preference.
- **Privapp-permissions must live on the same partition as the priv-app**
(AOSP rule since product partition work). Moving Phonesky later ⇒ move XMLs
with it.
- `system_ext` remains the natural home for ROM Google-compat substitutes
(AXP: microG, F-Droid, Aurora base) — still valid on A16.
- Prefer **allow-list exception** or **system_ext/product** over rewriting the
whole AXP layout when GSI rules bite.
---
## 3. Compatibility stack (microG / Play / spoofing)
| Topic | Note for A16 |
|-------|----------------|
| microG | Use **current** upstream; do not pin GOLDEN 0.3.15. Keep official signer for F-Droid. |
| Phonesky | Still patch via AXP `patch-playstore`; newer Play = newer apktool/SDK framework. |
| Signature spoofing | **Done in `005`:** extend LuK1337 allow-list for project Phonesky JKS; port EXTENDROM toggle + `FAKE_PACKAGE_SIGNATURE`. LAB US6 green 2026-09-15. |
| Phonesky FGS / exact-alarm | A16 `systemExempted` needs manifest `USE_EXACT_ALARM` (or SCHEDULE / VPN), not only privapp XML. Fixed in Phonesky rebuild; vendor prebuilt updated. |
| FakeStore | Still forbidden next to real Phonesky. |
---
## 4. Attestation / KeyMint / banking (`006`/`007`)
- GOLDEN already shows **KeyMint / RKP / TEE** stress with Yuh on **A15**.
- A16 does not remove that class of problem; RKP and attestation surfaces
continue to evolve — treat GrapheneOS/AOSP docs as *what* to measure, LOS+LAB
as *where* to measure.
- Magisk DenyList on GOLDEN ≠ our root plan. Production root = **KSU-Next +
SusFS** (`007`). Do not port Magisk-shaped packaging into `004`.
---
## 5. GrapheneOS on Android 16 (reference, not base)
- GOS ported features to A16; publicly noted **harder** device-support work
because AOSP dropped much Pixel board/userspace publishing with 16.
- For **sunfish** we stay on **LOS 23.2 device trees**, not GOS device bringup.
- Use modern GOS as reference for **hardening / privacy behaviour** patches
(`013`, hardening backlog) — classify each patch: direct / port / obsolete /
A16-incompatible (same discipline as `reference-sources.md`).
- Never treat historical GOS **A13 sunfish** kernel as A16 base.
---
## 6. Call recording / FLAG_SECURE / UX (`008`+)
- AXP overlays and older XML toggles often **break or no-op** across major
releases. On A16: re-find Dialer/Telecom/framework hooks in LOS 23.2 first;
then decide AXP vs EXTENDROM vs iodé vs native LOS.
- Screenshot / sensitive-content APIs continue to tighten — verify before
porting FLAG_SECURE bypasses.
---
## 7. Checklist before applying an AXP or GOS change on A16
1. What problem does it solve on GOLDEN / in GOS docs?
2. Does LOS 23.2 already provide an equivalent?
3. Partition + artifact-path impact?
4. Same-signer / update path impact (microG, Phonesky)?
5. Kernel 4.14 / sunfish EOL constraints?
6. If blocked: document blocker; redesign route; **same end state**.
---
## 8. Open follow-ups (fill as we hit them)
- [x] Exact LOS 23.2 spoofing surface for `005` — stock LuK1337 path + our
`SUNFISH_A16_005_SIGSPOOF` extension (2026-09-15); runbook
`docs/005-spoofing-runbook.md`
- [ ] Whether A16 needs any microG permission XML AXP ships outside privapp
- [ ] RKP/KeyMint behaviour on LAB after `004` (compare to `002` Yuh logs)
- [ ] Call-recording hook locations on LOS 23.2 Dialer
- [ ] Which GOS A16 hardening patches are realistic on sunfish 4.14
## 9. EXTENDROM A15 → A16 mapping (`005`)
| EXTENDROM A15 patch | LOS 23.2 action |
|---------------------|-----------------|
| `02`/`06` ComputerEngine FAKE + gate | **Adapt** existing LuK1337 `generateFakeSignature`; add toggle + permission + Phonesky signer |
| `01`/`12` api current + lint | Applied |
| `07` AndroidManifest permission | Applied |
| `03` PermissionMapping | Applied (`.kt` path) |
| `04` Activity requestPermissions | Applied (A16 short-circuit path context) |
| `05` Settings.Secure.ALLOW_SIGNATURE_FAKE | Applied |
| `08`–`11` Settings Developer Options | Applied (controllers + XML + strings) |
| Overlay strings | Inlined into frameworks/Settings `strings.xml` |
Source checkout: `/srv/rom/references/extendrom` @ `f129253c07e735a373441254457cf75efac11d04`.
Update this file when a delta is **measured** (build error, Self-Check, adb),
not from blog speculation alone.
+95
View File
@@ -0,0 +1,95 @@
# AXP.OS Pro feature parity matrix
Living tracker for how `sunfish-a16-rom` relates to **AXP.OS Pro** functionality.
**Non-normative.** This file is a product/backlog tracker. A row becomes a
requirement only when promoted into a feature `spec.md`. For an active feature,
constitution + that feature's `spec.md` are authoritative if they conflict with
this matrix.
**Goal (constitution):** reproduce AXP.OS Pro functionality except where a
feature is obsolete, incompatible with LineageOS 23.2 / Android 16 / sunfish, or
deliberately substituted (documented in this table).
**Do not** treat this as a blind copy list. Every row must state our target and
implementation source. Update status as features land.
Status values: `todo` | `decide` | `substitute` | `wont` | `partial` | `done` |
`los-provides` | `later`
The parity matrix **spans the whole project (`004`–`013`)**. Feature `010` owns
only accepted AXP **usability/privacy** items not already assigned to another
feature. Signing/AVB, userspace hardening, and kernel hardening remain `011`–
`013` even though they also appear here as AXP Pro concerns.
| Feature | AXP Pro | Our objective | Status | Source / implementation |
|---------|---------|---------------|--------|-------------------------|
| Base ROM | AXP (LOS-derived) | LineageOS 23.2 / Android 16 sunfish | todo | LOS official `lineage-23.2`; `003` |
| On-device release testing | Yes (pre-release device QA) | Yes — stage checklists + known-good builds | todo | `003` smoke; later release gate; `011` |
| ASB patch integration / backports | Yes | Prefer LOS merges; backport missing applicable patches; document non-applicable | todo | LOS first; gaps → `012`/`013` |
| SELinux enforcing | Yes | Expect **Enforcing**; measure on vanilla then keep | todo | Verify in `003`; then likely `los-provides` |
| Data encryption enforced | Yes | Expect encryption enabled; no silent weaken | todo | `003` PIN/encrypt smoke; then likely `los-provides` |
| Project signing | Yes | Own keys for release builds | todo | `011` |
| Strong signing / AVB key policy | Yes (AVB/APK/dm-verity hardening) | Project AVB + release signing policy | todo | `011` |
| Reproducible builds | Yes | Traceable builds; push toward reproducibility | todo | metadata from day one; harden in `011` |
| Privacy-focused system defaults | Yes | Decide defaults beyond LOS stock | decide | `010` |
| microG + GSF | Yes | Yes — working Self-Check / push | done | `004` packaging + `005` LAB US6 (`23.2-20260915`) |
| Patched Phonesky / Play | Yes | Yes — priv-app, updatable | done | Play 52.9.22-24 + exact-alarm; installs OK on LAB (`005`) |
| Signature spoofing | Advanced / configured | Restricted LOS + Phonesky JKS + DO toggle | done | `005` runbook; `allow_signature_fake` + `FAKE_PACKAGE` |
| Magisk / preroot | Magisk (**Magia** UI on GOLDEN, 29.0) | **Substitute:** KernelSU-Next (+ SusFS) / evaluate ReSukiSU | substitute | Captured in `002`; do **not** fix GOLDEN; project root = `007` on LAB |
| DenyList / umount for banks | Magia DenyList (pkg list in `docs/002-golden-inventory.md`) | KernelSU app profiles + verified umount | todo | `007` re-runs `006`; GOLDEN DenyList captured `002` |
| Insular work profile | AXP docs say Insular; **GOLDEN measures Island** (`com.oasisfeng.island.fdroid` 6.4.2) | Banking workflow parity (Island or equivalent) | todo | `006`; capture in `002` |
| Yuh in Insular/Island | Historically worked; **FAIL since ~weeks** (HA / “certificate invalid”; KeyMint/TEE errors in `002` logs) | Hard-gate Compatibility MVP (rootless in `006`); **Revolut not a hard-gate** | partial | `002` evidence; LAB may need SusFS+TrickyStore-class stack (`reference-sources.md` §8); never GOLDEN |
| Swissquote in Insular/Island | **PASS** on GOLDEN (owner report, DenyList) | Hard-gate Compatibility MVP (rootless in `006`) | partial | `002`; re-test in `006`/`007`/`011` |
| Unrestricted call recording | Yes | Yes — native ROM on Android 16 | todo | AXP/EXTENDROM/iodé as references only; re-validate on LOS 23.2; `008` |
| DNS blocker / privacy DNS | Yes | Wanted; exact UX TBD | decide | `010` |
| Hosts / tracker blocking | Yes | Wanted | decide | `010` |
| hardened_malloc | Yes | Wanted if cleanly portable | decide | Graphene/AXP; `012`; see hardening backlog |
| Hardened / CVE-patched kernel | Yes | LOS 23.2 kernel + Graphene/AXP/Divest backports | todo | `013` (+ selective work with `007`) |
| Deblobbing | Yes | Case-by-case; don't break HW | decide | `010` / `013` |
| AOSmium WebView | Yes | Ship usable WebView; allow installing alternate WebView providers and selecting the active one in **Developer options → WebView implementation** | decide | `010`; stock/AOSmium/Vanadium/etc. as candidates |
| WireGuard in kernel | Yes | Check if LOS 23.2 already sufficient | decide | Baseline in `003`; else `010`/`013` |
| eSIM support | Documented on AXP | sunfish support TBD; verify vanilla first | decide | Baseline in `003`; else `010` |
| F-Droid + extra repos | Yes | Decide default ship set | decide | `010` |
| Aurora Store | Used in golden banking flow | Document/capture; may remain user app | decide | `002` flow; `010` |
| NeoLauncher | Default on AXP | Decide LOS launcher vs NeoLauncher | decide | `010` |
| Fossify Gallery / bundled app set | Part of AXP usability set | Decide which extras to ship | decide | `010` |
| Boot debug | Available | Useful for bring-up | decide | `010` or early debug docs |
| Relock / custom AVB | AXP path | Project keys + AVB after stable ROM | todo | `011` |
| Widevine (basic) | Yes | Don't regress vs vanilla LOS if possible | decide | Measure in `003`; track in `010`/`011` |
| App/data backup | AXP tooling | Not a project gate (two devices: GOLDEN stays; LAB is disposable). Optional personal NeoBackup only; no custom ROM/recovery backup | substitute | See `docs/device-roles.md` |
| Extra bundled packages | Various | Only if justified | decide | `010` |
| Auto-reboot / radio timers | Privacy extras | Wanted where practical | decide | `010` |
## Mapping rules
| AXP mechanism | Our default approach |
|---------------|----------------------|
| Magisk | KernelSU-Next (`007`); Magisk only as A/B diagnostic if needed |
| AXP signature spoofing stack | LOS restricted spoofing + controlled Phonesky extension (`005`) |
| AXP hardened kernel tree | LOS sunfish kernel + selective backports |
| AXP Phonesky | Keep functionality; reproducible patch/sign pipeline |
| AXP call-recording toggles (older Android) | Re-validate on Android 16; do not blind-port XML |
| AXP backup/restore tooling | NeoBackup (external); no project-owned backup feature |
| AXP AOSmium / extras | Explicit decide rows above — not silent omissions |
| WebView provider | Keep Android multi-provider path: install other WebViews + choose in Developer options |
## Project-specific additions
Not AXP Pro catalog features. Tracked here so they are not confused with parity.
| Feature | Origin | Objective | Status | Feature # |
|---------|--------|-----------|--------|-----------|
| FLAG_SECURE override | Project requirement | Native control: Android default remains default; **global** DO override only (no per-app); screenshots + screen capture; no LSPosed/Zygisk/root module | todo | `009` / `008-call-rec-flag-secure` |
| Optional root-unavailable boot mode | Project / msg-GrapheneOS inspiration | Diagnostic/fallback boot without root | later | after daily-driver MVP |
| Upstream deprecation / Kotlin warning hygiene | Build log noise on LOS 23.2 | **Last steps before final ROM freeze:** triage remaining compile warnings; migrate call sites API-by-API (or inherit LOS fixes); keep our patches warning-clean. Not a gate for `005`–`010` MVPs. | later | after `010` / with `011` freeze; see `docs/hardening-backlog.md` |
## Ownership
- Matrix maintained in this file (`docs/axp-pro-parity.md`).
- Rows may map to `004`–`013`; **`010` is not “all of AXP Pro parity”** — only
remaining usability/privacy items not owned elsewhere.
- `002` captures what the **current device** actually runs; this matrix is the
**product intent** relative to AXP Pro docs + that capture.
- Compatibility MVP ends at `006` (rootless); Daily-driver MVP ends at `010`
(constitution). Hardening candidates: `docs/hardening-backlog.md`.
+510
View File
@@ -0,0 +1,510 @@
# AXP.OS sunfish preservation / reference archive
Parent overview (Lineage + AXP + GrapheneOS): [`reference-sources.md`](reference-sources.md).
## Purpose
AXP.OS is an important historical and functional reference for
`sunfish-a16-rom`.
Some AXP.OS infrastructure is self-hosted and may disappear. Preserve **useful
publicly accessible material related to sunfish / AXP.OS A15** before relying on
it during later ROM development.
This archive is **REFERENCE** material. It is **not** the base ROM.
```text
GOLDEN device: AXP.OS 22.2 / Android 15 on sunfish
historical GOS: Android 13 sunfish (hardening reference only)
new ROM base: LineageOS 23.2 / Android 16
AXP sources: reference inputs only
```
**Do not** block ROM development indefinitely on recovering unavailable AXP
material.
**Do not** archive all ~225 Disroot repositories (Sony, OnePlus, Fairphone,
unrelated Pixels, etc.).
```text
AXP archive != complete backup of AXP.OS
AXP archive == what is needed to understand, reproduce, and port
AXP.OS A15 for SUNFISH
```
Execute this work on **`rom-dev01`** under `/srv/rom/references/axp/`. Do not
keep growing large mirrors on the laptop once the server copy is verified.
---
## When to run
- After `001` has `/srv/rom` and `android-build` SSH working.
- Before or in parallel with early `002` (does not replace GOLDEN live `adb`).
- Prefer rsync of the existing laptop archive first, then fill gaps on the server.
---
## Architecture reminder
| Role | Meaning |
|------|---------|
| GOLDEN | Live AXP.OS 22.2 / A15 — do not flash for experiments |
| LAB | LOS / project ROM experiments only |
| This archive | Offline docs + sunfish-related git/releases on `rom-dev01` |
See also: `docs/device-roles.md`.
---
## 1. Known AXP.OS hosting
### Main website
Preserve (rendered + git source):
- https://axpos.org/
- https://axpos.org/news/
- https://axpos.org/docs/
- https://axpos.org/devices/
- https://axpos.org/devices/google/sunfish/
- https://axpos.org/devices/changelogs/2026/
- https://axpos.org/docs/knowledge/faq/
- https://axpos.org/docs/knowledge/reproducible/
- https://axpos.org/docs/knowledge/microg/
- https://axpos.org/docs/knowledge/callrec/
- https://axpos.org/docs/knowledge/features/
- https://axpos.org/docs/knowledge/technical-details/
Website hosting: Codeberg.
Website source:
- https://codeberg.org/AXP-OS/axpos-org
- https://codeberg.org/AXP-OS/axpos-org.git
Observed branches: `hugo`, `pages`.
Official docs describe:
| Host | Role |
|------|------|
| **Disroot** (`git.disroot.org/AXP.OS`) | Main **public** mirror of repos / APK releases / reproducible-build data |
| **code.binbash.rocks/AXP.OS** | Complete self-hosted set (CI, internals) — **not** anonymously public |
| **Codeberg** | Partial (website + fewer repos after Jan 2026 removals) |
| **GitHub** (`github.com/AXP-OS`) | Mainly APK/release mirror |
---
### Public source mirror: Disroot
- Org: https://git.disroot.org/AXP.OS
- API: https://git.disroot.org/api/v1/orgs/AXP.OS/repos
Snapshot **2026-08-09** (inventory only — not a mandate to clone everything):
- Repos: **225**
- Forgejo reported size: **49,908,145 KiB (~48 GiB)** Git metadata only
- **Additional** space needed for LFS, releases, wikis, downloads
Generate inventory dynamically from the API. Do **not** maintain a hard-coded
full org clone list.
---
### Codeberg
- https://codeberg.org/AXP-OS
- Snapshot 2026-08-09: **14** public repos, complete attempt ~**38 GiB**
- Includes `axpos-org` website source
- **Not** a complete AXP source archive (100+ repos removed Jan 2026 per AXP docs)
Known laptop archive: `~/Archives/AXP.OS/codeberg/`
Known issue: `legacy_prebuilt_apps.git` has historical LFS pointers whose objects
return **HTTP 404** on Codeberg.
```text
"Git mirror valid" != "all historical LFS content preserved"
```
Do **not** delete the Codeberg archive just because Disroot is newer/larger.
---
### GitHub
- https://github.com/AXP-OS
- Documented focus: APK/release mirror (PhoneSky, AOSmium, OpenEUICC, …)
- Lower priority; useful independent source for artifacts/public keys
---
### Self-hosted Gitea (not publicly readable)
- https://code.binbash.rocks / https://code.binbash.rocks/AXP.OS
- Observed DNS 2026-08-09: `148.251.225.181`
- Site root HTTP 200; anonymous org API returned `[]`
- Historical repo URLs request auth; web 404
**Do not** attempt to bypass access controls. Only use legitimately public
material. Official docs: this host holds the complete private set.
---
## 2. Download / OTA infrastructure
### Clearnet downloads
- https://download.axpos.org (DNS 2026-08-09 → `148.251.225.182`, PTR yes.binbash.rocks)
- Root `/` may time out; **always probe exact paths**
Sunfish paths:
```text
PRO: https://download.axpos.org/axp/sunfish/
…/factory/ …/recovery/ …/tests/
SLIM: https://download.axpos.org/axp-slim/sunfish/
```
Device page confirms **AXP.OS 22.2 / Android 15** for Pro and Slim, with links
to OS zip, recovery, factory, and keys.
When reachable, preserve for sunfish (Pro + Slim as available):
- OS ZIPs, `.prop`, `.sha512sum`
- recovery / factory packages
- relevant test artifacts
- signing / AVB **public** keys
Never treat images as trusted without published checksums/signatures.
Example filenames seen in indexes (verify live):
```text
AXP.OS-22.2-20260324-PRO-sunfish.zip(+.prop+.sha512sum)
AXP.OS-22.2-20260331-PRO-sunfish.zip(+.prop+.sha512sum)
```
### Tor download mirror
Onion advertised by AXP:
```text
e3tx35xwvqoihx36tjmnzurjcghs6kjwrwnen55jg7ujqilbaicutpid.onion
HTTP :8008 (main documented alias)
HTTPS :9009 (historical)
```
Example:
```text
http://…onion:8008/axp/sunfish/
http://…onion:8008/axp-slim/sunfish/
```
Probe when preservation runs; do not assume availability.
### Legacy download host
`leech.binbash.rocks` — observed unavailable 2026-08-09 (timeouts / refused).
Treat as legacy; `download.axpos.org` replaced port-8008 URLs per changelog.
### OTA
```text
https://update.axpos.org
https://update-unstable.axpos.org
```
Both resolved to `148.251.225.182` (2026-08-09); `/` → HTTP 403 Apache.
403 on `/` ≠ OTA API dead (updater endpoints, not browsing).
Older (pre build 2026.005):
```text
https://sfxota.binbash.rocks:8010
https://sfxota-unstable.binbash.rocks:8010
```
If needed: capture the exact OTA URL from **GOLDEN** via `adb logcat` while
checking for updates — **do not modify GOLDEN** merely for discovery.
---
## 3. Historical IP snapshot (diagnostic only)
Observed **2026-08-09** — **never hard-code**; re-resolve DNS before use:
| Host | IPv4 |
|------|------|
| axpos.org | 217.197.84.141 |
| code.binbash.rocks | 148.251.225.181 |
| download.axpos.org | 148.251.225.182 |
| update.axpos.org | 148.251.225.182 |
| update-unstable.axpos.org | 148.251.225.182 |
| leech.binbash.rocks | 162.55.66.39 |
---
## 4. Existing laptop archive → server first
Laptop working location:
```text
~/Archives/AXP.OS/
website-rendered/
codeberg/
disroot-repositories.jsonl
mirror-forgejo-org.sh
…
```
**Do not** redownload blindly.
Once `rom-dev01` is available:
```bash
ssh android-build@rom-dev01 \
'mkdir -p /srv/rom/references/axp/archive'
rsync -aH --partial --info=progress2 \
"$HOME/Archives/AXP.OS/" \
android-build@rom-dev01:/srv/rom/references/axp/archive/
du -sh ~/Archives/AXP.OS
ssh android-build@rom-dev01 'du -sh /srv/rom/references/axp/archive'
```
Keep the laptop copy until the server copy is verified.
---
## 5. Suggested server layout
```text
/srv/rom/references/axp/archive/
├── website-rendered/
├── codeberg/
│ ├── git/
│ ├── releases/
│ └── metadata/
├── disroot/
│ ├── git/ # sunfish-selected repos only (see §8)
│ ├── releases/
│ └── metadata/
├── github/ # optional APK/release mirrors
├── downloads/
│ ├── sunfish-pro/
│ └── sunfish-slim/
├── status/
├── tools/ # e.g. mirror-forgejo-org.sh
└── README.md
```
Separate from any **working checkouts** used during active porting.
---
## 6. Storage preflight (mandatory)
```bash
df -h /srv/rom
du -sh /srv/rom/references/axp/archive 2>/dev/null || true
```
Preservation **must not** consume space needed for LOS tree, `out/`, ccache,
artifacts.
Full Disroot org (~48 GiB Git + LFS/releases) is **out of scope**. Prefer
sunfish-selected set (typically far smaller) + website + sunfish images.
---
## 7. What to preserve (sunfish-scoped)
### Always (small / high value)
1. Rendered website (`axpos.org`) + `sitemap.xml` / `robots.txt`
2. `axpos-org.git` (Codeberg)
3. Existing Codeberg org archive (rsync; refresh optionally)
4. Disroot org **inventory JSONL** (metadata only)
5. Status probe snapshots under `status/`
### Highest priority if downloads work
Sunfish Pro/Slim OS zip, recovery, factory, `.prop`, `.sha512sum`, public keys
(clearnet path first; Tor fallback).
### Git (Disroot / public) — selection rule
**Do not** clone all 225 repos.
**Do** discover the exact set from **sunfish A15 / 22.2** reproducible-build /
manifest / `axp_versioning` data, then clone **that** set (+ histories).
Minimum explicit interest (seed list — expand from manifests):
| Priority | Examples |
|----------|----------|
| Device/kernel | `android_device_google_sunfish`, `android_kernel_google_msm-4.14` |
| Versioning | `axp_versioning`, sunfish-related manifests / `axp.xml` tags |
| Likely port targets | `android_frameworks_base`, `android_packages_apps_Settings`, shared `build`/config **if** present in sunfish build |
| Compat features | microG / signature spoofing / Phonesky-related public repos |
| Features we want | call recording, FLAG_SECURE-related, hardening **used by sunfish A15** |
Exclude by default: other OEMs/devices, unrelated Pixel generations, bulk
device trees not referenced by the sunfish A15 build.
### Lower priority
GitHub APK/release mirrors for apps we care about (Phonesky, etc.).
### Out of scope
Private `code.binbash.rocks` content without credentials.
---
## 8. Manifest-driven repo selection (preferred workflow)
1. From archived docs / Disroot, locate sunfish **22.2 / A15** reproducible
tags and `axp_versioning` / manifest (`axp.xml` or equivalent).
2. Extract the list of repositories + revisions for that build.
3. Clone `--mirror` **only those** repos (plus any tiny shared deps they need).
4. Record the selection in
`/srv/rom/references/axp/archive/status/sunfish-repo-selection.txt`
with tag/build id and date.
5. `git fsck --full` per mirror; `git lfs fetch --all` where applicable; log LFS
failures separately (404s are expected for some historical objects).
Fallback if manifests are incomplete: inventory API + name filters
(`sunfish`, `msm-4.14`, `phonesky`, `microg`, `signature`, `spoof`, `callrec`,
`versioning`, `manifest`, …) **plus** manual review — still not “clone org”.
---
## 9. Website commands (server refresh)
```bash
mkdir -p /srv/rom/references/axp/archive/website-rendered
wget \
--mirror --page-requisites --convert-links --adjust-extension \
--restrict-file-names=unix --domains=axpos.org \
--directory-prefix=/srv/rom/references/axp/archive/website-rendered \
https://axpos.org/
curl -fL https://axpos.org/sitemap.xml \
-o /srv/rom/references/axp/archive/website-sitemap.xml || true
curl -fL https://axpos.org/robots.txt \
-o /srv/rom/references/axp/archive/website-robots.txt || true
```
Prefer transferring the existing laptop `website-rendered/` and `axpos-org.git`
via rsync; refresh only if needed:
```bash
git --git-dir=axpos-org.git remote update --prune
git --git-dir=axpos-org.git show-ref
git --git-dir=axpos-org.git fsck --full
```
---
## 10. Forgejo inventory (Disroot / Codeberg)
```bash
tmp="$(mktemp)"; page=1
while :; do
data="$(curl -fsSL \
"https://git.disroot.org/api/v1/orgs/AXP.OS/repos?limit=50&page=$page")"
count="$(jq 'length' <<<"$data")"
[ "$count" -eq 0 ] && break
jq -c '.[]' <<<"$data" >> "$tmp"
page=$((page + 1))
done
cp "$tmp" /srv/rom/references/axp/archive/disroot/metadata/disroot-repositories.jsonl
rm "$tmp"
```
Useful queries: largest repos; name filter for sunfish-interest (see research
notes). Same pattern for Codeberg org `AXP-OS`.
If using `mirror-forgejo-org.sh` from the laptop archive:
- **Codeberg**: OK to refresh the small org into `archive/codeberg/`
- **Disroot**: do **not** invoke a full-org mirror by default; use
manifest-selected clones into `archive/disroot/git/`
Scripts must be resumable; one repo failure must not abort the batch; prefer
`git remote update --prune` over delete/reclone.
---
## 11. Service / path probes
Probe roots **and** exact sunfish paths (`/axp/sunfish/`, `/axp-slim/sunfish/`).
Save under `archive/status/` with `date -Is`.
Tor fallback (optional):
```bash
# apt: tor torsocks — only if needed
curl --socks5-hostname 127.0.0.1:9050 --connect-timeout 30 --max-time 120 -I \
'http://e3tx35xwvqoihx36tjmnzurjcghs6kjwrwnen55jg7ujqilbaicutpid.onion:8008/axp/sunfish/'
```
Mine archived website for URLs:
```bash
grep -RhoE 'https?://[^"<> ]+' \
/srv/rom/references/axp/archive/website-rendered/axpos.org 2>/dev/null |
grep -Ei 'axpos|binbash|codeberg|disroot|github|sunfish' | sort -u \
> /srv/rom/references/axp/archive/discovered-urls.txt
```
---
## 12. Verification
```bash
find /srv/rom/references/axp/archive -type d -name '*.git' -prune | wc -l
du -sh /srv/rom/references/axp/archive/*
date -Is > /srv/rom/references/axp/archive/SNAPSHOT_DATE.txt
```
Document LFS/release errors. Do not claim 100% completeness if objects 404.
---
## 13. Priority order (execution)
1. rsync laptop `~/Archives/AXP.OS/` → server; verify `du`
2. Storage preflight (`df`)
3. Website rendered + `axpos-org.git` (if not already complete)
4. Probe/download **sunfish** Pro/Slim images + checksums/keys (clearnet → Tor)
5. Disroot inventory JSONL + **manifest-driven** sunfish repo mirrors
6. Optional Codeberg refresh; optional GitHub APK repos of interest
7. Status snapshot + selection file
Skip: full Disroot org; private binbash Gitea; endless LFS 404 retries.
---
## 14. Later development use
Do **not** blindly cherry-pick AXP into LOS 23.2.
For each candidate:
1. Identify repo + AXP branch/tag/build (prefer sunfish 22.2 / A15)
2. Identify corresponding LOS/AOSP base
3. Inspect full diff
4. Classify: direct / port / obsolete / superseded / A16-incompatible
5. Record provenance; dedupe vs LOS / GrapheneOS / DivestOS
6. Apply one logical change; build; test on **LAB** only
AXP A15 GOLDEN remains the live oracle.
LOS 23.2 / Android 16 remains the base.
+100
View File
@@ -0,0 +1,100 @@
# ChatGPT conversation continuity
Imported context: 2026-09-13 from [mi ROM](https://chatgpt.com/share/6aa718c6-c834-83ed-9e2d-2b18fc1ad141).
This is a non-normative continuity note, not a replacement for the constitution
or feature specs. The shared page was retrieved and its user/assistant text
extracted. The payload contains repeated messages; extraction order is not a
reliable chronology. Linked attachments, redacted tool outputs and the original
external citations have not been independently recovered or verified. Historical
assistant suggestions are not new user instructions or verified technical facts.
The raw conversation is not copied into Git because it includes unrelated
personal topics and diagnostic data.
## Intent and settled direction
- Build a personal, maintainable ROM for **Pixel 4a / sunfish only**, with
LineageOS 23.2 / Android 16 as the base and AXP.OS Pro as the functional
reference. The motivation is continuity beyond AXP maintenance.
- Functionality comes first, especially microG, patched Google Play and banking
inside Insular. Yuh and Swissquote are the required targets; N26 and Revolut
were optional challenges, not reasons to block the project.
- Preserve two-device separation: GOLDEN is the daily AXP reference; LAB is for
experiments. The conversation corrected an earlier A13 mix-up: GOLDEN is AXP
A15; historical GrapheneOS sunfish is A13. Never revive the earlier assumption
that there is only one phone or that GOLDEN must be wiped/restored.
- Keep the compatible LineageOS kernel/device stack. Study historical sunfish
GrapheneOS, modern applicable GrapheneOS techniques, and AXP/DivestOS changes;
do not transplant an old GrapheneOS or stock A13 kernel into the A16 ROM.
- Rootless compatibility comes first (`006`), then KernelSU-Next (`007`) with
a complete banking regression. Magisk describes the existing AXP workflow,
not an intermediate production architecture for the new ROM.
- Native call recording and configurable FLAG_SECURE override matter for daily
use. Prefer integrating these into the ROM over depending on root modules.
- Insular starts as a user app; Managed Mainland is optional. Preserve the
historical Aurora → do not launch → deny root → Insular first launch → Play
update workflow as reference, with exact current behaviour measured in `002`.
- NeoBackup was accepted as an external personal tool. A custom TWRP-like full
backup implementation is not part of the ROM project.
- Signing, AVB/relocking and OTA remain later release work. The conversation
raised hardware attestation explicitly: do not promise Play Integrity or
hardware-attestation success merely because the ROM has KernelSU or custom
signing. Validate actual banking behaviour and research current requirements
when that feature is implemented.
## Additional research context
The last Phonesky discussion considered module-based installers, but retained
the AXP-style patch/build/sign pipeline as the intended direction. It identified
`https://github.com/AXP-OS/packages_apps_phonesky` as a research lead for the
patcher, versioned diffs and release automation. Revalidate upstream before use;
do not treat the historical version or maintenance claims as current facts.
Stable signing identity and subsequent APK updates belong in feature `004`.
Privacy candidates included SUPL identifiers, Build.SERIAL restrictions,
package visibility, MAC privacy, analytics removal, hosts/DNS filtering,
certificate review, auto-reboot, radio timeouts and log reduction. Preserve them
through [the existing backlog](hardening-backlog.md), promoting requirements
into specs before implementation.
## Working preferences and infrastructure
The user prefers Spanish conversation, concrete next steps and manageable stages;
large speculative plans and repeated reopening of settled decisions are tiring.
Technical project documentation remains English.
The laptop has limited storage. Android trees, builds and growing reference
archives belong on `rom-dev01`; local work centres on editing, SSH, adb/fastboot
and artifact transfer. The agreed planning capacity was **6 vCPU, 52 GB RAM,
32 GB swap and approximately 2 TB expandable disk**. The chat explicitly
corrected assistant claims of 16 vCPU/64 GB/750 GB and confusion between laptop
free space and server capacity. Actual host measurements still take precedence.
Keep independent Gitea repositories, Ansible and Semaphore; do not fold this
personal project into company GitLab, Redmine or Salt workflows. VM provisioning
remains outside this repository. Public IP, public web access, SSH tunnels and
possible future WireGuard were discussed separately. That discussion is not
authorization to expose services: current constitution/spec access rules govern.
AXP preservation is scoped to sunfish and relevant shared dependencies, not all
225 Disroot repositories or every GrapheneOS archive. Transfer and verify the
existing laptop archive before redundant downloads. Historical LFS failures and
service outages are observations, not proof of current availability. Existing
[reference sources](reference-sources.md) and
[preservation instructions](axp-sunfish-preservation.md) already capture this.
## Current repository supersedes historical waiting state
The chat ended its planning phase waiting for `rom-dev01` before implementing
`001`. That is historical context, not today's task status. Repository records
dated 2026-09-13 already describe host work, the `002` handoff, and `003` source
initialization. See [002 handoff](002-gate-handoff.md) and
[003 build notes](003-build-notes.md); live execution status was not checked
during this context import.
In particular, `002` now documents a Yuh failure on GOLDEN. The earlier statement
that both banks worked is a historical observation, not a substitute for the
current inventory or a reason to mark compatibility passed.
Resume from current feature evidence and tasks, preserving this conversation's
intent without replaying old commands or overwriting newer decisions.
+35
View File
@@ -0,0 +1,35 @@
# Device roles — two Pixel 4a (sunfish)
Project assumption: **two physical sunfish devices**.
| Role | Use | Flash / experiment? |
|------|-----|---------------------|
| **GOLDEN** | Daily-driver **AXP.OS Pro 22.2 / Android 15** (main phone) | **NO** — never flash/wipe/modify; **`adb` sample only** |
| **LAB** | Experiments / our ROM (as found: **iodé** / Lineage 7.8 A16) | **YES** — flash freely; factory rescue ready |
## Measured GOLDEN (2026-09-13)
| Field | Value |
|-------|--------|
| Serial | `11071JEC203839` |
| Build | `22.2-2026.003-sunfish-pro` (`ro.lineage.releasetype=PRO`), Android **15**, patch `2026-03-03` |
| Bootloader | **unlocked** (`flash.locked=0`, `verifiedbootstate=orange`) |
| Magisk | **29.0** (`/system_ext/bin/magisk`) |
| Work profile | Island user 10; Yuh + Swissquote in Island only |
| Inventory | [`002-golden-inventory.md`](002-golden-inventory.md) |
## Measured LAB (2026-09-13)
| Field | Value |
|-------|--------|
| Serial | `11231JEC207477` |
| As found | iodé / `7.8-20260902-sunfish`, Android **16**, patch `2026-08-01` |
| Bootloader | **locked** |
| Baseline note | [`002-lab-iodes-baseline.md`](002-lab-iodes-baseline.md) |
| Factory rescue | [`002-lab-stock-rescue.md`](002-lab-stock-rescue.md) — SHA-256 **VERIFY_OK** |
## Implications
- NeoBackup / wipe of GOLDEN is **not** a project requirement.
- `003+` flashing **only on LAB**.
- **Do not flash GOLDEN** under any circumstance.
+119
View File
@@ -0,0 +1,119 @@
# Hardening backlog (non-normative)
Candidate hardening ideas for later evaluation. **Not requirements** until a row
is promoted into an active feature `spec.md` (`012` / `013` or related).
Authority order for an active feature:
1. Constitution
2. That feature's `spec.md`
3. This backlog / `docs/axp-pro-parity.md` (hints only)
Status: `candidate` | `investigate` | `accepted→spec` | `rejected` | `los-provides`
## Privacy / identifiers
| Idea | Notes | Status |
|------|-------|--------|
| SUPL IMSI/MSISDN exposure | Reduce carrier ID leakage via SUPL | candidate |
| Build.SERIAL restrictions | Identifier privacy | candidate |
| Analytics / statistics removal | Drop unused telemetry | candidate |
| Package query visibility | Tighten package visibility | candidate |
| MAC / address privacy | Wi-Fi/BT randomization policy | candidate |
| Private DNS defaults | Sensible defaults without breaking banks | candidate |
| CA-store review | Trust anchors audit | candidate |
## Userspace
| Idea | Notes | Status |
|------|-------|--------|
| hardened_malloc | Graphene/AXP portability on sunfish | candidate |
| Bionic hardening | Selective, attributable | candidate |
| JNI protections | Safer JNI boundaries | candidate |
| Stack canaries | Confirm / strengthen where applicable | candidate |
| ptrace / Yama | Restrict debugging attach | candidate |
| seccomp | Incremental filter improvements | candidate |
| noexec / nosuid / nodev mounts | Where applicable without breaking HW | candidate |
| Dynamic code / JIT restrictions | Evaluate app compatibility cost | candidate |
| Exec spawning / ASLR-related | Attack-surface reduction | candidate |
## Kernel
| Idea | Notes | Status |
|------|-------|--------|
| Historical GrapheneOS sunfish kernel patches/config | Device-specific commits **and** config known to have run on sunfish — not CONFIG-only review | candidate |
| Modern GrapheneOS kernel techniques | Evaluate equivalent/backport on sunfish 4.14; no blind cherry-picks | candidate |
| AXP/DivestOS sunfish kernel patch audit | Identify AXP-owned vs inherited vs upstream; de-duplicate against LOS | candidate |
| Applicable CVE backports | Document EOL firmware vs patchable kernel | candidate |
| CFI | Feasibility on 4.14 | candidate |
| ASLR config | Kernel/userspace related knobs | candidate |
| IPv6 privacy | Address generation / exposure | candidate |
| Debug / UART restrictions | Reduce attack surface | candidate |
| Attack-surface reduction | Drop unused drivers/configs carefully | candidate |
## Kernel hardening source policy
The kernel base remains the LineageOS 23.2-compatible sunfish kernel.
```text
┌─ historical GOS sunfish
│
LOS23/A16 sunfish 4.14 ←─┼─ AXP own + DivestOS
OUR BASE │
├─ modern GOS techniques
│
└─ original upstream fixes
```
**Not:** GrapheneOS Android 13 (or any old GrapheneOS) kernel tree as our base.
Candidate hardening comes from four classes:
1. **Historical GrapheneOS sunfish** — device-specific patches/configuration known
to have run on sunfish (real commits, not only `.config`).
2. **AXP.OS / DivestOS sunfish** — audit the actual patch stack; distinguish
AXP-owned changes from inherited changes.
3. **Modern GrapheneOS** — use current kernel hardening as a source of
techniques/security properties; adapt/backport only where feasible on
non-GKI Linux 4.14; do not assume current Pixel kernel patches cherry-pick
cleanly.
4. **Original upstream** — where a GrapheneOS/AXP/Divest patch derives from AOSP,
Android common, Linux stable, or another upstream source, prefer the
appropriate original upstream fix when practical.
For each candidate record:
- security problem / property;
- original source and commit;
- source lineage / provenance;
- sunfish-specific or generic;
- already present in LOS 23.2?;
- already present through another patch?;
- kernel/hardware prerequisites;
- applicability to Linux 4.14;
- port/backport required;
- compatibility/regression risk;
- test result;
- final decision.
## Much later / evaluate
| Idea | Notes | Status |
|------|-------|--------|
| Per-app network control | High UX/complexity | candidate |
| Sensors permission model | Graphene-like | candidate |
| Storage Scopes-like behaviour | Compatibility cost | candidate |
| Contact Scopes-like behaviour | Compatibility cost | candidate |
| Duress PIN/password | Later | candidate |
| USB restrictions | Policy toggles | candidate |
| Exploit-protection toggles | User-facing switches | candidate |
| Modernize LOS API deprecations / Kotlin annotation targets | **Final-ROM hygiene (last steps)**, not a mid-feature gate. Measured on `005` bacon 2026-09-15 SUCCESS (~45k log lines): ~3k deprecated, ~4.5k KT-73255 annotation-target, ~8.7k “warning” hits total; **0** hard `error:`/`FAILED:` in successful run. Themes: Settings Slices `[removal]`; SystemUI KeyguardState/SceneContainer/CommunalScenes; DesktopMode desk APIs; `FingerprintManager`/`Executors`; SettingsLib `@StringRes` KT-73255. Fix **API-by-API** with attributable ports; never megapatch or `-Xlint:-deprecation` as “done”. Prefer waiting for LOS merges; only port what still warns at freeze. Own patches (`005` spoofing, vendor) must stay clean first. Evidence: `/srv/rom/artifacts/005-spoofing/20260915-bacon/bacon-log-triage.md`. | candidate |
## EOL firmware caveat (for `013`)
sunfish is EOL. The project can modify kernel/framework we build, but cannot
fabricate new proprietary vendor/modem firmware. Document clearly:
- security patches we can backport in-tree;
- old proprietary firmware/baseband/vendor images we **cannot** fix;
- do not claim hardware protections unsupported by Pixel 4a.
+365
View File
@@ -0,0 +1,365 @@
# Reference sources — LineageOS, AXP.OS, GrapheneOS (sunfish only)
Project documentation consolidating where sources live, what role they play, what
is already archived locally, and what to execute later on `rom-dev01`.
**Scope: Google Pixel 4a (`sunfish`) only.** Do not plan or archive unrelated
devices (other Pixels, OnePlus, Fairphone, Sony, …).
```text
LineageOS 23.2 / Android 16 → AUTHORITATIVE CURRENT BASE (build from this)
AXP.OS 22.2 / Android 15 → GOLDEN / functional reference (live device + selective archive)
GrapheneOS Android 13 sunfish → HISTORICAL hardening reference (selective archive)
```
Do **not** execute server archive/download/build commands from this file until
`rom-dev01` exists. Do **not** treat this as work on frozen feature `001`.
Detailed AXP hosting probes, Tor/download URLs, and Forgejo inventory commands:
[`axp-sunfish-preservation.md`](axp-sunfish-preservation.md).
Device roles (GOLDEN vs LAB): [`device-roles.md`](device-roles.md).
---
## Classification legend
| Label | Meaning |
|-------|---------|
| **AUTHORITATIVE CURRENT SOURCE** | Use for builds / official current truth |
| **HISTORICAL REFERENCE** | Study/port only; not the build base |
| **LOCAL ARCHIVE** | Copy under `/srv/rom/references/…` or laptop path |
| **UNAVAILABLE / PRIVATE** | Do not scrape or bypass access controls |
| **TO BE EXECUTED LATER ON rom-dev01** | Commands/actions deferred until host is ready |
---
## Target layout on rom-dev01
```text
/srv/rom/references/
├── axp/
│ ├── archive/ # see axp-sunfish-preservation.md
│ ├── manifests/ # sunfish A15 selection / versioning notes
│ └── README.md
├── lineage/
│ ├── manifests/ # repo manifest -r snapshots (reproducibility)
│ └── README.md
└── grapheneos/
├── mirrors/ # selective bare clones only
├── manifests/
├── metadata/
└── README.md
```
Plus the normal **build checkout** (not an “archive”):
```text
/srv/rom/src/lineage-23.2/ # full Lineage tree used to compile (feature 003+)
```
---
# 1. LineageOS 23.2 / Android 16 — BASE
## Role
**AUTHORITATIVE CURRENT SOURCE** for the ROM. Compiling = syncing and building
this tree for sunfish. No need for a redundant mass mirror of the entire
LineageOS GitHub organization.
## Authoritative URLs
| Item | URL | Class |
|------|-----|--------|
| Android / repo umbrella | https://github.com/LineageOS/android | AUTHORITATIVE CURRENT |
| Device sunfish | https://github.com/LineageOS/android_device_google_sunfish | AUTHORITATIVE CURRENT |
| Kernel msm-4.14 | https://github.com/LineageOS/android_kernel_google_msm-4.14 | AUTHORITATIVE CURRENT |
| Wiki device page | https://wiki.lineageos.org/devices/sunfish/ | AUTHORITATIVE CURRENT (verify against repos) |
**Note:** Indexed/cached wiki install pages may still mention older branches
(e.g. 22.2). For branch and source tree truth, trust **current GitHub branches /
official `repo init`**, not a stale wiki cache. As of project research,
`android_device_google_sunfish` and `android_kernel_google_msm-4.14` expose
**`lineage-23.2`**.
## Official source init
```bash
repo init \
-u https://github.com/LineageOS/android.git \
-b lineage-23.2 \
--git-lfs
repo sync
```
(Exact path on host: e.g. `/srv/rom/src/lineage-23.2` — set during `003`.)
## What to preserve locally (not a full org archive)
**TO BE EXECUTED LATER ON rom-dev01** after syncs/builds:
```bash
mkdir -p /srv/rom/references/lineage/manifests
cd /srv/rom/src/lineage-23.2 # or actual checkout path
# After first successful sync:
repo manifest -r \
-o /srv/rom/references/lineage/manifests/lineage-23.2-initial.xml
# After first known-good vanilla sunfish boot/build:
repo manifest -r \
-o /srv/rom/references/lineage/manifests/lineage-23.2-sunfish-vanilla-known-good.xml
```
Also keep short README under `/srv/rom/references/lineage/README.md` with:
- `repo init` command used
- date / host
- pointer to known-good images under `/srv/rom/artifacts/` (hashes)
## Explicitly out of scope for Lineage “archive”
- `git clone --mirror` of hundreds of LineageOS repos “just in case”
- Mirroring unrelated device trees
The build checkout **is** the working copy; revision-locked manifests are the
reproducibility archive.
---
# 2. AXP.OS 22.2 / Android 15 — GOLDEN / functional reference
## Role
**HISTORICAL + FUNCTIONAL REFERENCE** (and live GOLDEN device). Not the build
base. Infrastructure risk is higher (self-hosted pieces may vanish) → selective
archive matters more than for Lineage.
Full operational cookbook: [`axp-sunfish-preservation.md`](axp-sunfish-preservation.md).
## Source classes
| Source | Class | Notes |
|--------|-------|--------|
| GOLDEN phone (AXP 22.2 / A15) | AUTHORITATIVE CURRENT *for behaviour* | Live `adb`; do not flash |
| https://axpos.org (docs/news/devices) | AUTHORITATIVE CURRENT *docs* (while up) | Hosted via Codeberg |
| https://git.disroot.org/AXP.OS | AUTHORITATIVE CURRENT *public git* | Main public mirror (~225 repos; **do not clone all**) |
| https://codeberg.org/AXP-OS | HISTORICAL / PARTIAL | Website git + fewer repos after Jan 2026 removals |
| https://github.com/AXP-OS | AUTHORITATIVE for some **APK/releases** | Lower priority mirror |
| https://code.binbash.rocks/AXP.OS | UNAVAILABLE / PRIVATE anonymously | Alive; no anonymous repo list; do not bypass |
| https://download.axpos.org/…/sunfish/ | AUTHORITATIVE downloads (when reachable) | Probe exact paths |
| Tor onion `:8008` | FALLBACK downloads | See preservation doc |
| `leech.binbash.rocks` | UNAVAILABLE (2026-08-09 probes) | Legacy |
## Sunfish-only archive criteria
Preserve/reference **only**:
- documentation/web useful to the project
- `android_device_google_sunfish`, `android_kernel_google_msm-4.14`
- manifests / `axp_versioning` needed to identify exact AXP sunfish revisions
- common repos **only when** they contain AXP changes applicable to sunfish
(e.g. frameworks/base, Settings, build/config, microG/spoofing, Phonesky,
callrec, FLAG_SECURE, hardening) — **selected via sunfish A15 manifests**,
not by cloning the whole org
- sunfish Pro/Slim builds, recovery, factory, `.prop`, checksums, public keys
where recoverable
**Discarded:** full Disroot “225 repos / ~48 GiB Git” org mirror as a project
goal.
## LOCAL ARCHIVE (laptop today)
```text
~/Archives/AXP.OS/
website-rendered/
codeberg/ # ~14 repos / ~38 GiB; LFS gaps in legacy_prebuilt_apps
disroot-repositories.jsonl
mirror-forgejo-org.sh
…
```
## TO BE EXECUTED LATER ON rom-dev01
1. rsync laptop archive → `/srv/rom/references/axp/archive/`
2. `df` preflight (must not steal LOS/`out`/ccache space)
3. Refresh website / selected Disroot mirrors via **manifest-driven** selection
4. Probe/download sunfish Pro/Slim artifacts if hosts allow
5. Never attempt private binbash access
See [`axp-sunfish-preservation.md`](axp-sunfish-preservation.md) for commands.
---
# 3. GrapheneOS — Android 13 sunfish (historical hardening)
## Role
**HISTORICAL REFERENCE** for hardening techniques/patches that once ran on
sunfish. **Not** the ROM base. Official project: sunfish **unsupported**; old
trees moved toward **GrapheneOS-Archive**.
Confirms project rule: GrapheneOS sunfish = **A13 history**, never A16 base.
## Authoritative / archive URLs
| Item | URL | Class |
|------|-----|--------|
| Current GrapheneOS orgs | https://github.com/GrapheneOS | AUTHORITATIVE CURRENT (active devices) |
| Platform manifest (current) | https://github.com/GrapheneOS/platform_manifest | AUTHORITATIVE CURRENT |
| Official historical archive org | https://github.com/GrapheneOS-Archive | HISTORICAL REFERENCE |
| sunfish device (archived) | https://github.com/GrapheneOS-Archive/device_google_sunfish | HISTORICAL — archive/read-only (e.g. since 2025-12-04); branch **13** |
| Releases / changelog | https://grapheneos.org/releases | AUTHORITATIVE for release history (A13 era mentions Pixel 4a) |
| FAQ | https://grapheneos.org/faq | AUTHORITATIVE docs |
| Example reorganized tree | `GrapheneOS-Archive/platform_frameworks_base-old` (read-only) | HISTORICAL — illustrates repo moves |
**Do not** mirror all ~244 repos in GrapheneOS-Archive.
## Selection rule (same spirit as AXP)
1. From last sunfish **Android 13** GrapheneOS manifests/tags, list repos/commits
that carried GrapheneOS deltas relevant to sunfish.
2. Mirror **only** those (plus `device_google_sunfish` + useful manifests).
3. Likely candidates after proof: frameworks, system/core, SELinux, malloc,
kernel/configs, etc. — **only if** tied to sunfish A13 lineage.
4. Record provenance; port carefully to LOS 23.2 / 4.14 (see constitution /
`docs/hardening-backlog.md`).
## TO BE EXECUTED LATER ON rom-dev01 (minimum seed)
```bash
mkdir -p /srv/rom/references/grapheneos/{mirrors,manifests,metadata}
git clone --mirror \
https://github.com/GrapheneOS-Archive/device_google_sunfish.git \
/srv/rom/references/grapheneos/mirrors/device_google_sunfish.git
git clone --mirror \
https://github.com/GrapheneOS/platform_manifest.git \
/srv/rom/references/grapheneos/mirrors/platform_manifest.git
git --git-dir=/srv/rom/references/grapheneos/mirrors/device_google_sunfish.git fsck --full
git --git-dir=/srv/rom/references/grapheneos/mirrors/device_google_sunfish.git show-ref
```
Then expand selectively from A13 sunfish manifests/tags — not the whole Archive
org.
## LOCAL ARCHIVE
No complete GrapheneOS sunfish archive is assumed on the laptop yet (unlike
AXP). Seed mirrors are **TO BE EXECUTED LATER ON rom-dev01**.
---
# 4. Conceptual comparison
| | LineageOS | AXP.OS | GrapheneOS (sunfish) |
|--|-----------|--------|----------------------|
| Android | 16 (23.2) | 15 (22.2) | 13 (last) |
| Role | **BASE** | **GOLDEN / functional** | **Hardening history** |
| Full tree checkout | Yes (to build) | No (selective) | No (selective) |
| Mass org mirror | No | No | No |
| Key local artifact | `repo manifest -r` XML | web + sunfish repos + images | device_sunfish + selected A13 repos |
| Urgency | Low (GitHub active) | Higher (self-host risk) | Medium (already moving to Archive) |
---
# 5. Shared porting discipline
For **any** AXP or GrapheneOS candidate change:
1. Identify source repo + tag/branch/build (sunfish-relevant)
2. Identify corresponding LOS 23.2 / AOSP base
3. Inspect full diff
4. Classify: direct / port / obsolete / superseded / A16- or 4.14-incompatible
5. Deduplicate vs LOS / the other reference
6. One logical change; build; test on **LAB** only
---
# 6. Execution status checklist
| Item | Status |
|------|--------|
| Document Lineage URLs + manifest `-r` strategy | Done (this file) |
| Document AXP sunfish-scoped preservation | Done (`axp-sunfish-preservation.md` + this file) |
| Document GrapheneOS Archive sunfish strategy | Done (this file) |
| Laptop AXP archive under `~/Archives/AXP.OS/` | Exists (operator); rsync later |
| rom-dev01 AXP/Lineage/GOS archives | **TO BE EXECUTED LATER** |
| Lineage full sync / first `manifest -r` | **TO BE EXECUTED LATER** (`003`) |
| Feature `001` implement | Not started (frozen docs) |
| Features `002`/`003` | Not started |
---
# 7. Contradictions / pitfalls noted
1. **Wiki vs GitHub for Lineage:** install wiki caches may show 22.2; repos show
`lineage-23.2` — trust repos/`repo init`.
2. **AXP “~48 GiB Disroot” ≠ required archive size** — that is whole-org Git
metadata; project policy is sunfish-selected only.
3. **Codeberg AXP archive ≠ complete** — LFS 404s (e.g. `legacy_prebuilt_apps`);
keep anyway; do not claim 100% LFS.
4. **download.axpos.org `/` timeout ≠ paths dead** — probe `/axp/sunfish/` etc.
5. **OTA `/` → 403** does not mean updater API is down.
6. **GrapheneOS-Archive growth** — sunfish material is being archived; prefer
Archive URLs + manifests over assuming old live GrapheneOS device support.
7. **AXP Android version:** GOLDEN is **22.2 / A15**; last GrapheneOS sunfish is
**A13** — do not conflate.
---
# 8. Community inspiration (non-base) — LOS + microG + KSU-Next + SusFS
**Not** our ROM base. Useful field report for sunfish banking / root / SusFS.
| Item | Detail |
|------|--------|
| Thread | [XDA: UNOFFICIAL sunfish LOS 22.2 + MicroG + KSU-Next + SusFS](https://xdaforums.com/t/rom-unofficial-sunfish-pixel-4a-lineageos-22-2-microg-ksu-next-susfs.4772261/) (torukk) |
| Ship | LOS **22.2** / A15 + upstream microG + kernel KSU-Next + SusFS |
| Kernel tree (author) | https://github.com/abu-adm/android_kernel_google_msm-4.14 (branch noted compatible with LOS23 kernel work) |
| Claims | Revolut / hard banks workable with **spoofed** KSU-Next manager + SusFS + **TrickyStore** + microG “unblock hardware attestation”; integrity landscape changes fast |
| LOS 23.2 note (forum) | Boot image with SusFS/ReSukiSU flashed onto official LOS 23.2: SusFS works, but ReSukiSU config unstable, heat, sudden reboots vs Magisk |
| Our use | Inspiration for `006`/`007` (profiles, SusFS, attestation modules on **LAB**). Still build **our** LOS 23.2 vanilla first (`003`), then microG/Phonesky, then KSU-Next. Do **not** adopt as drop-in base; layer AXP/Graphene call-rec / FLAG_SECURE / hardening later as planned |
If iodé/vanilla keep failing Yuh KeyMint/RKP, this thread’s stack (SusFS + TrickyStore + microG HA toggles) is the first **LAB** experiment family to evaluate — after stock factory diagnosis when convenient.
---
# 9. Community inspiration (non-base) — rooted GrapheneOS OTAs
**Not** our ROM base. **Not** for sunfish (EOL; project targets modern supported Pixels).
| Item | Detail |
|------|--------|
| Repo | https://github.com/schnatterer/rooted-graphene |
| Idea | Patch **Magisk** into official GrapheneOS OTAs; keep **AVB + locked bootloader** + root; own OTA channel via [Custota](https://github.com/chenxiaolong/Custota) / GitHub Pages; switch Magisk ↔ rootless by OTA |
| Tooling of interest | [avbroot](https://github.com/chenxiaolong/avbroot), Custota, signed OTA pipeline |
| Limits (upstream notes) | Stock Magisk Zygisk broken in this setup → banking detection; optional third-party Magisk fork; **KernelSU hard to integrate** with this Magisk-in-OTA approach |
| Our use | Inspiration for later **`011`** (project signing / AVB / OTA) and for “locked + root” trade-offs — **not** a Magisk production path (constitution: KernelSU-Next). Do not treat as GrapheneOS-on-sunfish revival |
Contrast with §8 (torukk): that one is **sunfish + KSU-Next + SusFS + LOS**; this one is **modern Graphene + Magisk + locked AVB OTA**. Different lessons for different stages.
---
# 10. Attestation / banking compatibility references
Primary reading when designing `006`/`007` attestation strategy (not sunfish-specific
fixes by themselves):
| Source | URL | Why keep |
|--------|-----|----------|
| GrapheneOS attestation compatibility guide | https://grapheneos.org/articles/attestation-compatibility-guide | Explains **hardware attestation API** vs Play Integrity; `SelfSigned` + whitelist of official GOS verified-boot keys (`attestation.json`); why modern supported Pixels can work with banks that whitelist GOS; sunfish **not** on that key list (EOL) |
| PrivSec banking apps compat report (issue) | https://github.com/PrivSec-dev/banking-apps-compat-report/issues/509 | Crowdsourced banking/attestation outcomes across ROMs (incl. Graphene); use for comparative evidence, not as a recipe dump |
**Project takeaway:** Yuh/Swissquote working on **supported** GrapheneOS tracks
whitelist + healthy TEE/AVB on current Pixels. Our sunfish path still needs
LOS+microG/Phonesky + measured KeyMint/RKP behaviour; GOS docs inform *what*
apps check, not a drop-in port.
+87
View File
@@ -0,0 +1,87 @@
# Session handoff — 2026-09-16 (LAB / 007)
**For the next chat:** read this file first, then `docs/007-kernelsu-runbook.md`.
**Prior chat (full context):** Cursor transcript id
`a601104c-c593-45d3-b737-545a7838957d`
(path: `~/.cursor/projects/home-aventura-Workspaces-sunfish-a16-rom/agent-transcripts/a601104c-c593-45d3-b737-545a7838957d/`).
**Rules:** working tree only (no commit / push / MR). LAB only
`11231JEC207477` — never GOLDEN.
---
## Project spine (do not forget)
We did **not** start from abu or from LOS 22 as the product base.
1. **`003`** — LOS **23.2** vanilla on LAB
2. **`004`/`005`** — microG + patched Play (Phonesky) + restricted spoofing → bacon zip
3. **`007`** — our ReSukiSU root on **our** `msm-4.14`, then modules / SusFS / banks
**ROM zip (ours):**
`.local-artifacts/005-spoofing/20260915-bacon/lineage-23.2-20260915-UNOFFICIAL-sunfish.zip`
**abu / torukk / Downloads `boot-sunfish-resukisu*.img`:** reference only (SusFS / module ideas).
**Never** treat Downloads abu boots as “ours”.
---
## Our boot images (host + laptop)
| Role | File | Where |
|------|------|--------|
| **Penúltimo (known-good ReSukiSU, no SusFS)** | `boot-resukisu-our-20260915T183729Z.img` | Host `/srv/rom/artifacts/007-kernelsu/` (+ symlink `boot-resukisu-our-latest.img`). Copy to laptop before flash if missing locally. |
| **Último (ReSukiSU + SusFS API bridge)** | `boot-resukisu-susfs-20260916T004038Z.img` | Host + laptop `.local-artifacts/007-kernelsu/boot-resukisu-susfs-latest.img` — sha256 `455a77a6…` |
Earlier same-day builds: `…T172342Z` / `…T172400Z` (superseded by `T183729Z`).
---
## What happened today (facts)
### Kernel / SusFS (host)
- Integrated **ReSukiSU** (not KSU-Next) into our tree; fixed shallow-clone bogus `KSU_VERSION=30701`.
- Built SusFS after bridging non-GKI 4.14 SusFS (`INODE_STATE_*`, etc.) with ReSukiSU GKI-style ioctl `CMD_*` / wrappers — compile+link OK (~18m).
- Pin notes: `.local-artifacts/007-kernelsu/susfs-pin/` (host tree under `kernel/google/msm-4.14`).
- **Process note:** SusFS build ran before US2 hide verification (constitution prefers profiles first). Treat SusFS boot as **trial**, not claimed.
### LAB flash chaos (evening)
- Torukk **22.2 MicroG** was on device temporarily (test/capture); **not** our product path.
- Attempted restore to **our 23.2 bacon** + SusFS boot.
- `fastboot -w` erased userdata **without** formatting (`File system type raw not supported`) → “Can't load Android system / data may be corrupt”.
- Wrong boot paths (`~/Downloads/boot-sunfish-resukisu.img`) = **abu/torukk**, not ours — caused confusion.
- Sideload of our 23 zip from old recovery: signature / **downgrade** prompts (recovery compares timestamps/patch, not “22&lt;23” marketing). Accept install-anyway / downgrade when intentional.
- End of session: operator reported **sideload of our 23 finished and device booted**. Confirm with:
`adb shell getprop ro.lineage.version` → expect `23.2-20260915-…`.
- Next flash after that sideload: **our penúltimo** boot (`boot-resukisu-our-20260915T183729Z`), then later trial SusFS if needed.
### What is *not* done
- SusFS boot **not** proven stable on our 23.2 (earlier bootloop / corrupt-data episode with SusFS + messy slots).
- US2 hide verification, modules (TEESimulator-RS / Zygisk / PIF), Island banks — still open.
- microG **Location UI** gap on our 23.2 (see urgent next).
---
## Urgent next (new chat)
1. **Confirm LAB identity:** `ro.lineage.version` = our 23.2 bacon; slot; serial `11231JEC207477`.
2. **Flash our penúltimo boot** (if not already):
`fastboot flash boot …/boot-resukisu-our-20260915T183729Z.img` then reboot.
3. **URGENT — Location options (`005`/`004` follow-up):**
On `23.2-20260915` + microG 0.3.16, operator saw **no usable microG Location UI** (no remember-from-GPS / request-from-online / hotspot-style controls vs richer UI on community 22.2).
Re-check on restored stack: screenshots + `dumpsys`; decide if packaging/overlay/settings gap.
Docs: `docs/004-lab-selfcheck.md` § “LAB observation (follow-up — 2026-09-16)”.
4. Reinstall manager (matching `KSU_VERSION`) + modules; resume `007` US2 → SusFS trial → attestation / Yuh+Swissquote.
5. Do **not** re-introduce abu ROM/kernels into the flash path; keep abu trees as SusFS/reference only.
---
## Spec / doc touchpoints updated this handoff
- `docs/007-kernelsu-runbook.md` — our boots, SusFS trial status, abu archived as reference
- `specs/007-kernelsu-sunfish/tasks.md` — T018 build done; T019 pending proof
- `specs/README.md` — `007` active
- `docs/004-lab-selfcheck.md` — Location marked urgent for return to our stack
@@ -0,0 +1,228 @@
sunfish-a16-rom — Spec Kit beginner guide + pre-plan briefing for ChatGPT
Generated: 2026-08-09
Project path: /home/aventura/Workspaces/sunfish-a16-rom
Constitution: 0.1.5
Active feature (.specify/feature.json): specs/001-development-environment
================================================================================
1) WHAT HAPPENED WHEN WE "CREATED" THE PROJECT
================================================================================
In this project we did NOT use the SSYSTEMS command /speckit.project-create.
We used upstream Spec Kit:
specify init --here --integration cursor-agent --script sh
That command:
- created .specify/ (scripts, templates, workflows, memory/constitution template)
- installed Cursor skills under .cursor/skills/speckit-*
- those skills are what appear in Cursor as /speckit-plan, /speckit-tasks, etc.
So yes: initializing a Spec Kit project is what "brings" the other /speckit-*
commands into Cursor for THAT workspace.
Important:
- Open the sunfish-a16-rom folder in Cursor (not buildout-saar) so those
skills are available.
- /speckit-taskstoissues is GitHub-only — do NOT use it (we use Gitea later).
- We already wrote constitution + specs/001-003 manually. We do NOT need to
re-run /speckit-specify to create 001.
================================================================================
2) TYPICAL UPSTREAM ORDER (generic Spec Kit)
================================================================================
For a brand-new feature from scratch, the usual order is:
1. /speckit-constitution → project principles (once / rare)
2. /speckit-specify → create specs/NNN-name/spec.md from a prompt
3. /speckit-clarify → optional: ask structured questions
4. /speckit-plan → create plan.md (+ design artifacts)
5. /speckit-tasks → create tasks.md checklist
6. /speckit-checklist → optional quality checklist
7. /speckit-analyze → optional consistency report
8. /speckit-implement → execute tasks
9. /speckit-converge → later: find remaining work
You do NOT need every optional step every time.
================================================================================
3) ORDER FOR THIS PROJECT (sunfish-a16-rom) — RIGHT NOW
================================================================================
Already done:
[x] specify init (skills installed)
[x] custom constitution written (do NOT overwrite with generic /speckit-constitution
unless you intentionally re-seed — our constitution is already project-specific)
[x] specs 001, 002, 003 written
[x] feature.json → 001-development-environment
[x] design frozen at constitution 0.1.5
Recommended next commands (only for feature 001):
A. (optional) /speckit-clarify
Use if you want the agent to ask remaining ambiguities about 001 only.
Many ambiguities were already closed in chat — can skip.
B. /speckit-plan
Generates specs/001-development-environment/plan.md from the existing
spec.md + constitution. NO VM required.
C. /speckit-tasks
Generates tasks.md from the plan. NO VM required.
D. (optional) /speckit-analyze or /speckit-checklist
E. /speckit-implement
THIS is when rom-dev01 should exist (SSH working). Implementation of 001
assumes the VM is already provisioned outside this project.
Then later, switch feature and repeat plan→tasks→implement for:
002 → 003 → …
How to switch feature later (when 001 is done):
- edit .specify/feature.json:
{ "feature_directory": "specs/002-axp-reference-capture" }
- or use /speckit-specify only when creating a NEW feature number (004+)
- do NOT re-specify 001/002/003
================================================================================
4) WHAT EACH COMMAND DOES (short)
================================================================================
/speckit-constitution Seed/update constitution from template — SKIP for now
(ours is already custom 0.1.5)
/speckit-specify Create a NEW feature spec from a prompt
(we already have 001–003)
/speckit-clarify Ask clarifying questions before plan
/speckit-plan Write implementation plan (plan.md)
/speckit-tasks Break plan into tasks.md
/speckit-checklist Generate validation checklists
/speckit-analyze Cross-check spec/plan/tasks consistency
/speckit-implement Execute tasks against the real system/repo
/speckit-converge Find leftover work after partial implementation
/speckit-taskstoissues DO NOT USE (GitHub-specific)
================================================================================
5) PRE-PLAN BRIEFING FOR CHATGPT — FEATURE 001
================================================================================
Please review this briefing and return:
- anything missing/dangerous in the 001 plan approach
- suggested plan.md section outline
- P1 vs P2 sequencing recommendations
- explicit "do not do" list for Cursor when running /speckit-plan and later
/speckit-implement
--- PROJECT FACTS ---
Name: sunfish-a16-rom
Goal: LineageOS 23.2 / Android 16 custom ROM for Pixel 4a (sunfish), aiming at
AXP.OS Pro functionality with deliberate substitutions (Magisk→KernelSU,
backup→NeoBackup external, etc.)
Meta repo (this Spec Kit tree): eventually Gitea sunfish-a16-rom
Companion repos later: sunfish-rom-manifest, sunfish-rom-build, sunfish-rom-patches
Laptop role: Cursor Remote SSH, adb/fastboot, rsync, thin client
Build host: rom-dev01 (Ubuntu 24.04) — created OUTSIDE this project via
SSYSTEMS Salt/PVE by the human. Spec Kit must NOT create the VM.
001 starts ONLY when rom-dev01 already exists with:
- Ubuntu 24.04
- public IPv4, SSH works
- saltenv=base already applied by SSYSTEMS
Sizing to plan around (do not assume more):
6 vCPU / 52 GB RAM / 2 TB disk / 32 GB swap
→ conservative build parallelism later (003), not in 001 yet
001 IN SCOPE (ROM host environment):
- android-build user/workspace
- Android/Lineage build packages, git, git-lfs, repo, ccache
- /srv/rom layout (sources, artifacts, logs, tooling, reference)
- private Gitea (bind/tunnel; not world-exposed)
- four repos: sunfish-a16-rom, sunfish-rom-manifest, sunfish-rom-build,
sunfish-rom-patches
- off-VM backup/mirror of those Git repos (laptop/Borg/etc.)
- Ansible + Semaphore (P2; smoke playbook OK first)
- Spec Kit meta repo available on host
- Cursor Remote SSH + rsync artifacts workflow docs
- guest isolation posture docs/config (public NIC only; UIs not world-open)
[human already accepted keeping this in 001]
001 OUT OF SCOPE:
- Proxmox / pm-test01 / choosing public IP / company Salt VM creation
- Lineage sync/build (003)
- AXP capture / NeoBackup phone backup (002)
- microG/Phonesky/KernelSU/hardening
P1 vs P2 in 001:
P1: SSH + android-build + toolchain + /srv/rom + docs for thin-client workflow
(enough to unlock 003 later)
P2: Gitea + four repos + off-VM mirror + Ansible/Semaphore + isolation polish
Priority for ChatGPT review of the future plan.md:
- Must not invent PVE/qm/Salt SSYSTEMS steps
- Must not require Ansible on the laptop
- Must not put .repo/out/ccache on the laptop
- Must keep Gitea/Semaphore off public Internet by default
- Must include off-VM git backup
- Must use account android-build (not hard-code laptop username)
- First LOS build remains manual later (003); 001 only prepares the host
Authoritative files for Cursor when planning:
- .specify/memory/constitution.md (0.1.5)
- specs/001-development-environment/spec.md
- AGENTS.md
- docs/axp-pro-parity.md and docs/hardening-backlog.md are NON-NORMATIVE
Suggested plan.md outline for /speckit-plan to produce:
1. Summary / goals
2. Assumptions (VM pre-exists)
3. Non-goals
4. Host layout (/srv/rom, android-build home)
5. Package/toolchain set (LOS build deps)
6. ccache defaults for 6 vCPU / 52 GB
7. Gitea install + access model (SSH tunnel)
8. Initial empty repos + push meta Spec Kit tree
9. Off-VM backup/mirror procedure
10. Ansible + Semaphore minimal bootstrap
11. Isolation/firewall/DNS guest posture (docs + config)
12. Laptop workflow (Remote SSH, rsync, adb/fastboot)
13. Acceptance mapping to FR/SC in spec.md
14. Handoff criteria to start 002/003
Please critique this outline and list concrete plan bullets Cursor should include
or avoid.
================================================================================
6) AFTER CHATGPT ANSWERS
================================================================================
Human brings ChatGPT feedback into Cursor, then runs:
/speckit-plan
(optionally paste ChatGPT constraints as $ARGUMENTS)
Then:
/speckit-tasks
Implement only when rom-dev01 SSH works.
================================================================================
END
================================================================================
+31
View File
@@ -0,0 +1,31 @@
# sunfish-a16-rom — laptop OpenSSH (independent of SSYSTEMS Salt/GitLab)
# Included from: ~/.ssh/config.d/a15-rom-dev01
#
# Web UIs (no tunnel): https://a16.ssystems.de/ and /semaphore/
# HTTP Basic: /srv/rom/secrets/web-htpasswd.txt on host
# Default: root (ssh a16 / ssh a16.ssystems.de)
Host a16 a16.ssystems.de a16-root rom-dev01-root a15-root
HostName 49.13.104.83
User root
Port 37037
IdentityFile ~/.ssh/id_ed25519_rom_dev01
IdentitiesOnly yes
ForwardAgent no
# Build user when needed: ssh a16-build
Host a16-build rom-dev01 a15
HostName 49.13.104.83
User android-build
Port 37037
IdentityFile ~/.ssh/id_ed25519_rom_dev01
IdentitiesOnly yes
ForwardAgent no
Host a15.ssystems.de aventura.serv.ssystems.de
HostName 49.13.104.83
User root
Port 37037
IdentityFile ~/.ssh/id_ed25519_rom_dev01
IdentitiesOnly yes
ForwardAgent no
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Check a Linux module's versioned imports against its target Module.symvers.
Requires kmod's modprobe. A pass checks symbol CRCs only, not complete ABI,
vermagic, CFI, signatures, firmware or hardware behaviour.
"""
import argparse
from pathlib import Path
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("symvers", type=Path)
parser.add_argument("modules", nargs="+", type=Path)
args = parser.parse_args()
exports = {}
for line in args.symvers.read_text().splitlines():
fields = line.split()
if len(fields) >= 2:
exports[fields[1]] = int(fields[0], 16)
if not exports:
parser.error("Module.symvers contains no exports")
failed = False
for module in args.modules:
result = subprocess.run(
["modprobe", "--show-modversions", str(module.resolve())],
text=True, capture_output=True,
)
if result.returncode or not result.stdout.strip():
print(f"UNVERIFIED {module}: {result.stderr.strip() or 'no versioned imports'}")
failed = True
continue
mismatch = 0
imports = result.stdout.splitlines()
for line in imports:
crc, symbol = line.split()
expected = exports.get(symbol)
if expected != int(crc, 16):
mismatch += 1
target = "missing export" if expected is None else f"0x{expected:08x}"
print(f" {symbol}: module={crc}, kernel={target}")
failed |= bool(mismatch)
print(f"{'FAIL' if mismatch else 'PASS'} {module}: "
f"{mismatch}/{len(imports)} incompatible versioned imports")
return int(failed)
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,9 @@
# Contracts: N/A for 001
Feature `001-development-environment` is host/Git/service bootstrap.
It does **not** expose a project-owned HTTP API, CLI contract for external
consumers, or IPC schema that needs `contracts/` files.
Service access is operational (SSH tunnel to Gitea/Semaphore UIs) and is
documented in `plan.md` / `quickstart.md` instead.
@@ -0,0 +1,87 @@
# Data model (operational): 001-development-environment
This is **not** an application domain model. It records host/Git/service
entities for planning clarity. No ORM, no API schemas.
## Entities
### Host (`rom-dev01`)
| Field | Notes |
|-------|-------|
| OS | Ubuntu 24.04 LTS (precondition) |
| Capacity | ~6 vCPU / 52 GB / 2 TB / 32 GB swap |
| Network | Public IPv4 NIC; no required internal NIC |
| Management | External Salt `saltenv=base` + project config |
### Account `android-build`
| Field | Notes |
|-------|-------|
| Type | Unprivileged login user |
| Owns | `/srv/rom/{src,references,tooling,ccache,artifacts,logs}` (as documented) |
| Must not | Need root to compile; own Gitea/Semaphore system dirs |
### Meta working tree
| Path | Purpose |
|------|---------|
| `/home/android-build/sunfish-a16-rom` | Spec Kit + specs + docs (Cursor Remote SSH) |
### Path root `/srv/rom`
| Child | Purpose | Populated in 001? |
|-------|---------|-------------------|
| `src/` | Future Android trees | Empty (no sync) |
| `references/` | Future AXP/reference | Empty |
| `tooling/` | Project tooling helpers (not Spec Kit checkout) | Partial OK |
| `ccache/` | CCACHE_DIR | Yes (dir + env) |
| `artifacts/` | Laptop handoff | Dummy file for smoke |
| `logs/` | Bootstrap/preflight logs | Yes |
| `secrets/` | Restricted placeholder | Empty; no keygen |
### GiteaInstance
| Field | Notes |
|-------|-------|
| Bind | Loopback / non-public |
| Repos | `sunfish-a16-rom`, `sunfish-rom-manifest`, `sunfish-rom-build`, `sunfish-rom-patches` |
| Canonical | Yes after first successful push of meta repo |
### GitMirror (off-VM)
| Field | Notes |
|-------|-------|
| Location | Laptop bare mirrors (initial) |
| Direction | Laptop pulls (`clone --mirror` / `remote update`); no VM→laptop SSH |
| Scope | All four project repos |
| Success | At least one verified mirror cycle |
### AnsibleController / SemaphoreInstance
| Field | Notes |
|-------|-------|
| Host | `rom-dev01` |
| First job | Environment smoke playbook |
| Exposure | Same private access model as Gitea |
## Relationships
```text
AdminSSH ──manages──► Host packages/systemd/firewall
android-build ──writes──► /srv/rom (build-related trees)
GiteaInstance ──hosts──► four Repositories
Laptop ──mirrors──► Repositories (bare)
SemaphoreInstance ──runs──► Ansible smoke playbook on Host
Laptop Cursor ──RemoteSSH──► Host workspace
Laptop ──rsync──► /srv/rom/artifacts (pull)
```
## State transitions (meta Git)
1. `laptop-staging-only`
2. `copied-to-host` (pre-Gitea)
3. `gitea-canonical` (push complete)
4. `mirrored-off-vm` (SC-007)
Do not destroy state (1) until (3) and (4) succeed.
+476
View File
@@ -0,0 +1,476 @@
# Implementation Plan: Development environment on rom-dev01
**Branch**: `001-development-environment` | **Date**: 2026-08-09 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-development-environment/spec.md`
**Mode**: Documentation/design only until `rom-dev01` exists with working SSH.
No implementation, host mutation, or Android sync/build in this planning phase.
## Summary
Feature `001` prepares an already-existing Ubuntu 24.04 host (`rom-dev01`) as the
ROM development environment: unprivileged `android-build` account, `/srv/rom`
layout, Lineage/AOSP host toolchain + ccache, Spec Kit meta-repo bootstrap,
private Gitea (four repos) with off-VM Git history mirror, Ansible + Semaphore
smoke orchestration, thin-client laptop workflow, and guest isolation posture.
It does **not** create the Proxmox VM, sync LineageOS, build Android, or touch
the Pixel. Those belong to human Salt/PVE prep (`rom-dev01` prerequisite) and
features `002`/`003`+.
**Unlocks**: after P1, host is usable for work; after P2, Gitea is canonical and
orchestration smoke works → ready for `002` then `003`.
## Technical Context
**Language/Version**: Bash / Ansible YAML; Ubuntu Server 24.04 LTS on host;
Spec Kit docs in Markdown (English).
**Primary Dependencies**: apt packages per current LineageOS/AOSP host build
docs at implementation time; `repo`; `git` + `git-lfs`; `ccache`; Gitea;
Ansible; Semaphore UI; OpenSSH.
**Storage**: Host filesystem paths under `/srv/rom/` (no project-owned LVM/fstab
changes). Secrets directory restricted/empty in `001`.
**Testing**: Observable shell/service checks (clean-login tool smoke, systemd
active, local curl, rsync SHA256, restart survival). No Android instrumentation.
**Target Platform**: Single VM `rom-dev01` (~6 vCPU / 52 GB RAM / 2 TB /
32 GB swap) + thin laptop (Cursor Remote SSH, adb/fastboot, rsync).
**Project Type**: Infrastructure / host bootstrap for Android ROM development.
**Performance Goals**: Toolchain available in a clean `android-build` login;
services survive restart; no public exposure of Gitea/Semaphore. Build `-j`
tuning is **out of scope** (owned by `003`).
**Constraints**:
- VM/PVE/IP/Salt `saltenv=base` provisioning is external; do not fight managed
config silently.
- Never build Android as root; never `chmod 777`.
- Never flush firewall / risk active SSH path.
- Do not bind Gitea/Semaphore to `0.0.0.0` without an explicit protected design
(default: loopback / non-public + SSH tunnel).
- No secrets in Git or build logs.
- Do not delete laptop staging tree before Gitea + mirror verified.
**Scale/Scope**: One developer; four Gitea repos; one smoke Ansible playbook;
no distributed CI / registry / Kubernetes.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Gate | Status | Notes |
|------|--------|-------|
| Independence from SSYSTEMS governance | PASS | No Redmine/GitLab/company MR workflow; no Salt VM creation from this repo |
| Spec-as-source | PASS | Plan derived from `001/spec.md` + constitution `0.1.5` |
| Parity/hardening docs non-normative | PASS | Not used as requirements for `001` |
| Host boundary: VM is prerequisite | PASS | Preconditions assume host exists; non-goals forbid PVE/qm/Salt states |
| Isolation posture (guest) | PASS | Public-NIC workflow; UIs not world-exposed; SSH preserved |
| No Android trees on laptop | PASS | Thin-client + `/srv/rom` on host |
| Secrets not in Git | PASS | Explicit secrets handling |
| Magisk/KSU/microG/signing out of `001` | PASS | Listed in DO NOT |
| Capacity honesty | PASS | Plan around 6/52/2T/32; parallelism deferred to `003` |
**Post-design re-check**: PASS — artifacts (`research.md`, `quickstart.md`,
operational `data-model.md`) stay inside `001` scope; no invented API contracts.
## Project Structure
### Documentation (this feature)
```text
specs/001-development-environment/
├── plan.md # This file
├── research.md # Phase 0
├── data-model.md # Operational entities only (no app domain model)
├── quickstart.md # Validation / cold-start guide
├── contracts/ # N/A for 001 (see contracts/README.md)
└── tasks.md # Created later by /speckit-tasks — NOT this command
```
### Host layout (target on rom-dev01)
```text
/srv/rom/
├── src/ # future Android trees (empty in 001; no repo sync)
├── references/ # future AXP/reference captures (empty OK)
├── tooling/ # project scripts / ansible / helper tooling
├── ccache/ # CCACHE_DIR for android-build
├── artifacts/ # handoff to laptop (dummy file for smoke)
├── logs/ # install/bootstrap logs
└── secrets/ # restricted; empty; no key generation in 001
# service data (exact paths chosen at implement time; document them)
# e.g. /var/lib/gitea, /etc/gitea, semaphore data dir
# Canonical meta Spec Kit working tree (Cursor Remote SSH edits here):
/home/android-build/sunfish-a16-rom
# /srv/rom/tooling/ = project tooling only (scripts helpers, etc.)
# — NOT the Spec Kit meta checkout
```
### Repository roles (Gitea)
| Repo | Contents |
|------|----------|
| `sunfish-a16-rom` | Spec Kit + specs + docs (meta) |
| `sunfish-rom-build` | Scripts + Ansible + Semaphore/build automation |
| `sunfish-rom-manifest` | Repo manifests (empty at first) |
| `sunfish-rom-patches` | Patchsets / tracking (empty at first) |
Do **not** mix executable automation into the Spec Kit meta repo.
### Meta repository content (laptop staging → Gitea)
```text
/home/android-build/sunfish-a16-rom/ # Spec Kit + docs only
├── .specify/
├── specs/
├── docs/
├── AGENTS.md
└── …
```
**Structure Decision**: Infrastructure feature — host paths + Git/service
bootstrap. No application `src/` tree inside Spec Kit for `001`.
## Complexity Tracking
> No constitution violations requiring justification.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|--------------------------------------|
| — | — | — |
---
## 1. Summary (expanded)
`001` makes `rom-dev01` a usable ROM workstation and private Git home without
performing ROM builds. Completion means: clean `android-build` toolchain smoke
passes; `/srv/rom` owned correctly; Spec Kit tree lives on host; Gitea holds
four repos and is canonical; laptop has an off-VM bare mirror; Ansible+Semaphore
run one smoke playbook; thin-client + isolation docs/checks pass; no PVE/Salt
fight, no Pixel/LOS work.
## 2. Constitution check / source of truth
Authoritative for this plan:
1. `.specify/memory/constitution.md` **v0.1.5**
2. `specs/001-development-environment/spec.md`
3. `AGENTS.md`
Non-normative (do not treat as `001` requirements):
`docs/axp-pro-parity.md`, `docs/hardening-backlog.md`.
## 3. Preconditions
Assume already true before `/speckit-implement`:
- `rom-dev01` exists (human + SSYSTEMS Salt/PVE)
- Ubuntu Server 24.04 LTS
- One reachable public IPv4 (public NIC only)
- SSH administration works
- `saltenv=base` already applied
- Capacity approximately: 6 vCPU / 52 GB RAM / 2 TB disk / 32 GB swap
Do **not** create, resize, or re-IP the VM from this project.
## 4. Non-goals / boundaries
**DO NOT** (plan / tasks / implement):
- create or configure the Proxmox VM; emit `qm`/PVE provisioning;
- choose or hard-code the public IPv4;
- write SSYSTEMS Salt states; modify `pm-test01`;
- assume company internal DNS/VPN/LDAP/GitLab/NFS/Zabbix is required;
- start LineageOS `repo sync` or any Android build;
- touch/backup/flash the Pixel (NeoBackup belongs to `002`);
- introduce microG, Phonesky, KernelSU, Magisk, SUSFS, or hardening;
- generate ROM/AVB/Phonesky signing keys;
- put `.repo`, `out/`, or ccache on the laptop;
- require Ansible on the laptop;
- expose Gitea or Semaphore to the public Internet;
- design GitLab/Jenkins/Kubernetes/registry/distributed runners;
- turn Semaphore into the Android build pipeline yet;
- invent an API/data model merely to satisfy a template;
- hard-code facts that must be measured on `rom-dev01`;
- duplicate requirements owned by `002`/`003`+.
**Implement-time DO NOT** (operational):
- run Android builds as root; `chmod 777` project trees;
- blindly dist-upgrade; replace apt sources without need;
- overwrite Salt/base-managed config silently;
- flush firewall/nftables; risk active SSH management path;
- bind Gitea/Semaphore to `0.0.0.0` without explicit protected design;
- commit passwords/tokens/private keys; plaintext secrets in Ansible in Git;
- delete laptop staging before Gitea + mirror verified;
- delete/overwrite working data only to fake idempotency;
- download full Lineage tree; compile Lineage; connect/flash Pixel;
- automate ROM builds through Semaphore yet.
**Conflict rule**: If base-managed configuration conflicts with project needs,
**stop and report** — do not silently replace or fight it; do not “fix”
unrelated infrastructure opportunistically.
## 5. Host preflight (before modifying the host)
First implement steps are **observational**. Capture and store under
`/srv/rom/logs/preflight/` (or create layout first then log):
- Ubuntu version / kernel
- CPU / RAM / swap
- filesystem free space (esp. for `/` and future `/srv/rom`)
- network interfaces / default route (confirm no required internal NIC)
- SSH reachability
- existing users / groups
- existing firewall (nft/ufw/iptables) summary — **do not flush**
- listening ports / services
- apt sources summary
- hints of Salt/base-managed files if obvious
Produce a short baseline note in the meta repo docs or `/srv/rom/logs` for later
diff. Especially important because `saltenv=base` already touched the machine.
## 6. Account and permission model
| Actor | Role |
|-------|------|
| Existing admin SSH user | packages, systemd, system config, firewall changes (carefully) |
| `android-build` | Android sources/builds (later), ccache, artifacts, build tooling; SSH pubkey login from laptop; Cursor Remote SSH user; **never** build as root; **no sudo required to compile** |
| `gitea` (service user) | Gitea data/config ownership |
| `semaphore` (service user/context) | Semaphore data/config ownership |
Rules:
- No `chmod -R 777 /srv/rom`
- Group memberships only as needed (e.g. `android-build` owns `/srv/rom` trees
it writes; services own their state dirs)
- Admin elevates only for system packaging/services
## 7. `/srv/rom` filesystem layout
Create with explicit owners/modes (exact modes chosen at implement; document):
```text
/srv/rom/
├── src/
├── references/
├── tooling/
├── ccache/
├── artifacts/
├── logs/
└── secrets/ # empty, restricted; do not generate keys in 001
```
- Do **not** manage partitions/LVM/fstab from this project.
- Do not populate `references/` (that is `002`) or generate secrets.
- `artifacts/` must be writable by `android-build` for dummy + later builds.
## 8. Android host toolchain (no sync/build)
- Install host packages using **current primary LineageOS/AOSP build
documentation at implementation time** (do not freeze a stale 50-package list
in this plan).
- Record what was installed (log + short note in meta repo / `/srv/rom/logs`).
- Ensure `git`, `git-lfs`, `repo`, `python3`, and Java/toolchain prerequisites
required by that documentation.
- Configure `ccache`: location (`/srv/rom/ccache`), ownership (`android-build`),
size cap, environment (`CCACHE_DIR`, etc.). **Not** tied to inventing `-j`
from vCPU/RAM here.
- FR-007: document capacity (6/52/32) and that **conservative `-j` / OOM tuning
belongs to `003`**, with a one-line recommended starting point only if needed
for docs — no build execution in `001`.
- Smoke from a **clean login**: primary check is **direct SSH from the laptop as
`android-build`** (after pubkey setup) → verify tools. Optional local admin
check: `sudo -iu android-build` on the host if needed. **No `repo sync`**.
## 9. Spec Kit staging bootstrap (before Gitea)
Egg/chicken sequence (mandatory):
```text
laptop staging:
/home/aventura/Workspaces/sunfish-a16-rom
↓ rsync/scp/git bundle (preserve .git)
rom-dev01 working copy
↓
install Gitea
↓
create empty Gitea repo sunfish-a16-rom
↓
push meta repo
↓
create sunfish-rom-manifest, sunfish-rom-build, sunfish-rom-patches
↓
Gitea becomes canonical remote
↓
host working tree at /home/android-build/sunfish-a16-rom
uses Gitea as origin (after push)
```
Do **not** assume `git clone …gitea…` before Gitea exists.
Do **not** delete laptop staging until Gitea push + off-VM mirror verified.
## 10. Gitea
- Install on `rom-dev01` with persistent data dirs and systemd unit.
- Default **UI** access: **loopback / non-public bind + SSH tunnel** from laptop
(decision fixed for `001` unless implement discovers a hard conflict — then
stop/report).
- **Git transport**: document the concrete mechanism at implement time after
inspecting the host (HTTP via the same SSH tunnel and/or Git-over-SSH also
tunnelled are valid). Do **not** blindly modify OpenSSH managed by
`saltenv=base`, and do **not** assume or open a second public SSH port
(e.g. `:2222`) for Gitea.
- Create admin credentials with restricted local storage; never commit secrets.
- Verify: service active; local curl succeeds; direct public Internet access to
UI **not** required / not open.
## 11. Canonical Git transition
Repos:
1. `sunfish-a16-rom` — Spec Kit + specs + docs; push existing history
2. `sunfish-rom-manifest` — empty
3. `sunfish-rom-build` — scripts + Ansible + Semaphore/build automation (seed
minimal structure when ready)
4. `sunfish-rom-patches` — empty
Document remotes for host + laptop clones. Company GitLab must not be used.
## 12. Off-VM Git mirror (P2)
First implementation (sufficient for `001` SC-007): **laptop-initiated pull**
of bare mirrors. The laptop opens the connection (via SSH tunnel / documented
Git URL); `rom-dev01` must **not** need inbound SSH to the laptop.
```text
# On laptop (conceptual):
git clone --mirror <gitea-repo-url-via-tunnel>
cd <repo>.git && git remote update
```
Borg / full Gitea DR is **later**. Requirement is Git history redundancy.
## 13. Ansible bootstrap (P2)
- Install Ansible on `rom-dev01` for **project environment** automation.
- Keep playbooks and automation in **`sunfish-rom-build`** (not in the Spec Kit
meta repo `sunfish-a16-rom`).
- No plaintext secrets in Git.
## 14. Semaphore bootstrap (P2)
- Install Semaphore as UI over Ansible.
- Same exposure model as Gitea (loopback/tunnel).
- First integration: **unprivileged** environment smoke playbook only — e.g.
gather facts + assert `/srv/rom`, `android-build`, and toolchain checks.
**No sudo password / privileged secrets in Semaphore for this smoke.**
Privileged automation is a later explicit decision.
- **Not** an Android build pipeline.
## 15. Isolation / firewall / service exposure
Keep guest isolation posture from constitution/`001` spec:
- Public NIC only for project workflow
- Gitea/Semaphore not world-exposed
- Document firewall/DNS expectations
Implementation safety:
- never flush/replace existing firewall blindly;
- never close current SSH path before a tested replacement;
- keep a rollback path;
- prefer additive allow rules.
## 16. Laptop thin-client workflow
Document and smoke:
1. Direct SSH from laptop as `android-build` (pubkey); Cursor Remote SSH →
`/home/android-build/sunfish-a16-rom` with `User android-build`
2. Dummy artifact in `/srv/rom/artifacts` → `rsync` to laptop → SHA256 match
3. adb/fastboot remain on laptop; Pixel USB on laptop; no USB passthrough design
## 17. Verification matrix (FR/SC → evidence)
| ID | Implementation focus | Observable verification |
|----|----------------------|-------------------------|
| FR-001 | Create `android-build` + home/workspace docs + SSH pubkey | `id android-build`; `ssh android-build@rom-dev01` from laptop |
| FR-002 | LOS/AOSP host deps + ccache | clean SSH login as `android-build`: `git`, `repo`, `ccache -s`, etc. |
| FR-003 | `/srv/rom` tree + ownership | `namei -l` / `stat` shows owners; writable artifacts |
| FR-004 | Gitea + four repos | local UI/API; four repos listed |
| FR-005 | Ansible + Semaphore smoke | Semaphore runs smoke playbook successfully |
| FR-006 | Thin-client docs | doc exists; Remote SSH + rsync smoke done |
| FR-007 | Capacity / defer `-j` to `003` | short doc records 6/52/32 and defers build jobs |
| FR-008 | Spec Kit on host | tree present; after P2, cloned/pushed via Gitea |
| FR-009 | Isolation posture | checklist: NIC, binds, firewall notes; SSH still works |
| FR-010 | Off-VM Git mirror | laptop bare mirror of four repos once |
| SC-001 | Toolchain smoke | clean `android-build` session has tools for later `003` |
| SC-002 | Artifacts handoff | dummy file rsync SHA256 match |
| SC-003 | Four Gitea repos | listed on Gitea; not company GitLab |
| SC-004 | Cold-start workflow doc | quickstart path SSH→workspace→(later build)→rsync→flash |
| SC-005 | No PVE management | no qm/PVE playbooks in repo/tasks evidence |
| SC-006 | Isolation checklist | passes without requiring VPN/internal DNS |
| SC-007 | Mirror demonstrated | off-VM mirror exists and was verified once |
## 18. Handoff
**`001` complete** means environment ready for `002` (phone capture) and later
`003` (LOS vanilla build) — **not** “start building Lineage now”.
Do not begin `/speckit-plan` for `002`/`003` until `001` doc cycle + implement
are done (avoid document saturation).
## P1 / P2 sequencing
### P1 — host usable without Gitea
1. Host preflight baseline
2. Create `android-build`
3. Configure developer SSH public-key access for `android-build`
4. Verify direct SSH login from the laptop as `android-build`
5. Create `/srv/rom` + permissions
6. Install build prerequisites (current LOS/AOSP docs)
7. `repo` / `git` / `git-lfs` / ccache config
8. Copy laptop staging meta repo to host path `/home/android-build/sunfish-a16-rom` (preserve `.git`)
9. Clean-shell toolchain smoke via SSH as `android-build`
10. Cursor Remote SSH smoke (`User android-build`)
11. Dummy artifact → rsync → laptop (SHA256)
### P2 — reproducible / comfortable
1. Install Gitea (loopback/tunnel)
2. Create four repos
3. Push meta repo; set Gitea canonical
4. Off-VM bare mirrors on laptop
5. Install Ansible
6. First smoke playbook
7. Install Semaphore
8. Run smoke playbook from Semaphore
9. Isolation / access / firewall validation (SSH-safe)
10. Service restart (+ reboot survival where practical)
Finish P2 before formally starting `002`.
## Phase outputs
| Artifact | Role |
|----------|------|
| [research.md](./research.md) | Decisions: Gitea/Semaphore access, mirror, deps source, ccache |
| [data-model.md](./data-model.md) | Operational entities only |
| [quickstart.md](./quickstart.md) | Cold-start validation |
| [contracts/](./contracts/) | N/A |
Next command after human review of these files: `/speckit-tasks` (still no VM).
Then `/speckit-analyze`. Stop before `/speckit-implement` until SSH to
`rom-dev01` works.
@@ -0,0 +1,132 @@
# Quickstart / validation: 001-development-environment
Use after `/speckit-implement` on a real `rom-dev01`. Until the VM exists, treat
this as the acceptance script shape.
## Prerequisites
- Host: `a15.ssystems.de` (`49.13.104.83`), SSH port **37037**, guest name `rom-dev01`
- Laptop key: `~/.ssh/id_ed25519_rom_dev01` + Host aliases in
`misc/config.d/a15-rom-dev01` (see `docs/001-host-runbook.md`)
- Laptop has `rsync`, `ssh`, Cursor Remote SSH, `adb`/`fastboot`
## P1 validation path
### 1. Preflight captured
```bash
ssh rom-dev01-root 'less /srv/rom/logs/preflight/preflight-latest.txt'
```
### 2. Build user workspace
```bash
ssh rom-dev01 'whoami; hostname; test -f ~/sunfish-a16-rom/specs/001-development-environment/spec.md && echo SPEC_OK'
```
### 3. Toolchain smoke (clean shell)
```bash
ssh rom-dev01 'bash -l -c "git --version; git lfs version; repo --version; python3 --version; ccache -s"'
```
Expected: all commands succeed without sudo. See `/srv/rom/logs/toolchain-smoke.txt`.
### 4. Layout
```bash
namei -l /srv/rom /srv/rom/artifacts /srv/rom/ccache
touch /srv/rom/artifacts/.write_test && rm /srv/rom/artifacts/.write_test
```
### 5. Meta repo present on host (pre-Gitea OK)
```bash
test -d /home/android-build/sunfish-a16-rom/.git
test -f /home/android-build/sunfish-a16-rom/specs/001-development-environment/spec.md
```
### 6. Cursor Remote SSH
From laptop: open host workspace; edit a trivial file; confirm it is on the VM
disk (not only local).
### 7. Artifact rsync smoke
On host:
```bash
echo '001-smoke' > /srv/rom/artifacts/dummy-001.txt
sha256sum /srv/rom/artifacts/dummy-001.txt
```
On laptop:
```bash
rsync -avz USER@ROM_DEV01:/srv/rom/artifacts/dummy-001.txt ./
sha256sum ./dummy-001.txt # must match
```
## P2 validation path
### 8. Gitea private access
```bash
ssh -L 3000:127.0.0.1:3000 rom-dev01-root
# browser: http://127.0.0.1:3000/ (user sunfish-admin; password in host /srv/rom/secrets/)
```
Gitea binds **only** `127.0.0.1:3000`. Git over HTTP to that URL (no public `:2222`).
### 9. Four repos
In Gitea: `sunfish-a16-rom`, `sunfish-rom-manifest`, `sunfish-rom-build`,
`sunfish-rom-patches` (under `sunfish-admin/`).
### 10. Ansible CLI smoke
```bash
ssh rom-dev01 'cd ~/sunfish-rom-build && ansible-playbook -i ansible/inventory.ini ansible/smoke.yml'
```
### 11. Semaphore smoke (unprivileged)
```bash
ssh -L 3001:127.0.0.1:3001 rom-dev01-root
# http://127.0.0.1:3001/ project sunfish-rom / template env-smoke
```
Semaphore clones canonical `sunfish-rom-build` from Gitea and runs the smoke
playbook without sudo. Evidence: `/srv/rom/logs/semaphore-smoke.txt`.
### 12. Off-VM mirrors
Laptop path: `/home/aventura/Archives/sunfish-rom-mirrors/*.git` (rsync from
`/srv/rom/artifacts/gitea-mirrors/` on host).
```bash
# refresh mirrors (host builds mirrors; laptop pulls via rsync — no token on laptop)
rsync -a android-build@a15.ssystems.de:/srv/rom/artifacts/gitea-mirrors/ \
~/Archives/sunfish-rom-mirrors/
git -C ~/Archives/sunfish-rom-mirrors/sunfish-a16-rom.git fsck
```
### 13. Restart survival
```bash
ssh rom-dev01-root 'systemctl restart gitea semaphore; curl -fsS -o /dev/null http://127.0.0.1:3000/; curl -fsS -o /dev/null http://127.0.0.1:3001/'
```
Evidence: `/srv/rom/logs/restart-smoke.txt`.
### 14. Isolation checklist
- No required internal NIC for workflow
- Gitea/Semaphore binds only `127.0.0.1`
- SSH still works after service restarts
- Day-to-day docs do not require company VPN/internal DNS/LDAP/GitLab
## Done means
`001` complete → start **`002`** (AXP capture).
Do **not** start Lineage `repo sync` / build as part of `001` success.
@@ -0,0 +1,110 @@
# Research: 001-development-environment
Phase 0 decisions for planning. Implementation may refine versions using live
primary docs on `rom-dev01`.
## R1 — Gitea access model
**Decision**: Bind Gitea to loopback (or other non-public address) and access via
SSH local port forward from the laptop. Do not require public HTTPS exposure.
**Rationale**: Constitution/`001` require private project Git without unnecessary
Internet exposure; single developer; SSH already required for admin/Remote SSH.
**Alternatives considered**:
- Public HTTPS + firewall allowlist — rejected for `001` (more moving parts;
accidental exposure risk).
- VPN-only bind — rejected as a *required* path (may exist externally; project
must not depend on company VPN).
## R2 — Semaphore access model
**Decision**: Same as Gitea — loopback/non-public + SSH tunnel. First job is an
**unprivileged** localhost/environment smoke playbook only (facts + path/tool
asserts). No sudo credentials in Semaphore for `001` smoke.
**Rationale**: Semaphore is orchestration UI, not a public portal; Android build
pipeline is explicitly out of scope for `001`; avoid baking privileged secrets
into the first integration.
**Alternatives considered**: Public Semaphore — rejected. Full ROM pipeline in
Semaphore — deferred past `003` manual first build. Privileged playbooks —
later explicit decision.
## R3 — Off-VM Git history backup
**Decision**: Laptop-initiated **bare mirrors** (`git clone --mirror` /
`git remote update`) of the four Gitea repos for `001` (SC-007). Laptop pulls;
VM does not SSH into the laptop. Borg/full Gitea DR later.
**Rationale**: Spec requires off-VM copy of project Git history, not a complete
backup platform. Pull-from-laptop avoids opening inbound access on the laptop.
**Alternatives considered**: Borg-only — optional later. Push-from-VM to laptop
— rejected (would require laptop inbound SSH).
## R4 — Lineage/AOSP host dependencies
**Decision**: At implement time, follow **current primary LineageOS / AOSP host
build documentation** for Ubuntu; record installed package set in logs. Do not
freeze a long package list in the plan.
**Rationale**: Dependency lists drift; stale pins cause false confidence.
**Alternatives considered**: Copy a historical package list from blogs —
rejected.
## R5 — `repo` installation source
**Decision**: Install Google's `repo` launcher via the method recommended by
current Lineage/AOSP docs (typically curl to `~/bin/repo` or distro package if
still adequate); verify `repo --version` as `android-build`.
**Rationale**: Align with upstream bring-up docs used for `003`.
## R6 — ccache policy vs build parallelism
**Decision**: `001` configures `CCACHE_DIR=/srv/rom/ccache` (or equivalent),
ownership for `android-build`, and a disk size cap. Document host capacity
(6 vCPU / 52 GB / 32 GB swap). **Defer `-j` / OOM tuning to `003`.**
**Rationale**: ccache is storage/env; parallel link/compile jobs belong to the
first real build feature.
**Alternatives considered**: Encoding `-j$(nproc)` defaults in `001` — rejected
as premature and confusing with FR-007 (document capacity; don't run builds).
## R7 — Meta-repo bootstrap before Gitea
**Decision**: Copy laptop staging Git tree to host path
`/home/android-build/sunfish-a16-rom` first (rsync/scp/bundle preserving
`.git`), install Gitea, create repos, push, then mark Gitea canonical. Keep
laptop staging until mirror verified. Ansible/automation lives in
`sunfish-rom-build`, not in the meta Spec Kit tree.
**Rationale**: Avoid chicken/egg `git clone` against non-existent Gitea; keep
Cursor edit path single and clear.
## R7b — Gitea Git transport vs OpenSSH base
**Decision**: UI via loopback + SSH tunnel. Exact Git clone/push transport
chosen at implement after host inspection (tunneled HTTP and/or tunneled Git
SSH are valid). Do not assume a public secondary SSH port for Gitea; do not
blindly edit `saltenv=base`-managed sshd.
## R8 — Coexistence with `saltenv=base`
**Decision**: Preflight + stop/report on conflicts with base-managed config.
Project Ansible may manage **project** paths/services; must not silently rewrite
Salt-owned system policy.
**Rationale**: Host is dual-touched (SSYSTEMS base then project). Fighting Salt
causes flapping.
## R9 — data-model / contracts
**Decision**: No application domain model or HTTP API contracts for `001`.
Operational entities only in `data-model.md`. `contracts/` marked N/A.
**Rationale**: Infrastructure bootstrap feature; inventing APIs would be template
noise.
+230
View File
@@ -0,0 +1,230 @@
# Feature Specification: Development environment on rom-dev01
**Feature Branch**: `001-development-environment`
**Created**: 2026-08-09
**Status**: Draft
**Input**: Provision the ROM development environment on an already-existing
`rom-dev01` host and establish the laptop ↔ VM ↔ Pixel workflow defined by the
project constitution.
## Context / non-goals
**Assumed already true before this feature starts:**
- `rom-dev01` exists (Ubuntu Server 24.04 LTS);
- one reachable public IPv4 address (public NIC only);
- SSH administration works;
- SSYSTEMS base provisioning (`saltenv=base`) is complete.
**Explicitly out of scope:**
- Proxmox / `pm-test01` VM creation;
- public subnet/IP selection;
- company Salt states, Redmine, or SSYSTEMS governance;
- LineageOS source sync or first ROM build (see `003`);
- AXP capture (see `002`).
**Isolation posture (in scope for guest config/docs):**
- Keep the host usable without SSYSTEMS internal network services.
- Do not add an internal NIC from this feature.
- Bind Gitea/Semaphore so they need not be world-exposed.
- Document guest firewall / egress expectations (block or avoid unnecessary
traffic to SSYSTEMS internal ranges; SSH from laptop must work).
- Sharing the public VLAN with lab/gateway VMs is acceptable.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Reachable ROM build host workspace (Priority: P1)
As the developer, I can SSH from my laptop into `rom-dev01` and use a dedicated
`android-build` account with a documented home/workspace layout for Android
work.
**Why this priority**: Without a stable host account and layout, no later build
feature can proceed.
**Independent Test**: SSH as the project build user, create a file under the
documented workspace, confirm ownership and paths.
**Acceptance Scenarios**:
1. **Given** `rom-dev01` is up, **When** I connect via SSH from the laptop,
**Then** authentication succeeds without requiring this project to change
PVE/network provisioning.
2. **Given** SSH works, **When** the `android-build` account is provisioned,
**Then** it exists with a documented home and build-related directories.
3. **Given** the account exists, **When** I inspect `/srv/rom` (or the agreed
layout), **Then** directories for sources, artifacts, logs, and tooling are
present with clear ownership.
---
### User Story 2 - Android build toolchain prerequisites (Priority: P1)
As the developer, the host has the packages and tools needed to sync and build
LineageOS for sunfish (repo, git, git-lfs, JDK, build deps, ccache), sized for
6 vCPU / 52 GB RAM / 32 GB swap.
**Why this priority**: Vanilla LOS build (`003`) depends on this.
**Independent Test**: `repo`/`git`/`ccache` respond; documented package set is
installed; ccache directory exists and is writable by `android-build`.
**Acceptance Scenarios**:
1. **Given** `android-build` session, **When** I run `repo --version` and
`git --version`, **Then** both succeed.
2. **Given** build dependencies are installed, **When** I check ccache config
and cache dir, **Then** ccache is usable by `android-build`.
3. **Given** constrained host resources, **When** build parallelism defaults are
documented, **Then** they are conservative (not assuming 16 vCPU / 64 GB).
---
### User Story 3 - Project Git and orchestration services (Priority: P2)
As the developer, private Gitea and Semaphore (over Ansible) run on `rom-dev01`
for project repos and ROM workflow orchestration, without exposing those UIs
unnecessarily to the public Internet. Project Git history must not exist only
on the build VM.
**Why this priority**: Needed before multi-repo ROM automation; can follow basic
toolchain if sequenced. Losing `out/` is fine; losing specs/patches/history is
not.
**Independent Test**: Gitea serves locally/over SSH tunnel; initial repos
`sunfish-a16-rom`, `sunfish-rom-manifest`, `sunfish-rom-build`,
`sunfish-rom-patches` exist or are documented to create; Semaphore can run a
trivial Ansible playbook; off-VM backup/mirror procedure documented and proven
once.
**Acceptance Scenarios**:
1. **Given** Gitea is installed, **When** I access it via the approved path
(SSH tunnel or existing access policy), **Then** I can create/browse private
project repositories.
2. **Given** Ansible is installed, **When** Semaphore is configured, **Then** it
drives Ansible without hiding undocumented build steps.
3. **Given** public IP exists, **When** reviewing listening services, **Then**
Gitea/Semaphore are not required to be open to the whole Internet for normal
operation.
4. **Given** the four project repositories, **When** backup/mirror is configured,
**Then** an off-VM copy exists (laptop clone, Borg, or other agreed target)
so `rom-dev01` is not the only copy of project history.
---
### User Story 4 - Guest isolation from SSYSTEMS internals (Priority: P2)
As the developer, normal ROM work on `rom-dev01` does not require company
internal DNS, VPN, LDAP, GitLab, NFS, or Zabbix, and project UIs are not
exposed more than necessary.
**Why this priority**: Limits blast radius and keeps the personal project
logically separate even though the hypervisor is shared lab hardware.
**Independent Test**: From the VM, confirm no required route/NIC into internal
SSYSTEMS networks for the workflow; Gitea/Semaphore reachable via approved
non-public path; checklist documents firewall/DNS posture.
**Acceptance Scenarios**:
1. **Given** the provisioned guest, **When** I inspect network interfaces,
**Then** only the public NIC is required for this project's workflow.
2. **Given** Gitea and Semaphore are installed, **When** I review listen
addresses / access docs, **Then** they are not required to be open to the
whole Internet.
3. **Given** guest firewall/DNS docs, **When** I follow them, **Then** day-to-day
ROM work does not depend on SSYSTEMS internal resolvers or VPN.
---
### User Story 5 - Laptop remains a thin client (Priority: P2)
As the developer, Cursor Remote SSH uses `rom-dev01` as the heavy workspace;
the Pixel stays USB-attached to the laptop; artifacts are `rsync`'d for
flashing.
**Why this priority**: Enforces the disk/space strategy and flashing model.
**Independent Test**: Documented Remote SSH workflow; sample artifact path
`/srv/rom/artifacts` syncs to the laptop with `rsync`.
**Acceptance Scenarios**:
1. **Given** Cursor on the laptop, **When** I open the project via Remote SSH,
**Then** I edit/build on `rom-dev01` without keeping `.repo`/`out` on the
laptop.
2. **Given** a dummy artifact in `/srv/rom/artifacts`, **When** I `rsync` it to
the laptop, **Then** the file arrives for local `adb`/`fastboot` use.
3. **Given** the Pixel is attached to the laptop, **When** following the
workflow docs, **Then** no USB passthrough to the VM is required.
---
### Edge Cases
- Host reboots: services and mount/layout expectations are documented.
- Disk pressure on 2 TB: cleanup conventions for `out/` and old artifacts.
- SSH admin user ≠ `android-build`: project must not hard-depend on laptop
usernames (`aventura` / `ubuntu`).
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: Provide dedicated `android-build` account and documented workspace.
- **FR-002**: Install Android/LineageOS build prerequisites and ccache.
- **FR-003**: Create `/srv/rom` layout (sources, artifacts, logs, tooling) or
document an equivalent and use it consistently.
- **FR-004**: Install and configure private Gitea for project repositories,
including meta repo `sunfish-a16-rom` plus `sunfish-rom-manifest`,
`sunfish-rom-build`, and `sunfish-rom-patches`.
- **FR-005**: Install Ansible; configure Semaphore as UI over Ansible for ROM
workflow (initially may only prove a smoke playbook).
- **FR-006**: Document Cursor Remote SSH + `rsync` artifact + laptop
adb/fastboot workflow.
- **FR-007**: Document conservative build parallelism defaults for 6 vCPU /
52 GB / swap 32 GB.
- **FR-008**: Ensure Spec Kit project for `sunfish-a16-rom` is available on the
host (push/clone the meta repo from this staging tree or Gitea once published).
- **FR-009**: Document and apply guest isolation posture: public-NIC-only
workflow; no required dependency on SSYSTEMS internal DNS/VPN/LDAP/GitLab/NFS/
Zabbix; Gitea/Semaphore not world-exposed by default; guest firewall guidance
to avoid unnecessary traffic to internal SSYSTEMS ranges (without encoding
concrete company IPs as project secrets/config in Git unless necessary).
- **FR-010**: Provide an off-VM backup or mirror of project-owned Gitea
repositories so the build VM is not the only copy of project history
(mechanism may be laptop clones, Borg, or another agreed target).
### Key Entities
- **rom-dev01**: pre-existing Ubuntu 24.04 build host.
- **android-build**: project build account.
- **/srv/rom**: canonical on-host data layout.
- **Gitea / Semaphore / Ansible**: project-owned services on the VM.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: From a clean SSH session as `android-build`, toolchain commands
required for `003` are available without further ad-hoc package hunting.
- **SC-002**: `/srv/rom/artifacts` (or documented equivalent) is writable and
used as the artifact handoff point to the laptop.
- **SC-003**: Gitea hosts (or is ready to host) the **four** initial project
repos (`sunfish-a16-rom`, `sunfish-rom-manifest`, `sunfish-rom-build`,
`sunfish-rom-patches`) without using company GitLab.
- **SC-004**: Written workflow doc allows a cold start: SSH → workspace →
(later) build → rsync → flash on laptop-attached Pixel.
- **SC-005**: No playbook or doc in this feature creates or reconfigures the
Proxmox VM itself.
- **SC-006**: Isolation checklist passes: no internal NIC required; project UIs
accessible without public Internet exposure; workflow docs do not require
SSYSTEMS VPN/internal DNS for normal use.
- **SC-007**: Off-VM backup/mirror of the four Gitea project repos is documented
and demonstrated at least once.
+208
View File
@@ -0,0 +1,208 @@
# Tasks: Development environment on rom-dev01
**Input**: Design documents from `/specs/001-development-environment/`
**Prerequisites**: plan.md (required), spec.md, research.md, data-model.md,
quickstart.md, contracts/ (N/A)
**Tests**: Observational verify steps embedded in tasks (no separate TDD suite).
**Organization**: Setup → Foundational (preflight) → User stories by priority →
Polish. **Do not run these tasks until `rom-dev01` SSH works.** Generating this
file is documentation-only.
**Canonical paths**:
- Meta Spec Kit tree: `/home/android-build/sunfish-a16-rom`
- Data root: `/srv/rom/`
- Automation repo (after Gitea): `sunfish-rom-build` (not the meta repo)
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no incomplete dependencies)
- **[Story]**: US1…US5 from spec.md
- Every task includes a path and an observable verify hint
## Path Conventions
Host paths are absolute on `rom-dev01`. Docs/logs also land under
`/home/android-build/sunfish-a16-rom/docs/` or `/srv/rom/logs/` as noted.
---
## Phase 1: Setup (docs readiness — can be done on laptop before VM)
- [x] T001 Confirm active feature is `specs/001-development-environment` via `.specify/feature.json` and that constitution is `0.1.7` in `.specify/memory/constitution.md`
- [x] T002 [P] Create host-facing doc stub `docs/001-host-runbook.md` in meta repo (preflight results, paths, Gitea tunnel notes — fill during implement)
- [x] T003 [P] Create FR/SC evidence checklist stub `docs/001-verification-matrix.md` mapping each FR-*/SC-* from `specs/001-development-environment/spec.md` to blank evidence fields
---
## Phase 2: Foundational (blocking — host preflight before stories)
**Goal**: Observe `rom-dev01` without fighting `saltenv=base`; abort on conflict.
- [x] T004 On `rom-dev01` as admin, capture preflight baseline into `/srv/rom/logs/preflight/` (create `/srv/rom/logs` with safe perms if needed): Ubuntu/kernel, CPU/RAM/swap, disk free, interfaces/routes, users, firewall summary (**do not flush**), listening ports, apt sources → also summarize in `docs/001-host-runbook.md`
- [x] T005 Record coexistence rule in `docs/001-host-runbook.md`: if base-managed config conflicts with project needs, **stop and report**; do not silently replace Salt-managed settings
- [x] T006 Verify SSH admin path still works after any read-only inspection; note rollback contacts/commands in `docs/001-host-runbook.md`
---
## Phase 3: User Story 1 — Reachable build workspace (P1)
**Story goal**: `android-build` exists with SSH key login from laptop; `/srv/rom` owned correctly; meta tree at fixed path.
**Independent test**: `ssh android-build@rom-dev01` from laptop; `/srv/rom/artifacts` writable; `/home/android-build/sunfish-a16-rom` contains Spec Kit tree (after T013).
### Implementation
- [x] T007 [US1] Create unprivileged user `android-build` with home `/home/android-build` (admin); document uid/gid in `docs/001-host-runbook.md`
- [x] T008 [US1] Configure SSH public-key access for `android-build` using the developer's existing public key only (do not copy private keys; do not modify global sshd/`saltenv=base` policy unless required). Verify from the laptop that `ssh android-build@rom-dev01` succeeds. Document Cursor `Host` entry (`User android-build`) in `docs/001-host-runbook.md`
- [x] T009 [US1] Create `/srv/rom/{src,references,tooling,ccache,artifacts,logs,secrets}` with ownership for `android-build` on writable trees; `secrets/` restricted/empty — document modes in `docs/001-host-runbook.md`
- [x] T010 [US1] Verify: `namei -l /srv/rom /srv/rom/artifacts` and `sudo -iu android-build touch /srv/rom/artifacts/.write_test` succeeds; no `chmod 777`
- [x] T011 [US1] Document account model (admin vs `android-build` vs service users) in `docs/001-host-runbook.md` — never build Android as root
- [x] T012 [US1] Copy laptop staging Git tree to `/home/android-build/sunfish-a16-rom` preserving `.git` (rsync/scp/bundle); verify `test -f /home/android-build/sunfish-a16-rom/specs/001-development-environment/spec.md`
- [x] T013 [US1] Ensure `/srv/rom/tooling` is **not** used as Spec Kit checkout; note distinction in `docs/001-host-runbook.md`
---
## Phase 4: User Story 2 — Android host toolchain (P1)
**Story goal**: Clean-login toolchain ready for later `003` (no `repo sync` / build).
**Independent test**: `ssh android-build@rom-dev01` → `git`, `git lfs`, `repo`, `python3`, `ccache -s`, and LOS-doc Java/prereqs succeed.
### Implementation
- [x] T014 [US2] As admin, install host packages per **current** primary LineageOS/AOSP Ubuntu build documentation; record exact package list in `/srv/rom/logs/toolchain-packages.txt`
- [x] T015 [US2] Install/configure `repo` for `android-build` per current docs; verify `ssh android-build@rom-dev01 'repo --version'`
- [x] T016 [US2] Ensure `git` + `git-lfs` for `android-build`; verify `ssh android-build@rom-dev01 'git lfs version'`
- [x] T017 [US2] Configure ccache: `CCACHE_DIR=/srv/rom/ccache`, ownership `android-build`, size cap; document env in `/home/android-build/.bashrc` or `/home/android-build/.profile` and `docs/001-host-runbook.md`
- [x] T018 [US2] Verify: `ssh android-build@rom-dev01 'ccache -s'` shows configured cache dir
- [x] T019 [US2] Document FR-007 capacity (6 vCPU / 52 GB / 32 GB swap) and **defer `-j`/OOM tuning to `003`** in `docs/001-host-runbook.md` — do not run a Lineage build
- [x] T020 [US2] Clean-shell toolchain smoke checklist executed; paste command outputs into `/srv/rom/logs/toolchain-smoke.txt` and tick FR-002/SC-001 in `docs/001-verification-matrix.md`
---
## Phase 5: User Story 5 — Laptop thin client (P1/P2 overlap — do after US1–US2)
**Story goal**: Cursor Remote SSH as `android-build` + artifact rsync proven; Pixel stays on laptop USB.
**Independent test**: Remote SSH edit on host as `android-build`; dummy artifact SHA256 matches after rsync; laptop has `adb`/`fastboot` binaries.
### Implementation
- [x] T021 [US5] Document Cursor Remote SSH → `/home/android-build/sunfish-a16-rom` with `User android-build` in `docs/001-host-runbook.md` and update `specs/001-development-environment/quickstart.md` if needed
- [x] T022 [US5] Create `/srv/rom/artifacts/dummy-001.txt` as `android-build`; record `sha256sum` in `/srv/rom/logs/rsync-smoke.txt`
- [x] T023 [US5] From laptop, `rsync` dummy artifact; verify SHA256 match; record laptop path in `/srv/rom/logs/rsync-smoke.txt` and `docs/001-verification-matrix.md` (SC-002)
- [x] T024 [US5] On laptop only: run `adb --version` and `fastboot --version` (do **not** connect or flash the Pixel); record versions in `docs/001-host-runbook.md`. Document adb/fastboot-on-laptop / no USB passthrough rule (SC-004 partial)
---
## Phase 6: User Story 3 — Gitea, Ansible, Semaphore, off-VM mirror (P2)
**Story goal**: Private Gitea with four repos; canonical meta remote; `sunfish-rom-build` committed+pushed; laptop pull mirrors; unprivileged Semaphore smoke from canonical repo.
**Independent test**: Four repos on Gitea; meta history pushed; `sunfish-rom-build` fetch shows smoke playbook commit; laptop `git clone --mirror` works; Semaphore runs unprivileged smoke.
### Implementation
- [x] T025 [US3] Install Gitea on `rom-dev01` with systemd + persistent data; bind UI to loopback/non-public; document unit/paths in `docs/001-host-runbook.md`
- [x] T026 [US3] Store Gitea admin credentials only in restricted host location under `/srv/rom/secrets/` or service-native secret path — **never** commit to Git; document location (not secrets) in `docs/001-host-runbook.md`
- [x] T027 [US3] Verify: local `curl` to Gitea UI succeeds; confirm not relying on public Internet exposure; record bind address in `docs/001-host-runbook.md`
- [x] T028 [US3] Document Git transport chosen after host inspection (HTTP via SSH tunnel and/or tunneled Git SSH); **do not** open public `:2222` or blindly edit `saltenv=base` sshd — write choice in `docs/001-host-runbook.md`
- [x] T029 [US3] Create Gitea repos: `sunfish-a16-rom`, `sunfish-rom-manifest`, `sunfish-rom-build`, `sunfish-rom-patches`
- [x] T030 [US3] Push `/home/android-build/sunfish-a16-rom` history to Gitea `sunfish-a16-rom`; set `origin` to Gitea; verify remote in that directory
- [x] T031 [US3] Seed minimal structure in a working tree of `sunfish-rom-build` for Ansible (e.g. `ansible/` playbooks path) — **not** inside Spec Kit meta repo
- [x] T032 [US3] Install Ansible on `rom-dev01`; document version in `docs/001-host-runbook.md`
- [x] T033 [US3] Add unprivileged smoke playbook under `sunfish-rom-build` (gather_facts + asserts for `/srv/rom`, `android-build`, toolchain); **no become/sudo secrets**
- [x] T034 [US3] Run smoke playbook successfully via `ansible-playbook` CLI; log output to `/srv/rom/logs/ansible-smoke.txt`
- [x] T035 [US3] Commit and push the initial `sunfish-rom-build` structure and smoke playbook to its canonical Gitea repository; verify the pushed commit is visible from a fresh fetch/clone (document clone URL/path in `docs/001-host-runbook.md`)
- [x] T036 [US3] Install Semaphore; bind like Gitea (loopback/tunnel); no public exposure; document in `docs/001-host-runbook.md`
- [x] T037 [US3] Wire Semaphore so it executes the smoke playbook **originating from the canonical `sunfish-rom-build` repository**, using the simplest private mechanism supported by the actual installation (token/HTTP/SSH — decide at implement). Run once successfully; evidence in `/srv/rom/logs/semaphore-smoke.txt` (FR-005). Still unprivileged / no sudo password in Semaphore
- [x] T038 [US3] From **laptop**, create bare mirrors with `git clone --mirror` for all four repos (laptop initiates connection); run `git remote update` + `git fsck`; document mirror paths in `docs/001-host-runbook.md` (FR-010/SC-007)
- [x] T039 [US3] Confirm laptop staging `/home/aventura/Workspaces/sunfish-a16-rom` is **retained** until T030+T035+T038 verified; only then optionally mark staging as non-canonical in `docs/001-host-runbook.md`
---
## Phase 7: User Story 4 — Guest isolation (P2)
**Story goal**: Isolation checklist passes without breaking SSH.
**Independent test**: Public-NIC-only workflow; UIs non-public; SSH still works; no VPN/internal DNS required in docs.
### Implementation
- [x] T040 [US4] Document interfaces/routes: project workflow requires only public NIC — `docs/001-host-runbook.md` + evidence in `docs/001-verification-matrix.md` (SC-006)
- [x] T041 [US4] Apply isolation/firewall guidance **additively** if needed: never flush nftables/iptables; never close active SSH before tested alternative; record before/after in `/srv/rom/logs/firewall-notes.txt`
- [x] T042 [US4] Re-verify Gitea/Semaphore binds remain non-public; SSH admin + `android-build` + tunnel still work — update `docs/001-verification-matrix.md` FR-009/SC-006
- [x] T043 [US4] Confirm day-to-day runbook does not require SSYSTEMS VPN/internal DNS/LDAP/GitLab/NFS/Zabbix — `docs/001-host-runbook.md`
---
## Phase 8: Polish & cross-cutting
- [x] T044 [P] Fill all FR-*/SC-* evidence rows in `docs/001-verification-matrix.md` (including SC-005: no PVE/qm/Salt VM tasks were performed)
- [x] T045 [P] Align `specs/001-development-environment/quickstart.md` with final ports/paths/tunnel commands actually used
- [x] T046 Restart Gitea (+ Semaphore) via systemd; confirm recovery; optional reboot when safe; note results in `/srv/rom/logs/restart-smoke.txt`
- [x] T047 Declare handoff in `docs/001-host-runbook.md`: **`001` complete → proceed to `002`**, do not start Lineage sync/build
- [x] T048 Verify that no credentials/tokens/private keys are present in the **current trees OR committed history** of `sunfish-a16-rom` and `sunfish-rom-build`. Review current files plus reachable Git history; if a secret was ever committed, remove it from history and rotate/revoke it before closing `001`. Method may be manual/`git log -p`/`git rev-list` review (no mandatory Gitleaks/TruffleHog)
---
## Dependencies & story order
```text
Phase 1 (laptop docs)
↓
Phase 2 preflight (host)
↓
US1 user + SSH key + /srv/rom + meta copy
↓
US2 toolchain ──────────────┐
↓ │
US5 thin-client smokes ←───┘ (needs artifacts + meta path + android-build SSH)
↓
US3 Gitea → repos → Ansible in sunfish-rom-build → commit/push → Semaphore → laptop mirrors
↓
US4 isolation hardening/validation
↓
Polish / handoff → STOP until implement; next feature 002 after 001 done
```
**MVP (minimum useful host)**: complete through US2 + US5 (T007–T024) even if Gitea delayed — but **formal `001` done** requires P2 (US3+US4+polish) per plan.
## Parallel opportunities
- T002/T003 on laptop before VM
- After T009: T011 docs can parallel T012 copy if disk ready
- T014–T016 package/`repo`/`git-lfs` partly serial (apt) then T017 ccache
- T044/T045 polish docs in parallel after evidence exists
## FR/SC coverage map
| ID | Tasks |
|----|-------|
| FR-001 | T007–T011, T008 |
| FR-002 | T014–T018, T020 |
| FR-003 | T009–T010, T013 |
| FR-004 | T025–T030 |
| FR-005 | T031–T037 |
| FR-006 | T021–T024 |
| FR-007 | T019 |
| FR-008 | T012, T030 |
| FR-009 | T040–T043 |
| FR-010 | T038–T039 |
| SC-001 | T020 |
| SC-002 | T022–T023 |
| SC-003 | T029–T030 |
| SC-004 | T021, T024, T047 |
| SC-005 | T005, T044 |
| SC-006 | T040–T043 |
| SC-007 | T038 |
## Implementation strategy
1. Do **not** execute host tasks until SSH to `rom-dev01` works.
2. After analyze approval: STOP until VM exists, then `/speckit-implement`.
3. Follow P1 then P2; stop/report on Salt conflicts.
4. Do not plan `002` until `001` implement + verification complete.
@@ -0,0 +1,3 @@
# Contracts: N/A for 002
No HTTP/API contracts. Device observation + documentation only.
@@ -0,0 +1,30 @@
# Data model: 002-axp-reference-capture
Operational entities only (no application domain model).
## DeviceRole
| Field | Notes |
|-------|-------|
| role | `GOLDEN` \| `LAB` |
| serial_or_label | Measured; may be redacted in public docs |
| bootloader_state | Measured (`fastboot getvar unlocked` / UI) |
| build_fingerprint | From `getprop` |
| flash_policy | GOLDEN=forbidden; LAB=allowed |
## GoldenInventory
Essential package/config rows: name, version, cert fingerprint (short), path,
notes, `re-query` flag.
## BankingFlow
Ordered steps 1–8 from spec; redacted (no passwords/OTPs/PII).
## LabRescueBundle
Factory zip path on host, SHA-256, Google source URL, flash steps, wipe warning.
## ReferenceArchive
Host path `/srv/rom/references/axp/archive/` — sunfish-scoped AXP materials.
+112
View File
@@ -0,0 +1,112 @@
# Implementation Plan: AXP.OS GOLDEN reference capture
**Branch**: `002-axp-reference-capture` | **Date**: 2026-09-13 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/002-axp-reference-capture/spec.md`
## Summary
Identify GOLDEN vs LAB sunfish, write LAB Google factory-image emergency
rescue, capture essential live GOLDEN AXP inventory + banking flow (redacted),
and store raw artifacts under `/srv/rom/references/axp/` on `rom-dev01` while
committing only redacted docs to `sunfish-a16-rom`. Parallel: rsync existing
laptop AXP archive per `docs/axp-sunfish-preservation.md` (does not replace
live `adb`).
## Technical Context
**Language/Version**: Bash + `adb`/`fastboot` inventory scripts; Markdown docs
**Primary Dependencies**: OpenSSH to `rom-dev01`; Android platform-tools on
laptop; optional wget/curl for factory images on host
**Storage**: `/srv/rom/references/axp/` (raw); Git = redacted summaries only
**Testing**: Observational checklists (getprop/dumpsys excerpts); no unit suite
**Target Platform**: Two physical sunfish + `rom-dev01` Ubuntu 24.04
**Project Type**: Device documentation / reference capture
**Performance Goals**: N/A — correctness and non-invention of device facts
**Constraints**: Never flash GOLDEN; no secrets/APKs in Git; NeoBackup not a gate;
bootloader state measured not assumed; gaps marked “re-query GOLDEN”
**Scale/Scope**: One developer; essential inventory for `004`–`006` hard-gates
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Independence from SSYSTEMS: yes (Gitea/host paths only)
- GOLDEN do-not-flash: enforced in docs + FR-006
- No Magisk-as-production-path invention: Magisk recorded as GOLDEN oracle only
- Secrets/APKs off Git: FR-005 / SC-005
- Prefer primary sources: Google factory images page; live `adb` over blogs
**Post-design**: Still pass — plan does not invent package versions.
## Project Structure
### Documentation (this feature)
```text
specs/002-axp-reference-capture/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/ # N/A (README only)
└── tasks.md
```
### Source / artifact layout
```text
# In-repo (redacted)
docs/device-roles.md # update serials/labels + lock notes
docs/002-golden-inventory.md # essential inventory + banking flow
docs/002-lab-stock-rescue.md # LAB factory rescue
docs/axp-pro-parity.md # cross-links only (non-normative)
# On rom-dev01 (raw; not Git)
/srv/rom/references/axp/
├── archive/ # rsync from ~/Archives/AXP.OS
├── golden/ # adb pulls, APK copies, dumpsys (restricted)
├── lab/ # factory image zip + checksums
└── logs/ # capture session logs
```
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| — | — | — |
## Phases
### P0 — Docs + archive seed (no phone required)
1. Freeze plan/research/tasks.
2. Rsync laptop `~/Archives/AXP.OS` → `/srv/rom/references/axp/archive/`.
3. Draft LAB stock rescue from current Google sunfish factory image listing;
download image+hash to `/srv/rom/references/axp/lab/` when network allows.
### P1 — Device roles + measured identity (phones required)
1. Connect GOLDEN and LAB; record serials/labels; `getprop` + fastboot lock.
2. Update `docs/device-roles.md` + `docs/002-golden-inventory.md` stubs with
measured facts only.
### P2 — Essential GOLDEN inventory + banking flow
1. Scripted `adb` collection into `/srv/rom/references/axp/golden/` (redact
before any Git paste).
2. Write banking install order from GOLDEN observation.
3. Cross-link parity matrix rows that become known.
### Gate before `003`
LAB rescue doc reviewed; GOLDEN explicitly do-not-flash; FR-006 satisfied.
**No** Lineage sync in this feature.
@@ -0,0 +1,21 @@
# Quickstart: 002-axp-reference-capture
## Prerequisites
- `001` done (`rom-dev01`, `android-build`, `/srv/rom`)
- Laptop: `adb` / `fastboot`
- Both phones labeled (or serials ready)
## Path
1. Confirm no device attached confusion: `adb devices -l`
2. Capture GOLDEN then LAB (or vice versa) — never flash GOLDEN
3. Store raw under `/srv/rom/references/axp/golden/` (rsync from laptop pulls)
4. Update redacted `docs/002-golden-inventory.md` + banking flow
5. Verify LAB rescue zip + hash on host before any `003` flash
## Gate before 003
- [ ] GOLDEN marked do-not-flash in docs
- [ ] LAB factory rescue doc + image hash present
- [ ] Essential inventory + banking flow (gaps = re-query, not invented)
@@ -0,0 +1,47 @@
# Research: 002-axp-reference-capture
## R1 — Live GOLDEN vs offline AXP archive
**Decision**: Treat live GOLDEN `adb` as the compatibility oracle. Offline archive
under `/srv/rom/references/axp/archive/` is supporting reference (docs/git/
images), never a substitute for measured device state.
**Rationale**: Spec FR-003/SC-003; constitution two-device model.
## R2 — NeoBackup / wipe GOLDEN
**Decision**: Out of scope / not a gate (spec + `docs/device-roles.md`).
## R3 — LAB factory rescue source
**Decision (measured 2026-09-13)**: Target build `TQ3A.230805.001.S1` /
`sunfish-tq3a.230805.001.s1-factory-82902ea9.zip`, SHA-256
`82902ea98c80f1a22706279a30d2bc46aa55cbe60703c9a02ba043bd052c6d99`.
Google CDN URL for that zip now returns **404**; sunfish section on
`developers.google.com/android/images` is effectively pruned. Download from
Internet Archive item `sunfish-tq3a.230805.001.s1` and **verify SHA-256** against
the historical Google/Calyx hash before treating it as a rescue image.
**Alternatives**: Other community mirrors only if IA fails; same hash required.
## R4 — Raw artifact location
**Decision**: `/srv/rom/references/axp/{archive,golden,lab,logs}/` on
`rom-dev01`. Git holds redacted Markdown only.
## R5 — Inventory depth
**Decision**: Essential checklist from spec US2; missing items marked
`re-query GOLDEN` — never invent versions/UIDs.
## R6 — Magisk on GOLDEN
**Decision**: Record Magisk/DenyList as **oracle facts**. Project production
root path remains KernelSU-Next in later features; Magisk is not promoted as
our stage.
## R7 — Parallelism with archive rsync
**Decision**: Start laptop→host AXP archive rsync as soon as `001` host is
ready; does not block writing LAB rescue docs; does not wait for phone `adb`.
+183
View File
@@ -0,0 +1,183 @@
# Feature Specification: AXP.OS GOLDEN reference capture
**Feature Branch**: `002-axp-reference-capture`
**Created**: 2026-08-09
**Status**: P1 complete (gate to `003`); archive rsync may still finish in background
**Input**: Document and sample the **GOLDEN** daily-driver AXP.OS sunfish as a
live compatibility oracle; label GOLDEN vs LAB; prepare **LAB** stock factory
rescue. Experimental flashing stays on LAB only (`docs/device-roles.md`).
## Context / non-goals
- There are **two** sunfish devices: **GOLDEN** (**AXP.OS 22.2 / Android 15**
daily driver — do not experiment/flash) and **LAB** (flash/reflash freely).
- Exact GOLDEN build fingerprint/patch is **measured** here (`getprop`). GOLDEN
is AXP 22.2/A15 — **not** the last GrapheneOS sunfish release (that was
**A13**, historical reference only).
- Broader offline AXP *sunfish* archive (website, selected git, images) is
documented in `docs/axp-sunfish-preservation.md` for execution on `rom-dev01`
— not a full Disroot org clone; does not replace live GOLDEN `adb`.
- Capture **essential** versions/config up front; GOLDEN remains available for
later live `adb` re-queries — do not require an exhaustive one-shot dump of
everything before `003`.
- **NeoBackup / wipe / migrate GOLDEN are not project requirements.**
- Do **not** invent package versions/UIDs in the constitution; this feature
records what GOLDEN actually runs.
- Do not commit credentials, tokens, or personal identifiers to Git.
- Observe devices; do **not** assume bootloader unlock state until measured
(both GOLDEN and LAB).
- Product intent vs AXP Pro lives in non-normative `docs/axp-pro-parity.md`.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Device roles + LAB stock rescue (Priority: P1)
As the developer, GOLDEN and LAB are clearly identified, and LAB has a written
Google factory-image emergency rescue so experiments cannot permanently brick
the lab phone.
**Why this priority**: Prevents flashing the wrong phone; keeps LAB recoverable.
**Independent Test**: Serials/labels documented; factory image procedure for LAB
reviewed; GOLDEN marked do-not-flash.
**Acceptance Scenarios**:
1. **Given** two sunfish devices, **When** I record serials (or equivalent IDs)
and physical/doc labels, **Then** GOLDEN vs LAB cannot be confused.
2. **Given** GOLDEN and LAB, **When** I inspect bootloader/fastboot/`getprop`
on each, **Then** lock state and build identity are recorded without prior
assumptions.
3. **Given** official Google sunfish factory images, **When** I document image
set, hashes, and flash steps for **LAB**, **Then** LAB emergency stock rescue
exists before `003` experiments.
4. **Given** project docs, **When** reviewed, **Then** they state GOLDEN must
not receive experimental project flashes.
---
### User Story 2 - Essential GOLDEN inventory (Priority: P1)
As the developer, I have enough structured inventory of GOLDEN AXP to start
`004`–`006`, knowing I can re-probe GOLDEN later for missing details.
**Why this priority**: Defines the live oracle without blocking on perfection.
**Independent Test**: Essential checklist complete or explicitly deferred with
“re-query GOLDEN later” notes — no silent unknowns for hard-gates.
**Acceptance Scenarios**:
1. **Given** GOLDEN AXP, **When** I collect `getprop` / fingerprints, **Then**
exact ROM/build identifiers are stored.
2. **Given** microG, **When** inventoried at essential level, **Then** recorded:
version, signing cert; Self-Check; Device Registration; Cloud Messaging;
relevant permissions.
3. **Given** Phonesky / `com.android.vending`, **When** inventoried, **Then**
recorded: version; APK path; cert; permissions; spoofing state; useful
`dumpsys package` / priv-app XML notes.
4. **Given** Insular, **When** inventoried, **Then** recorded: profile IDs;
profile owner; `dumpsys device_policy` excerpt; owner vs work UIDs/packages;
Yuh/Swissquote locations.
5. **Given** Magisk/root, **When** inventoried, **Then** recorded: Magisk
version; modules; **DenyList including processes**.
6. **Given** GSF Proxy and related packages, **When** inventoried, **Then**
versions/paths/signatures are recorded.
7. **Given** banking interest, **When** measurable, **Then** record observational
Play Integrity (or equivalent) state plus Yuh/Swissquote pass/fail on GOLDEN
— comparative baseline only, not a Strong Integrity promise for our ROM.
8. **Given** Daily-driver MVP features we will replace natively, **When**
capturing modules/settings, **Then** document current call-recording and
FLAG_SECURE bypass behaviour/source on GOLDEN (AXP vs module; global/per-app).
---
### User Story 3 - Golden banking flow on GOLDEN (Priority: P1)
As the developer, the known-good banking install order is written from GOLDEN
so `006` can reproduce it on LAB (with KernelSU substituting DenyList later).
**Why this priority**: Order is load-bearing.
**Independent Test**: Step list matches GOLDEN procedure.
**Acceptance Scenarios**:
1. **Given** GOLDEN, **When** I document the banking app install flow, **Then**
it explicitly includes:
1. Aurora Store → install
2. **Do not open**
3. Magisk DenyList **before** first process (include processes)
4. Clone into Insular
5. First launch inside Insular
6. Phonesky/Play in Insular
7. Play update inside work profile if required
8. Verify login/biometrics/push/reboot as applicable
2. **Given** that flow, **When** stored in the redacted summary, **Then** it
omits passwords, OTPs, and personal identifiers.
---
### User Story 4 - Safe artifact handling (Priority: P2)
As the developer, sensitive captures stay off Git; redacted summaries are
committed to `sunfish-a16-rom`.
**Acceptance Scenarios**:
1. **Given** raw pulls/APKs, **When** stored, **Then** they live under e.g.
`/srv/rom/references/axp/` on `rom-dev01`, not in Git.
2. **Given** in-repo summary, **When** reviewed, **Then** it has versions/hashes/
workflow steps without personal secrets.
---
### Edge Cases
- GOLDEN encrypt/PIN: document without storing the PIN.
- Missing detail later: re-attach GOLDEN and `adb` — do not invent.
- LAB brick: use LAB stock factory rescue; do not wipe GOLDEN to “recover”.
- Optional personal NeoBackup of GOLDEN is outside project gates.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: Label and record GOLDEN vs LAB identities; measure bootloader/
fastboot state on both.
- **FR-002**: Document LAB Google factory-image emergency rescue (hashes + steps).
- **FR-003**: Capture essential GOLDEN inventory (microG, GSF, Phonesky, Insular,
Magisk/DenyList processes, Yuh/Swissquote, call-recording + FLAG_SECURE
sources, observational Play Integrity if measurable).
- **FR-004**: Document GOLDEN banking flow (Aurora → NO OPEN → DenyList →
Insular → Play update).
- **FR-005**: Store raw artifacts off-Git; commit only redacted docs.
- **FR-006**: Gate: no experimental flash on LAB until LAB rescue docs exist and
GOLDEN is explicitly marked do-not-flash; **no** NeoBackup/wipe of GOLDEN
required.
- **FR-007**: Cross-link useful facts into `docs/axp-pro-parity.md` /
`docs/device-roles.md` without inventing unseen features.
### Key Entities
- **GOLDEN**: live AXP oracle (not wiped for bring-up).
- **LAB**: experimental flash target + stock rescue docs.
- **Reference bundle**: host-local capture directory.
- **Redacted summary**: in-repo documentation.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: GOLDEN vs LAB clearly identified; agents/docs forbid flashing GOLDEN.
- **SC-002**: LAB stock factory rescue written and reviewed before `003`.
- **SC-003**: Essential GOLDEN inventory + banking flow complete enough for
`006` hard-gates; gaps marked “re-query GOLDEN” not invented.
- **SC-004**: Yuh/Swissquote verified working on GOLDEN and documented.
- **SC-005**: Git has no proprietary APK dumps or personal credential material.
- **SC-006**: Bootloader states in docs match measured GOLDEN and LAB reality.
+59
View File
@@ -0,0 +1,59 @@
# Tasks: AXP.OS GOLDEN reference capture
**Input**: Design docs from `/specs/002-axp-reference-capture/`
**Prerequisites**: `001` complete on `rom-dev01`
## Phase 1: Setup (no phone)
- [x] T001 Confirm active feature `specs/002-axp-reference-capture` in `.specify/feature.json`
- [x] T002 [P] Create `docs/002-lab-stock-rescue.md` stub (LAB only; Google factory source)
- [x] T003 [P] Create `docs/002-golden-inventory.md` stub (essential checklist + banking flow placeholders)
- [x] T004 [P] Create host dirs `/srv/rom/references/axp/{archive,golden,lab,logs}` as `android-build`
## Phase 2: Offline AXP archive seed (no phone)
- [~] T005 Rsync `~/Archives/AXP.OS/` → host archive *(~18G/38G in progress; not a `003` flash gate)*
- [x] T006 Record archive size + SNAPSHOT_DATE in `/srv/rom/references/axp/logs/archive-rsync.txt` *(partial; refresh when rsync ends)*
## Phase 3: LAB stock rescue (P1 — may download without phone)
- [x] T007 Resolve current official sunfish factory image URL + SHA-256 from Google primary page (or document pruning if only one remains) *(Google CDN 404; hash + IA fallback documented)*
- [x] T008 Download factory zip to `/srv/rom/references/axp/lab/`; verify SHA-256 *(VERIFY_OK 2026-09-13)*
- [x] T009 Complete `docs/002-lab-stock-rescue.md` with flash-all steps, wipe warning, LAB-only scope
- [x] T010 Cross-check FR-002/SC-002 in a short verification note in that doc
## Phase 4: Device roles measured (P1 — phones required)
- [x] T011 Connect GOLDEN (AXP A15 main); record serial + `getprop` into golden inventory (raw log on host) — **adb sample only, no modifications** *(serial `11071JEC203839`)*
- [x] T012 Connect LAB; record serial/label + build identity *(serial `11231JEC207477`, iodé/LOS 7.8 A16 — see `docs/002-lab-iodes-baseline.md`)*
- [x] T013 Measure bootloader/fastboot unlock state on **both**; update `docs/device-roles.md` + inventory (SC-006) *(GOLDEN unlocked/orange; LAB locked/green — via getprop)*
- [x] T014 Explicit “do not flash GOLDEN” callout in `docs/device-roles.md` and inventory (SC-001)
## Phase 5: Essential GOLDEN inventory (P1 — GOLDEN adb)
- [x] T015 Capture microG essential fields (version, cert, Self-Check notes) *(version + sig short; Self-Check UI = re-query)*
- [x] T016 Capture Phonesky / `com.android.vending` essentials *(Phonesky_AXP-OS 50.2.12)*
- [x] T017 Capture Insular / work profile essentials (`dumpsys device_policy` excerpt redacted) *(Island 6.4.2, user 10)*
- [x] T018 Capture Magisk version, modules, DenyList **including processes** *(Magia UI DenyList packages from screenshots; Magisk 29.0; process expand partial; modules dir not readable without root)*
- [x] T019 Capture GSF Proxy / related packages versions/paths/signatures
- [x] T020 Record observational Play Integrity + Yuh/Swissquote pass/fail on GOLDEN (SC-004) *(Swissquote PASS; Yuh FAIL HA / certificate invalid; Finsky PI token path still completes; keystore2 TEE errors in log)*
- [x] T021 Document call-recording + FLAG_SECURE bypass source on GOLDEN *(deferred — re-query; not blocking `003`)*
- [x] T022 Mark any missing rows `re-query GOLDEN` — do not invent
## Phase 6: Banking flow (P1)
- [x] T023 Write redacted GOLDEN banking install order / observed layout in `docs/002-golden-inventory.md`
- [x] T024 Confirm no passwords/OTPs/PII in Git docs (SC-005)
## Phase 7: Polish / gate
- [x] T025 Cross-link known facts into `docs/axp-pro-parity.md` without inventing features (FR-007)
- [x] T026 FR/SC checklist pass for gate: LAB rescue ready; GOLDEN do-not-flash; inventory+flow sufficient for `006` *(see `docs/002-gate-handoff.md`)*
- [x] T027 Handoff note: **`002` complete → `003` on LAB only**
- [x] T028 Verify Git trees contain no proprietary APK dumps or credential files
## Parallel opportunities
- T002–T004 parallel; T005 archive rsync while drafting T007–T009
- T011–T014 require physical devices; block inventory until done
@@ -0,0 +1,3 @@
# Contracts: N/A for 003
No HTTP/API contracts. Build/flash/smoke documentation only.
@@ -0,0 +1,27 @@
# Data model: 003-lineage-23-vanilla-build
## VanillaBuild
| Field | Notes |
|-------|-------|
| manifest_url / branch | From `repo init` |
| resolved_manifest | `repo manifest -r` artifact |
| lunch_target | e.g. `lineage_sunfish-userdebug` (confirm at build) |
| build_id / date | Host timestamp |
| artifact_paths | img/zip under `/srv/rom/artifacts/003-…` |
| sha256 | Per artifact |
| build_log | Compressed under `/srv/rom/logs` |
## LabFlashSession
| Field | Notes |
|-------|-------|
| serial | Must be LAB `11231JEC207477` |
| bootloader_state_before/after | Measured |
| flash_steps | Documented |
| boot_ok | bool |
| getprop_fingerprint | Post-boot |
## SmokeChecklist
P1 + extended rows from spec; each `pass` / `fail` / `skip` + evidence.
@@ -0,0 +1,80 @@
# Implementation Plan: LineageOS 23.2 vanilla build for sunfish
**Branch**: `003-lineage-23-vanilla-build` | **Date**: 2026-09-13 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/003-lineage-23-vanilla-build/spec.md`
## Summary
On `rom-dev01`, sync official LineageOS **23.2** for `sunfish`, build
`userdebug` (test-keys), publish hashed artifacts under `/srv/rom/artifacts`,
`rsync` to the laptop, flash **LAB only** (`11231JEC207477`), and run P1 +
extended hardware smoke. No microG, KernelSU, Magisk, or hardening patches.
On flash failure, use `docs/002-lab-stock-rescue.md` — never touch GOLDEN.
## Technical Context
**Language/Version**: Bash + Lineage `repo` / Soong / ninja (AOSP Android 16)
**Primary Dependencies**: LineageOS 23.2 manifest; device/kernel trees for
sunfish; `ccache`; OpenSSH; laptop `adb`/`fastboot`
**Storage**: `/srv/rom/src` (tree), `/srv/rom/ccache`, `/srv/rom/artifacts`,
`/srv/rom/logs` on `rom-dev01`
**Testing**: Observational boot + hardware checklists (documented results)
**Target Platform**: `rom-dev01` Ubuntu 24.04 build; LAB Pixel 4a flash
**Project Type**: ROM bring-up / reproducible baseline artifact
**Performance Goals**: Complete sync+build with conservative `-j`; survive
OOM via documented recovery
**Constraints**: LAB only; no project patches beyond LOS device needs; no
Magisk/KSU “just to test”; GOLDEN forbidden
**Scale/Scope**: One developer; one known-good vanilla baseline for regression
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Independence from SSYSTEMS: yes
- GOLDEN do-not-flash: enforced (FR-007)
- Functionality-first vanilla base before hardening: yes
- Secrets/APKs off Git: artifacts stay on host/laptop paths; Git = docs/metadata
- Prefer primary sources: Lineage wiki + official manifest
**Post-design**: Pass — plan does not invent device trees or flash GOLDEN.
## Project Structure
### Documentation (this feature)
```text
specs/003-lineage-23-vanilla-build/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
└── tasks.md
```
### Source / artifact layout
```text
# rom-dev01
/srv/rom/src/lineage/ # repo workspace (or documented path)
/srv/rom/artifacts/003-*/ # images, zip, SHA-256, repo manifest -r
/srv/rom/logs/003-*/ # compressed build logs
# laptop
~/…/artifacts/003-*/ # rsync target for flash
docs/003-*.md # flash + smoke results (redacted)
```
## Complexity Tracking
None — single vanilla bring-up path.
@@ -0,0 +1,27 @@
# Quickstart: 003 vanilla LOS 23.2
## Prerequisites
- `001` host ready (`rom-dev01`, `android-build`, ccache, repo)
- `002` gate: LAB rescue zip OK; GOLDEN do-not-flash
- USB to **LAB** only for flash
## Build (host)
```bash
ssh rom-dev01 # as android-build
# repo init/sync lineage-23.2 sunfish — exact commands in tasks / build notes
# breakfast/lunch sunfish userdebug
# mka bacon or documented target; log + hash artifacts into /srv/rom/artifacts
```
## Flash (laptop → LAB)
1. `adb devices` / `fastboot devices` — confirm LAB serial
2. Unlock LAB if needed (wipes device)
3. Flash per LOS wiki / project `docs/003-flash-lab.md` (to be written)
4. If brick/bootloop: `docs/002-lab-stock-rescue.md`
## Smoke
Fill `docs/003-smoke-lab.md` P1 + extended; keep as known-good baseline before `004`.
@@ -0,0 +1,51 @@
# Research: 003-lineage-23-vanilla-build
## R1 — Manifest / branch
**Decision**: Use official LineageOS `lineage-23.2` (or the wiki-documented
sunfish 23.2 branch name measured at implement time). Record exact
`repo init` URL/branch and `repo manifest -r` for the known-good build.
**Rationale**: Constitution base = LOS 23.2 / A16; no AXP/GOS base.
## R2 — Build user / path
**Decision**: Build as `android-build` on `rom-dev01`; tree under
`/srv/rom/src/` (exact subdir documented at init); `CCACHE_DIR=/srv/rom/ccache`.
## R3 — Variant
**Decision**: `userdebug` + LOS default/test-keys for this stage (FR-002).
Release keys / AVB = later (`011`).
## R4 — Flash target
**Decision**: **LAB only** (`11231JEC207477`). Owner approved unlock/wipe for
first custom flash. GOLDEN never flashed. Factory zip on disk =
**emergency rescue only** — owner does not want stock as a planned LAB OS;
skip “Yuh on stock” experiment.
## R4b — Banking / root / Phonesky (owner 2026-09-13)
- Hard-gate focus: **Yuh** (+ Swissquote as tracked); Revolut not a hard-gate.
- Root: **KernelSU-Next + SusFS**; ReSukiSU only if KSU-Next fails on LAB.
- Play: **AXP-style patched Phonesky/GPlay** priv-app (not XDA FakeStore-first).
## R5 — Parallelism / OOM
**Decision**: Start conservative `-j` (e.g. half of vCPUs); document failure
recovery (`out/` cleanup) in build notes. Measure host: ~8 vCPU / 62 GiB RAM.
## R6 — What “vanilla” excludes
**Decision**: No microG, Phonesky, signature-spoof patches, KernelSU/SUSFS,
Magisk, call-recording/FLAG_SECURE ports, Graphene/Divest hardening — even if
present on LAB’s current iodé image (that baseline will be wiped by flash).
## R8 — Proprietary blobs
**Decision (owner 2026-09-13):** Factory/Lineage-equivalent blob base, then
selective **AXP sunfish** diffs and **GrapheneOS** diffs (historical sunfish A13
+ transferable bits from a currently supported Pixel). No exotic blob hunting
for GPS; do not overthink for `003` vanilla (factory-equivalent only).
+183
View File
@@ -0,0 +1,183 @@
# Feature Specification: LineageOS 23.2 vanilla build for sunfish
**Feature Branch**: `003-lineage-23-vanilla-build`
**Created**: 2026-08-09
**Status**: Active — implementation next (vanilla sync/build on `rom-dev01`)
**Input**: Produce the first vanilla LineageOS 23.2 / Android 16 build for
`sunfish`, copy images to the laptop, flash, and verify boot plus basic
hardware — with no microG, KernelSU, or hardening yet.
## Context / non-goals
**Prerequisites:**
- `001-development-environment` sufficiently complete to sync/build;
- `002-axp-reference-capture` P1 complete (GOLDEN labeled; LAB stock rescue
documented). Flash **LAB only** — never GOLDEN (`docs/device-roles.md`).
**Out of scope for this feature:**
- microG / Phonesky / signature spoofing;
- KernelSU / SUSFS / Magisk parity;
- call recording, FLAG_SECURE toggles, privacy extras;
- project signing / AVB / OTA implementation;
- Graphene/AXP/Divest patch ports.
Build target: **`userdebug`** with development/test-keys.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Reproducible vanilla sync & build (Priority: P1)
As the developer, I can sync LineageOS 23.2 for sunfish on `rom-dev01` and
complete a build that emits flashable images, with manifest/revisions recorded.
**Why this priority**: Establishes the real base ROM.
**Independent Test**: Build finishes; artifact paths listed; manifest revision
document stored under `/srv/rom` with hashes.
**Acceptance Scenarios**:
1. **Given** `android-build` on `rom-dev01`, **When** I sync the LOS 23.2
sunfish source via `repo`, **Then** the tree matches a documented manifest
revision.
2. **Given** the synced tree, **When** I build `userdebug` for sunfish with
conservative parallelism, **Then** the build completes without requiring
project patches beyond LOS-needed device/kernel trees.
3. **Given** a successful build, **When** I collect outputs, **Then** images/
zip needed to flash sunfish are in `/srv/rom/artifacts` (or documented path)
with SHA-256 hashes and a compressed build log.
---
### User Story 2 - Flash from laptop (Priority: P1)
As the developer, I `rsync` artifacts to the laptop and flash the Pixel 4a over
USB using fastboot/adb, following the recovery notes from `002` if something
fails.
**Why this priority**: Closes the loop on the thin-client workflow.
**Independent Test**: Device boots the new build; `getprop` shows LineageOS /
expected fingerprints (not AXP).
**Acceptance Scenarios**:
1. **Given** artifacts on the laptop, **When** I flash per documented steps,
**Then** the device boots into the vanilla LOS build.
2. **Given** a failed flash/boot on **LAB**, **When** I follow `002` LAB stock
factory rescue docs, **Then** LAB returns to usable stock. GOLDEN is not
touched.
---
### User Story 3 - Basic hardware smoke test (Priority: P1)
As the developer, after first boot I verify essential radios and peripherals
well enough to trust this build as the clean base for later features — and I
know what was already broken on **vanilla** LOS before we add patches.
**Why this priority**: A boot-only “success” that breaks telephony/Wi-Fi is not
a usable base. Later regressions must be attributable.
**Independent Test**: Checklist results stored for P1 and extended items on the
vanilla build.
**Acceptance Scenarios**:
1. **Given** first boot completed, **When** I run the **P1 smoke** set, **Then**
each item either works or is logged as a blocking defect with evidence:
- encryption / PIN
- SELinux mode (`Enforcing` expected)
- Android security patch level
- vendor security patch level / build baseline
- SIM detection
- mobile data
- phone calls
- SMS (send/receive as applicable)
- Wi-Fi
- Bluetooth
- GPS
- camera(s)
- audio (speaker/mic)
- NFC
- sensors (basic)
- fingerprint
- USB data / ADB
- charging
2. **Given** P1 smoke done, **When** I run the **extended baseline** set,
**Then** each is recorded (need not all be P1 blockers, but must not be
skipped silently):
- airplane mode
- flashlight
- vibration
- screen rotation
- USB OTG (if hardware/cable available)
- headphone jack
- WebView implementation / basic page load
- app sideload install smoke (non-Google)
- Widevine / DRM security level
- eSIM capability/state, if available
- WireGuard kernel capability/baseline
- bootloader version
- baseband / modem version
3. **Given** smoke results, **When** deciding to proceed to `004`, **Then** no
unresolved P1 hardware blockers remain (or they are explicitly accepted with
rationale).
---
### Edge Cases
- Build disk/oom with 52 GB + swap: document job `-j` and failure recovery
(`out/` cleanup).
- Partial artifact set: do not flash until required partitions/images are
present and hashed.
- Do not install Magisk/KernelSU “just to test” in this feature.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: Initialize from the documented official LineageOS 23.2
manifest/branch (`repo init` → sync) and capture a revision-locked resolved
manifest (e.g. `repo manifest -r`) for every known-good build.
- **FR-002**: Build `userdebug` images with test-keys / default LOS signing for
this stage.
- **FR-003**: Record manifest, revisions, build variant, config, hashes, and
compressed log for the successful build.
- **FR-004**: Publish artifacts to `/srv/rom/artifacts` and document `rsync` +
flash procedure.
- **FR-005**: Boot sunfish on the vanilla build and run the P1 + extended
hardware/baseline checklist (record results even for non-blockers).
- **FR-006**: Keep tree free of microG/Phonesky/KernelSU/hardening patches
intended for later features.
- **FR-007**: On LAB flash failure, follow `002` LAB stock factory rescue; never
flash or wipe GOLDEN as part of recovery.
### Key Entities
- **Vanilla LOS build**: clean base artifact set.
- **Build metadata**: manifest + hashes + log bundle.
- **Smoke checklist**: hardware/regression minimum for this stage.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: One successful sunfish `userdebug` build exists with recorded
metadata and hashes.
- **SC-002**: Device boots that build after laptop-side flash.
- **SC-003**: P1 and extended hardware/baseline checklists are completed and
stored (extended items may be non-blocking but must be recorded).
- **SC-004**: No dependency on LSPosed/Magisk modules for this stage’s success.
- **SC-005**: This build is tagged/kept as a known-good flashable baseline for
regression when later features break the device.
@@ -0,0 +1,54 @@
# Tasks: LineageOS 23.2 vanilla build for sunfish
**Input**: Design docs from `/specs/003-lineage-23-vanilla-build/`
**Prerequisites**: `001` done; `002` P1 gate (`docs/002-gate-handoff.md`)
## Phase 1: Prep
- [x] T001 Confirm active feature `specs/003-lineage-23-vanilla-build`
- [x] T002 [P] Create `docs/003-build-notes.md` stub (manifest URL/branch TBD at init)
- [x] T003 [P] Create `docs/003-flash-lab.md` stub (LAB serial check; link rescue)
- [x] T004 [P] Create `docs/003-smoke-lab.md` checklist from spec P1 + extended
- [x] T005 Verify LAB serial still `11231JEC207477`; confirm GOLDEN not attached before any flash *(LAB serial documented; flash later)*
- [x] T006 Confirm host dirs `/srv/rom/{src,artifacts,logs,ccache}` writable by `android-build`
## Phase 2: Sync
- [x] T007 `repo init` official LineageOS 23.2 (wiki-verified URL/branch) under `/srv/rom/src/…` *(`/srv/rom/src/lineage`, `lineage-23.2`)*
- [x] T008 `repo sync` sunfish device/kernel dependencies; record wall time / failures *(finished 2026-09-13T21:27:26Z exit=0; tree ~180G)*
- [x] T009 Save `repo manifest -r` to `/srv/rom/artifacts/003-…/manifest-rev.xml`
## Phase 3: Build
- [x] T010 Configure ccache env; choose parallelism (**`-j8`** = match vCPU)
- [x] T011 `lunch`/`breakfast` sunfish `userdebug`; start build; capture log
- [x] T012 On OOM/disk failure: document recovery; retry (no silent `-j` guessing) *(N/A — no OOM; mid-run bump -j4→-j8)*
- [x] T013 Collect flashable images/zip into `/srv/rom/artifacts/003-…`; SHA-256 all
- [x] T014 Compress build log beside artifacts; write build metadata (variant, date, host)
## Phase 4: Flash LAB
- [x] T015 `rsync` artifacts to laptop *(`${WORKSPACE}/.local-artifacts/003-vanilla/20260914-lineage-23.2-sunfish/`)*
- [x] T016 Unlock LAB bootloader if still locked (owner-approved wipe) — **LAB only**
- [x] T017 Flash per `docs/003-flash-lab.md`; verify serial before every `fastboot` command
- [x] T018 Confirm first boot; `getprop` shows LOS vanilla (not AXP / not prior iodé id)
- [ ] T019 If fail: factory rescue from `002`; do not touch GOLDEN *(N/A so far)*
## Phase 5: Smoke
- [x] T020 Complete P1 smoke checklist in `docs/003-smoke-lab.md`
- [x] T021 Complete extended baseline checklist (record skips with reason)
- [x] T022 Decide gate to `004`: no unresolved P1 blockers (or explicit accept)
## Phase 6: Polish
- [x] T023 Tag/keep known-good baseline note (paths + hashes) for regression
(`docs/003-gate-handoff.md` + artifacts dir)
- [x] T024 Confirm tree has no microG/KSU/hardening patches (FR-006)
(vanilla identity props; no microG packages on LAB pre-004)
- [x] T025 Handoff: **`003` complete → `004`** (microG path) on LAB only
## Parallel opportunities
- T002–T004 while T006 host checks run
- T015 rsync can start as soon as T013 hashes exist
@@ -0,0 +1,35 @@
# Specification Quality Checklist: microG + Phonesky
**Purpose**: Validate specification completeness before planning
**Created**: 2026-09-14
**Feature**: [spec.md](../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
- Spoofing deferred to `005` by design; Self-Check may be partially blocked until then.
- Phonesky signing strategy is FR-010 (document during plan/implement; keys host-only).
@@ -0,0 +1,35 @@
# Contract: system packages (004)
## Packages that MUST be present on LAB after flash
| Package | Type | Notes |
|---------|------|-------|
| `com.google.android.gms` | system/priv | microG |
| `com.google.android.gsf` | system/priv | GsfProxy |
| `com.android.vending` | **priv-app** | Patched Phonesky |
## Packages that MUST NOT be present
| Package | Notes |
|---------|-------|
| FakeStore (typical `com.android.vending` stub from partner_gms) | Conflicts with Phonesky |
| Magisk / KernelSU manager (as ROM-preinstalled) | Out of scope until `007` |
## Host path contract (rom-dev01)
| Path | Contents | In Git? |
|------|----------|---------|
| `/srv/rom/prebuilts/microg/` | GmsCore + GsfProxy APKs + `VERSIONS.txt` | No |
| `/srv/rom/prebuilts/phonesky/` | Phonesky APK + permission XMLs + `VERSIONS.txt` | No |
| `/srv/rom/secrets/phonesky-signing/` | Optional project keys | No |
| Meta-repo `docs/004-*.md` | Runbook + self-check | Yes |
| Product/vendor mk under LOS tree | References prebuilts by path | Tree on host; mirror patches in meta if small |
## Verification commands (LAB)
```bash
adb -s 11231JEC207477 shell pm path com.google.android.gms
adb -s 11231JEC207477 shell pm path com.google.android.gsf
adb -s 11231JEC207477 shell pm path com.android.vending
adb -s 11231JEC207477 shell pm list packages | grep -i fakestore || true
```
@@ -0,0 +1,56 @@
# Data model: 004 packages
## Entities
### MicroGGmsCore
| Field | Description |
|-------|-------------|
| package | `com.google.android.gms` |
| role | microG Services Core |
| install | privileged / system (product or system_ext per chosen mk) |
| source | Official microG release APK on host prebuilts |
| golden_ref | `0.3.15.250932` (AXP inventory) |
### GsfProxy
| Field | Description |
|-------|-------------|
| package | `com.google.android.gsf` |
| role | Legacy GSF / C2DM proxy |
| install | system app or priv-app |
| source | microG GsfProxy release (or rebuilt for targetSdk if needed) |
| golden_ref | `GsfProxy_GH` v0.1.0 on GOLDEN |
### PhoneskyPatched
| Field | Description |
|-------|-------------|
| package | `com.android.vending` |
| role | Patched Play Store |
| install | **priv-app** + privapp-permissions + default-permissions XML |
| source | AXP archive APK (not Git) |
| golden_ref | Phonesky_AXP-OS **50.2.12-24** |
| first_candidate | archive **50.3.27-24** or **51.4.19-24** |
### PermissionConfig
| Artifact | Purpose |
|----------|---------|
| `privapp-permissions-com.android.vending.xml` | Privileged permissions allowlist |
| `com.android.vending.xml` | Feature/permission declarations as needed |
| `phonesky-permissions.xml` (default-permissions) | Default grants |
### ForbiddenCompanion
| package | rule |
|---------|------|
| FakeStore / conflicting Companion | MUST NOT ship alongside Phonesky |
## Relationships
- Phonesky **replaces** FakeStore in the compatibility stack.
- microG Self-Check may require spoofing of GmsCore (and later Phonesky) —
spoofing policy owned by `005`.
- All binaries → host prebuilts → product makefile → image; evidence →
`/srv/rom/artifacts/004-…`.
@@ -0,0 +1,89 @@
# Implementation Plan: microG + GSF Proxy + patched Phonesky
**Branch**: `004-microg-phonesky-integration` | **Date**: 2026-09-14 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/004-microg-phonesky-integration/spec.md`
## Summary
Add the Google compatibility stack to the sunfish LOS 23.2 / Android 16 tree on
**LAB only**: microG GmsCore, GSF Proxy, and **patched Phonesky** as priv-app
(AXP-style), without FakeStore, Magisk, or KernelSU. Signature spoofing beyond
what LOS already provides stays in `005`. Proprietary APKs live only under
`/srv/rom/` (never Git).
## Technical Context
**Language/Version**: Android.mk / Soong (`Android.bp` where needed); bash
runbooks on `rom-dev01`
**Primary Dependencies**: LineageOS 23.2 tree (`/srv/rom/src/lineage`); microG
release APKs; AXP `packages_apps_phonesky` permission XMLs + signed APK from
project AXP archive; LOS restricted signature spoofing (baseline)
**Storage**: Host paths `/srv/rom/prebuilts/{microg,phonesky}/`, secrets under
`/srv/rom/secrets/`
**Testing**: LAB flash + `pm`/`dumpsys` + microG Self-Check screenshots; no
unit-test harness required
**Target Platform**: Pixel 4a (`sunfish`) LAB `11231JEC207477`; Android 16
**Project Type**: Android ROM packaging / device product overlay
**Performance Goals**: N/A (functional compatibility)
**Constraints**: No proprietary APKs in Git; no GOLDEN flash; no Magisk/KSU in
`004`; Phonesky signing keys host-only
**Scale/Scope**: Three system packages + permission XMLs + product makefile +
runbook; one rebuild + LAB flash
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Gate | Status |
|------|--------|
| LAB only; never GOLDEN | pass |
| No proprietary Google APKs in Git | pass (host `/srv/rom/prebuilts`) |
| Phonesky = patched priv-app, not FakeStore primary | pass |
| No Magisk/KSU in Compatibility path before `006`/`007` | pass (`004` rootless) |
| Spoofing restricted / auditable → full policy in `005` | pass (use LOS baseline only here) |
| Prefer AXP reference for packaging | pass (archive Phonesky + XMLs) |
| Prefer small attributable changes | pass (vendor/product overlay, not megapatch) |
**Post-design**: unchanged — design stays packaging + docs.
## Project Structure
### Documentation (this feature)
```text
specs/004-microg-phonesky-integration/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── packages.md
└── tasks.md
```
### Host + tree (not all in Git)
```text
# Meta repo (this workspace)
docs/004-phonesky-runbook.md
docs/004-lab-selfcheck.md
vendor packaging stubs / product mk (synced to rom-dev01 tree)
# rom-dev01 (canonical build)
/srv/rom/src/lineage/ # LOS tree
/srv/rom/prebuilts/microg/ # GmsCore + GsfProxy APKs (gitignored host)
/srv/rom/prebuilts/phonesky/ # Phonesky APK + permission XMLs
/srv/rom/secrets/phonesky-signing/ # keys if we re-sign (optional first pass)
/srv/rom/artifacts/004-…/ # build zip + self-check evidence
```
## Complexity Tracking
| Violation | Why needed | Simpler alternative rejected because |
|-----------|------------|--------------------------------------|
| None | — | — |
## Phase 0 / Phase 1
See `research.md`, `data-model.md`, `contracts/packages.md`, `quickstart.md`.
@@ -0,0 +1,42 @@
# Quickstart: validate 004 on LAB
## Prerequisites
- `003` vanilla known-good on LAB (or re-flash `004` build cleanly)
- Serial **only** `11231JEC207477`
- Host prebuilts populated per `docs/004-phonesky-runbook.md`
## Build (rom-dev01)
```bash
ssh a16-build # or android-build via a16-build alias
# ensure prebuilts exist
cat /srv/rom/prebuilts/microg/VERSIONS.txt
cat /srv/rom/prebuilts/phonesky/VERSIONS.txt
cd /srv/rom/src/lineage
source build/envsetup.sh
breakfast sunfish userdebug
mka bacon -j8
```
Copy zip to `/srv/rom/artifacts/004-…/` + SHA-256; rsync to laptop.
## Flash LAB
Same flow as `docs/003-flash-lab.md` (dtbo → boot recovery → format if needed →
sideload). **No** GApps add-ons beyond what is already in the ROM image.
## Validate
1. `adb devices` shows LAB only.
2. Package contract (`contracts/packages.md`).
3. Open **microG Settings → Self-Check**; screenshot to
`/srv/rom/artifacts/004-…/selfcheck/` (or laptop `.local-artifacts`).
4. Open Play Store; note whether login/install works or is blocked pending
`005`.
5. Confirm no Magisk/KSU: `pm list packages | grep -iE 'magisk|kernelsu'`.
## Pass criteria
- SC-001…SC-005 from `spec.md`
- Spoofing-only Self-Check failures explicitly listed as `005` follow-ups
@@ -0,0 +1,95 @@
# Research: 004 microG + Phonesky
## Decision: Packaging layout — follow AXP GOLDEN scheme (owner 2026-09-15)
**Decision**: Mirror AXP GOLDEN partition roles. Do not invent a `product`-centric
layout. Measured on GOLDEN (`002`):
| Package | AXP GOLDEN path | Our packaging |
|---------|-----------------|---------------|
| Phonesky | `/system/priv-app/Phonesky_AXP-OS` | **system** + privileged |
| GmsCore | `/system_ext/app/MicrogGmsCore_GH_presigned` | **system_ext** app (not priv-app) |
| GsfProxy | `/system_ext/app/GsfProxy_GH` | **system_ext** app |
Ship via a small `vendor/sunfish-a16` include (partner_gms *pattern*) but
**replace FakeStore** with patched Phonesky. Permission XMLs follow the same
partitions (`system` for Phonesky, `system_ext` for microG).
**Rationale**: Owner — AXP methods exist for a reason; goal is modernize (LOS
23.2 / A16) and later replace Magisk with KSU-Next+SusFS (`007`), not redesign
the compatibility stack. Ad-hoc `LOCAL_PRODUCT_MODULE` + ABI-strip/re-sign of
GmsCore risks different update/SELinux/behaviour vs GOLDEN.
**Alternatives rejected**:
- All on `PRODUCT` — layout drift; hit partition checks awkwardly
- ABI-strip + project re-sign of GmsCore — breaks official microG F-Droid signer
- Stock lineageos4microg + FakeStore — wrong Store model
- Magisk module installers — not for `004` (root is `007`, non-Magisk)
**Partition headroom**: AXP release Phonesky APKs stay **fat** (all ABIs); CI
does not strip x86. If LOS 23.2 sunfish `BOARD_SUPER_PARTITION_ERROR_LIMIT` is
too tight for this package set, raise the error limit modestly (still under
`BOARD_GOOGLE_DYNAMIC_PARTITIONS_SIZE`) rather than breaking microG’s signer.
## Decision: Phonesky binary — patch latest, AXP procedure, project sign
**Decision**: Latest official Play (apkmd) → AXP `patch-playstore` → project JKS
under `/srv/rom/secrets/phonesky-signing/`. XMLs from AXP `etc/`. Do **not** ship
`Phonesky_AXP-OS-signed.apk` as product (wrong trust domain). Keep fat APK unless
a later measured need forces arch filtering (document then).
## Decision: microG — latest official PRESIGNED + F-Droid
**Decision**: Latest official GmsCore **default** APK (`com.google.android.gms-*.apk`
without `-hw`) + GsfProxy, `PRESIGNED`, system_ext like AXP `*_GH_presigned`.
Never re-sign. F-Droid microG repo is the update path. No FakeStore/Companion APK
from microG releases.
**Do not ship `*-hw.apk`**: that Huawei flavor omits
`NetworkLocationService` / `NetworkLocationProviderService` from the manifest, so
microG Location hides Wi‑Fi/cell NLP toggles. GOLDEN ships the default flavor
(`versionName` without `-hw`, e.g. `0.3.15.250932`); LAB previously had
`0.3.16.252432-hw` and only Timeline — corrected 2026-09-16 to
`0.3.16.252432` default for GOLDEN Location UI parity.
## Decision: Signature spoofing in `004`
**Decision**: Use LOS 23.2 restricted spoofing only; gaps → `005`.
## Decision: AXP scheme vs Android 16 / LOS 23.2 (owner 2026-09-15)
**Decision**: Prefer the AXP **A15 / GOLDEN** packaging and behaviour scheme when
LOS 23.2 / Android 16 still supports it. If A16 makes an equivalent path
*easier* (cleaner APIs, existing LOS hooks), use that — still targeting the
same end state (priv Play, updatable official-signer microG, banking path).
If A16 makes the AXP path *harder* or impossible, do **not** silently invent a
divergent stack: document the blocker and redesign the route to the **same**
goals (layout roles, update model, no Magisk-shaped packaging).
**Rationale**: AXP’s choices were intentional; A16 is a port/modernization, not
a license to ignore the oracle. Divergence only when measured necessity.
**Oracle scope (owner 2026-09-16)**: GOLDEN and abu are **Android 15** reference
images — use them for packaging/layout/behaviour (e.g. which GmsCore *flavor*
exposes Location NLP), not as a pin to their older package versions. On LOS 23.2
/ A16 we ship the **newest** fitting official microG (and peers) that preserve
that scheme (default/non-hw GmsCore, not `*-hw`).
**Measured A16 blocker (2026-09-14)**: Putting Phonesky on `system/priv-app`
fails `artifact_path_requirements` (`generic_system.mk`) under LOS 23.2.
**Route to same end state**: keep AXP paths and add them to
`PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST` (same mechanism sunfish already
uses for a few system libs) — do not move Phonesky to `product` just to silence
the check. GmsCore/Gsf on `system_ext` already passed.
**Align-first (owner 2026-09-15)**: Do not invent `privapp-permissions` for
GmsCore while GOLDEN ships it as non-privileged `system_ext` app. Ship only
AXP Phonesky XMLs; repair from Self-Check / build errors if A16 truly requires
more.
## Decision: Signing strategy (FR-010) — locked
| Package | Signer | Update path |
|---------|--------|-------------|
| GmsCore / GsfProxy | Official microG (`PRESIGNED`) | F-Droid microG repo |
| Phonesky | Project JKS `/srv/rom/secrets/phonesky-signing/` | Same-signer patched APKs |
@@ -0,0 +1,178 @@
# Feature Specification: microG + GSF Proxy + patched Phonesky
**Feature Branch**: `004-microg-phonesky-integration`
**Created**: 2026-09-14
**Status**: Draft
**Input**: Continue after `003` gate — integrate Google compatibility stack on LAB
vanilla LOS 23.2 / Android 16 (sunfish), without Magisk/KernelSU yet.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - microG Self-Check green on LAB (Priority: P1)
As the developer, after flashing a build that includes microG + GSF Proxy, I can
open microG Settings Self-Check and see the checks required for basic Google
compatibility pass (or document exact remaining gaps owned by `005`).
**Why this priority**: Without a working microG core, Play and banking paths
cannot start.
**Independent Test**: Flash LAB-only build; open microG Self-Check; capture
screenshots/log; no Magisk/KSU installed.
**Acceptance Scenarios**:
1. **Given** LAB on `003` vanilla baseline, **When** a `004` build is flashed,
**Then** `com.google.android.gms` and `com.google.android.gsf` are present as
system packages and microG Settings opens.
2. **Given** microG is configured (device registration as required), **When**
Self-Check runs, **Then** either all P1 checks pass, or each failure is
explicitly attributed to signature spoofing (`005`) vs packaging error.
---
### User Story 2 - Patched Phonesky as priv-app (Priority: P1)
As the developer, patched Play Store (`com.android.vending`) is installed as a
privileged system app with required permission XMLs, obtained/patched/signed via
a documented host-side procedure (APK never committed to Git).
**Why this priority**: Constitution requires AXP-style patched Phonesky, not
FakeStore→Play as primary.
**Independent Test**: Package present as priv-app; permission XMLs deployed;
Play UI launches; update path documented even if spoofing still incomplete.
**Acceptance Scenarios**:
1. **Given** Phonesky artifacts under a restricted host path, **When** the ROM
is built, **Then** `com.android.vending` is a privileged system app with the
required default-permission / privapp-permissions configuration.
2. **Given** the build is flashed to LAB, **When** Play Store is opened,
**Then** it starts (login/install may still depend on `005` spoofing — mark
clearly if so).
3. **Given** Git history, **When** inspected, **Then** no Google proprietary
Phonesky APK bytes are committed.
---
### User Story 3 - No FakeStore conflict (Priority: P2)
As the developer, FakeStore / microG Companion are not installed when they would
conflict with real patched `com.android.vending`.
**Why this priority**: Prevents the wrong Play stub from masking Phonesky.
**Independent Test**: `pm list packages` shows no conflicting FakeStore when
Phonesky is present.
**Acceptance Scenarios**:
1. **Given** Phonesky is included, **When** packages are listed, **Then**
FakeStore is absent (or explicitly disabled with documented reason).
---
### User Story 4 - Reproducible obtain → patch → sign pipeline (Priority: P2)
As the developer, I can re-run a documented procedure on `rom-dev01` to produce
signed Phonesky + microG packaging inputs without tribal knowledge.
**Why this priority**: Required before any updatable Play Store distribution.
**Independent Test**: Second operator (or clean host path) follows the runbook
and produces matching package names/permissions (signature may differ if keys
differ — keys stay host-only).
**Acceptance Scenarios**:
1. **Given** the `004` runbook, **When** followed on `rom-dev01`, **Then**
inputs land under `/srv/rom/…` (restricted) and the build consumes them
without manual one-off hacks.
2. **Given** signing keys, **When** stored, **Then** they live only under
host secrets (e.g. `/srv/rom/secrets/`), never in Git.
---
### Edge Cases
- LOS 23.2 already ships partial microG spoofing → use it; do not invent a full
spoofing stack in `004` (that is `005`).
- Self-Check fails only on signature-related items → record as `005` dependency,
not a `004` packaging failure.
- Play login/install fails while Self-Check packaging is OK → same split.
- Flash/upgrade wipes user data → LAB only; never GOLDEN.
- Wrong device serial → abort flash.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: Integrate **latest** official microG Services Core as
`com.google.android.gms` on the LOS 23.2 sunfish LAB build (do not pin to
GOLDEN’s older version).
- **FR-002**: Integrate GSF Proxy as `com.google.android.gsf` (latest upstream).
- **FR-003**: Integrate patched Phonesky as privileged `com.android.vending`
with required privapp-permissions / default-permissions XML, produced by
patching the **latest** official Play Store APK (not an AXP archive binary).
- **FR-004**: Document and implement a reproducible obtain → patch → sign →
priv-app pipeline for Phonesky on `rom-dev01` using AXP `patch-playstore` /
apktool; proprietary APKs MUST NOT be committed to Git.
- **FR-005**: Do not install FakeStore (or conflicting Companion) alongside
patched Phonesky.
- **FR-006**: Record microG Self-Check results on LAB (pass/fail per item) after
flash; attribute spoofing-related failures to `005`.
- **FR-007**: Keep Magisk and KernelSU out of `004` builds (root arrives in
`007`; banking baseline rootless in `006`).
- **FR-008**: Flash and validate on LAB serial `11231JEC207477` only; never
GOLDEN `11071JEC203839`.
- **FR-009**: Prefer AXP golden packaging behaviour as reference (inventory from
`002`); do not blind-copy Magisk modules or ship AXP-signed Play as product.
- **FR-010**: Phonesky is signed with a **project-owned** stable keystore under
`/srv/rom/secrets/phonesky-signing/` so later patched Builds can update Play.
- **FR-011**: microG GmsCore/GsfProxy MUST remain **PRESIGNED** with the
official microG signer so they stay updatable from the microG F-Droid
repository; document repo setup for LAB Self-Check.
### Key Entities
- **microG Services Core** — `com.google.android.gms`
- **GSF Proxy** — `com.google.android.gsf`
- **Patched Phonesky** — `com.android.vending` (external proprietary input)
- **LAB** — Pixel 4a `11231JEC207477` (Android 16 / LOS 23.2 target)
- **GOLDEN** — AXP Pro Android 15 reference (read-only)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: LAB build includes microG + GSF + patched Phonesky packages with
correct package names.
- **SC-002**: microG Self-Check evidence captured; packaging failures = 0; any
remaining failures mapped to `005` or explicitly accepted.
- **SC-003**: Phonesky opens on LAB; Git contains no proprietary Play APK.
- **SC-004**: Runbook exists such that a clean `rom-dev01` path can rebuild the
same packaging layout.
- **SC-005**: No Magisk/KSU in the `004` validation build.
## Assumptions
- `003` vanilla baseline remains the parent tree; `004` adds packaging/patches
on top without unrelated hardening.
- LineageOS 23.2 microG-oriented spoofing hooks may already exist; full
restricted spoofing policy is `005`.
- `002` / GOLDEN inventory supplies reference behaviour and permission
patterns only — not version pins for microG or Play.
- Phonesky binary is obtained outside Git via latest Play APK + AXP
`patch-playstore` (apktool) + project signing keys on `rom-dev01`.
- Official microG release APKs share the signer used by the microG F-Droid
repo, enabling same-package updates without re-signing.
## Out of Scope
- Signature spoofing implementation beyond using what LOS already provides
(`005`)
- Insular / banking hard-gates (`006`)
- KernelSU (`007`)
- Release signing / AVB relock (`011`)
- Flashing or modifying GOLDEN
@@ -0,0 +1,94 @@
# Tasks: microG + GSF Proxy + patched Phonesky
**Input**: Design docs from `/specs/004-microg-phonesky-integration/`
**Prerequisites**: `003` gate (`docs/003-gate-handoff.md`)
## Phase 1: Setup
- [x] T001 Create host dirs `/srv/rom/prebuilts/{microg,phonesky}/` and
`/srv/rom/artifacts/004-microg/` on `rom-dev01` (android-build writable)
- [x] T002 [P] Add `docs/004-phonesky-runbook.md` (obtain → place APKs → versions
file → no Git) in meta repo
- [x] T003 [P] Add `docs/004-lab-selfcheck.md` stub (Self-Check table + `005`
attribution) in meta repo
## Phase 2: Foundational
- [x] T004 Stage **latest** microG GmsCore + GsfProxy APKs into
`/srv/rom/prebuilts/microg/` + `VERSIONS.txt` (URLs/hashes;
`PRESIGNED` for F-Droid updates) — currently `v0.3.16.252432` /
GsfProxy `v0.1.0` (GitHub latest as of 2026-09-14)
- [x] T005 Obtain **latest** official Play Store (apkmd), patch with AXP
`patch-playstore` + apktool, sign with project JKS under
`/srv/rom/secrets/phonesky-signing/`; stage APK + AXP permission XMLs
into `/srv/rom/prebuilts/phonesky/` + `VERSIONS.txt`
*(Play **52.9.22-24**, not AXP archive)*
- [x] T006 Add LOS product/vendor makefile include that packages the three APKs
as system/priv-app **without FakeStore** (`vendor/sunfish-a16/` on host tree)
- [x] T007 Wire include into `lineage_sunfish` product so `mka bacon` picks it up
## Phase 3: User Story 1 — microG Self-Check (P1)
**Goal**: GmsCore + GsfProxy on LAB; Self-Check evidence
**Independent test**: packages present; Self-Check screenshots
- [x] T008 [US1] Build `004` image (`mka bacon -j8`); log under
`/srv/rom/logs/004-build/` — **superseded by `005` T020 bacon**
(`bacon-20260915T103756Z.log`)
- [x] T009 [US1] Flash LAB only (`11231JEC207477`) per quickstart —
**superseded by `005` T022**
- [x] T010 [US1] Verify `pm path` for `com.google.android.gms` and
`com.google.android.gsf`; fill `docs/004-lab-selfcheck.md` —
**done via `005` US6** (2026-09-15)
## Phase 4: User Story 2 — Patched Phonesky (P1)
**Goal**: priv-app Phonesky with XMLs; Play launches
**Independent test**: `pm path com.android.vending`; app opens
- [x] T011 [US2] Confirm privapp-permissions XMLs installed on device
(`/etc/permissions` or equivalent) — **covered by `005` packaging**
- [x] T012 [US2] Launch Play Store on LAB; record result — **pass after
`005` spoof + exact-alarm** (was blocked → `005`, now closed)
- [x] T013 [US2] Confirm Git/meta repo contains **no** Phonesky APK bytes
## Phase 5: User Story 3 — No FakeStore (P2)
- [x] T014 [US3] `pm list packages` shows no FakeStore; document in self-check
— **`docs/004-lab-selfcheck.md` 2026-09-15**
## Phase 6: User Story 4 — Reproducible pipeline (P2)
- [x] T015 [US4] Finalize runbook with exact copy commands from AXP archive +
microG download pins — **`docs/004-phonesky-runbook.md` + `005` runbook**
- [x] T016 [US4] Document FR-010 signing strategy (project JKS under
`/srv/rom/secrets/phonesky-signing/`; GmsCore stays PRESIGNED)
## Phase 7: Polish
- [x] T017 Collect artifacts: zip SHA-256, `VERSIONS.txt` copies, Self-Check
evidence → **`005` artifacts `20260915-bacon` + `20260915-us6-validation`**
- [x] T018 Update `docs/axp-pro-parity.md` rows microG/Phonesky status —
**done in `005` T027**
- [x] T019 Handoff note: spoofing done in `005`; next `006` banking rootless;
no Magisk/KSU in this build
## Dependencies
- T001–T003 before staging
- T004–T007 before build
- T008–T010 (US1) before claiming microG done
- T011–T013 (US2) can overlap validation with US1 after same flash
- T014 after flash
- T015–T016 can refine docs in parallel with builds
- T017–T019 after validation
## MVP
T001–T012: packages on LAB + Self-Check notes + Play launch attempt.
## Parallel opportunities
- T002/T003 with T001
- T004/T005 in parallel
- Doc polish T015/T016 while T008 builds
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff. Show more