From purlin
Reverse-engineers source code into structured 3-section specs (What it does, Rules, Proof) via parallel exploration and interactive taxonomy review.
How this skill is triggered — by the user, by Claude, or both
Slash command
/purlin:spec-from-codeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Scan an existing codebase and generate specs in 3-section format (`## What it does`, `## Rules`, `## Proof`). Uses parallel exploration, interactive taxonomy review, and dependency-ordered generation with durable state for cross-session continuity.
Scan an existing codebase and generate specs in 3-section format (## What it does, ## Rules, ## Proof). Uses parallel exploration, interactive taxonomy review, and dependency-ordered generation with durable state for cross-session continuity.
purlin:spec-from-code [directory] Scan a directory (default: src/ or lib/ or .)
purlin:spec-from-code --resume Resume from last incomplete phase
Before starting, check for .purlin/cache/sfc_state.json.
{"phases": [{"name": "...", "status": "complete"|"pending"}], ...}). If malformed or missing required fields, warn the user and offer to start fresh. If valid, resume from the last incomplete phase. Skip phases whose status is "complete". Do not re-ask questions whose answers are preserved in prior artifacts (sfc_inventory.md, sfc_taxonomy.md).List the project's top-level directories (via ls). Ask the user (via AskUserQuestion) which directories to scan — offer the ones that look like source code as defaults. Everything not selected is automatically excluded. In the question, note which directories you will skip and why (e.g., "Skipping docs/ (documentation), templates/ (scaffolding), .purlin/ (runtime)"). Base the skip list on what actually exists in the project, not a hardcoded list.
Create .purlin/cache/sfc_state.json:
{
"phase": 1,
"status": "in_progress",
"started_at": "<ISO 8601>",
"directories": { "include": [] },
"completed_categories": []
}
Existing spec detection: Scan for specs that can be used as migration context. Check two locations:
a) Legacy features/ directory: If features/ exists at the project root:
.md files recursively (excluding .impl.md and .discoveries.md companion files from the main spec list).impl.md — deviations table, architecture details, test quality audit data.discoveries.md — bug entries (resolved and open), user testing observations, Figma/design referencesb) Non-compliant specs in specs/: Glob specs/**/*.md and read each file. A spec is non-compliant if any of the following are true:
## Rules sectionRULE-N: format)## Proof section> Description: metadataFor each non-compliant spec, extract: feature name, category, existing rules (even if unnumbered), existing proofs, description, and any metadata fields already present.
Compliant specs (with numbered rules, proofs, and proper sections) are left untouched — they are not migration candidates.
Save all migration candidates to .purlin/cache/sfc_existing.md with per-feature entries: name, source location (features/ or specs/), original content summary, and list of compliance issues.
Print summary:
Found N specs to migrate: X from features/, Y non-compliant in specs/.No existing specs found. Generating from code.Launch up to 3 Explore sub-agents in parallel (Agent tool, subagent_type: Explore):
Agent A (Structure): "Scan the following directories for: directory tree structure, entry points (main/index files), route definitions, CLI entry points, config files, and file types present. Directories: <include>. Exclude: <exclude>. Return a structured summary."
Agent B (Domain): "Analyze the following directories for: frameworks used, domain concepts and terminology, tech stack (languages, key dependencies from package manifests), module boundaries, and public API surfaces. Also identify test characteristics for each module: does it require database setup, network calls, external APIs, browser automation, or manual human judgment? Flag modules that would need integration, e2e, or manual test tiers. Directories: <include>. Exclude: <exclude>. Return a structured summary."
Agent C (Comments): "Scan the following directories for: significant code comments (TODO, FIXME, HACK, architectural decision comments), module-level docstrings, and inline documentation. Directories: <include>. Exclude: <exclude>. Return a structured summary with file locations."
Synthesize all sub-agent results into .purlin/cache/sfc_inventory.md:
e2e_capable flag: true only if an e2e-capable test runner is detectable — an e2e framework (Playwright, Cypress, Puppeteer, WebdriverIO, or similar) appears in the package manifest, or an e2e config file (playwright.config.*, cypress.config.*, etc.) exists. Record the detected runner name (or none). This drives the @e2e warning in Phase 3 step 12 and the Phase 4 summary.Generate environment anchor (mandatory): Extract project-level environment data and write specs/_anchors/project_environment.md. This anchor captures what's needed to compile, run, and configure the project — information that no individual feature spec carries.
Extract from:
package.json (engines field, main framework), go.mod, pyproject.toml, Cargo.toml, Gemfile, etc.package-lock.json, yarn.lock, poetry.lock, go.sum) for pinned versions of direct dependencies. Don't list every transitive dep — list the top-level deps that appear in import statements.next.config.js, webpack.config.js, tsconfig.json, Makefile, CMakeLists.txt, Dockerfile, etc. Capture the build command and key overrides (output dir, asset prefix, compilation targets).process.env., import.meta.env., os.environ, os.Getenv, System.getenv, ENV[. Collect every env var name. Group into: required (app fails without), optional (has fallback), and secret (API keys, tokens — note the name but not the value).Write the anchor:
# Anchor: project_environment
> Description: Runtime, dependencies, build config, and environment variable inventory.
> Global: true
> Scope: package.json, next.config.js, .env*
## Rules
- RULE-1: Runtime is <language> <version> with <framework> <version>
- RULE-2: Key dependencies: <name>@<version>, <name>@<version>, ...
- RULE-3: Build command: <command>; key config: <asset prefix, output mode, etc.>
- RULE-4: Required env vars: <list with descriptions>
- RULE-5: Dev/prod split: <which env vars differ between environments>
## Proof
- PROOF-1 (RULE-1): Read package.json engines and main framework version; verify match
- PROOF-2 (RULE-2): Read lock file; verify listed dependency versions match
- PROOF-3 (RULE-3): Read build config; verify build command and key overrides
- PROOF-4 (RULE-4): Grep source for env var usage; verify all listed vars are present
- PROOF-5 (RULE-5): Read .env files or env var references; verify dev/prod differences documented
Present the environment anchor for review. Commit: spec(sfc): create anchor project_environment
Update state: phase: 1, status: "complete".
Commit per references/commit_conventions.md: chore(sfc): codebase survey complete (Phase 1)
Read .purlin/cache/sfc_inventory.md.
Check for existing specs: If specs already exist (glob specs/**/*.md), read them to extract existing category names and naming conventions. The proposed taxonomy MUST reuse existing category names where applicable. Only propose new categories when no existing one fits.
Check for migration candidates: If .purlin/cache/sfc_existing.md exists (created in Phase 1), read it. Existing specs (from features/ or non-compliant specs/) are the primary seed for the taxonomy — use their category names and feature names as starting points. When presenting the taxonomy, annotate each feature as (migrating) if it has an existing spec to migrate, or (new) if discovered only from code. This lets the user see what's being preserved vs. what's net-new.
Propose a category taxonomy grouping feature candidates into logical categories. Follow the categorization rules in references/spec_quality_guide.md ("Spec Categories"):
hooks/, mcp/, proof/)schema/references/, skills/, agents/) → instructions/integration/Explain this categorization to the user when presenting the taxonomy. For each category, list: name, feature count, and per-feature name + one-line description.
Present categories in batches of 2–3 via AskUserQuestion. For each batch, show the proposed categories and end with the approval block:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚡ REVIEW CATEGORIES — Does this grouping look right?
[y] Approve these categories
[rename] Rename a category
[merge] Merge two categories
[split] Split a category
[add] Add a missed feature
[remove] Remove a false positive
Waiting for your response...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Do NOT proceed to the next batch without an explicit response.
Near-duplicate detection: After the taxonomy is drafted but before presenting anchors, compare proposed features within each category for rule similarity. Two features are near-duplicates when they would have substantially the same behavioral constraints (same rules, different implementations — e.g., three proof plugins that all do "parse markers, emit JSON, feature-scoped overwrite"). For each cluster of 2+ near-duplicates:
AskUserQuestion: "These N features share similar behavior: <names>. Consolidate into one spec with per-implementation rules, or keep separate?"Single-feature category check: Scan the proposed taxonomy for categories containing exactly one feature. A category folder must never hold a single spec. For each single-feature category:
AskUserQuestion: "Category <name> would contain only <feature>. Merge into <closest category>, or keep it standalone?" If kept standalone, plan the spec at specs/<name>.md directly — do NOT create a folder for it. (Specs at the specs/ root display under "other" in the dashboard.)Detect anchor candidates from cross-cutting concerns. Use the following heuristics per anchor type to actively search for candidates — do not rely on passive observation alone:
| Prefix | Domain | Detection heuristics |
|---|---|---|
api_ | API contracts, REST conventions | Shared route patterns, middleware chains, response envelope formats, error response shapes, pagination conventions. Look for: express Router, Flask blueprints, API versioning patterns |
security_ | Auth, access control, secrets | Auth middleware, password hashing, token validation, input sanitization, CORS config, rate limiting. Look for: bcrypt, JWT, helmet, csrf, rate-limit imports |
design_ | Visual standards, layout | Shared UI component libraries, CSS token files, theme configs, layout patterns. Look for: styled-components, tailwind config, design token files, shared component directories |
schema_ | Data models, validation | Database models, ORM definitions, migration files, validation schemas, shared types. Look for: sequelize/prisma/sqlalchemy models, zod/joi schemas, TypeScript interfaces in shared dirs |
platform_ | Platform constraints, browser support | Browser compat configs, polyfills, platform-specific code paths, accessibility helpers. Look for: browserslist, babel config, a11y utilities |
brand_ | Voice, naming, identity | Copy constants, i18n files, terminology glossaries, tone-of-voice docs. Look for: locales/, i18n imports, string constant files |
prodbrief_ | User stories, UX requirements | User flow definitions, feature flags, A/B test configs, analytics event schemas. Look for: feature flag configs, analytics track calls, user journey comments |
legal_ | Privacy, data handling, compliance | Cookie consent, privacy policy references, data retention configs, GDPR helpers. Look for: consent managers, data deletion utilities, PII handling |
API surface anchor (mandatory when API calls detected): If Phase 1 exploration found HTTP client usage (fetch, axios, http.get, requests, net/http, etc.), generate an api_surface anchor listing every external endpoint the codebase calls. For each endpoint, capture: HTTP method, full path (including any base path prefix), and parameter shapes (query params, body fields). Trace from the HTTP call sites back to the URL construction to capture the full path — don't just capture the relative path passed to the client.
# Anchor: api_surface
> Description: All external API endpoints with methods, paths, and parameter shapes.
> Global: true
## Rules
- RULE-1: Base path prefix is /EdgeMobileService/EdgeService.svc/json/
- RULE-2: GetAnalysisDisplay — GET — params: {analysisId, reportType, isClient, ...}
- RULE-3: SaveLoanProductBenefit — POST — body: {analysisId, loanProductId, benefitTitle, benefitSubmessage}
Domain schema anchors (mandatory when shared types detected): If Phase 1 found shared type definitions (TypeScript interfaces, Python dataclasses, Go structs, SQL schemas) consumed by 3+ features, generate a schema_ anchor for each major domain entity. Rules must include the critical field names — the fields that appear in transformations, display logic, or conditional gates across features. Don't list every field; list the ones that would cause wrong behavior if an engineer used the wrong name.
# Anchor: schema_mortgage_report
> Description: Critical field names in the MortgageReport API response.
## Rules
- RULE-1: Contact info is at response.contact (lowercase), not AnalysisContact
- RULE-2: User info is at response.user (lowercase), not User
- RULE-3: Loan product name is LoanProduct.Name, not ProductName
- RULE-4: Monthly payment is LoanProduct.Piti, not TotalMonthlyPayment
- RULE-5: 5-year cost is LoanProduct.FiveYrCost, not GraphShort
Architecture choices should be anchors. If the codebase uses a specific pattern consistently across multiple features (middleware auth, write-through caching, event-driven architecture), that pattern should become an anchor — not be buried in individual feature specs. After detecting candidates, group them: "These N features all use <pattern> → propose anchor: <prefix>_<name>." Present the grouping evidence to the user for confirmation.
Security anchor detection (mandatory): In addition to the heuristic scan above, specifically grep the scanned directories for dangerous patterns:
eval(, exec( — arbitrary code executionos.system( — unquoted shell executionsubprocess calls with shell=True — shell injection vectorpassword, secret, api_key, token = "...")Then:
security_ anchor with FORBIDDEN rules as negative assertions verifying these patterns don't exist in unsafe contexts.security_ anchor anyway (e.g., security_no_dangerous_patterns) with rules confirming the codebase is clean — "No eval/exec calls", "No subprocess with shell=True", etc. Proving the absence of dangerous patterns is itself a valuable assertion.The security anchor MUST always be proposed. Proofs should be grep-based negative assertions (e.g., grep -r "eval(" scripts/ returns zero matches).
Present proposed anchors and ask for approval:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚡ REVIEW ANCHORS — <N> cross-cutting constraints detected
[y] Approve all anchors
[rename] Rename an anchor
[remove] Remove an anchor
[add] Add a missing anchor
Waiting for your response...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Use AskUserQuestion to pause. Do NOT proceed without an explicit response.
Security anchor gate (mandatory, not skippable): Before proceeding to Phase 3, verify that at least one security_ prefixed anchor exists in the confirmed taxonomy. If none was confirmed:
eval(, exec(, os.system(, shell=True, hardcoded credentials)security_no_dangerous_patterns with rules confirming absencesecurity_<name> with FORBIDDEN rulesAskUserQuestionWrite the validated taxonomy to .purlin/cache/sfc_taxonomy.md:
Update state: phase: 2, status: "complete".
Commit per references/commit_conventions.md: chore(sfc): taxonomy review complete (Phase 2)
Resume logic: If resuming Phase 3, read completed_categories from the state file. Skip those categories. Continue with the first incomplete category.
For each approved anchor from the taxonomy:
specs/<category>/<prefix_name>.md using 3-section format:# Anchor: <prefix_name>
> Description: <What cross-cutting concern this anchor defines>
> Scope: <file patterns this anchor governs>
## What it does
<One paragraph: what cross-cutting concern this anchor defines.>
## Rules
- RULE-1: <Constraint that applies to all features requiring this anchor>
- RULE-2: <Another constraint>
## Proof
- PROOF-1 (RULE-1): <How to verify compliance>
- PROOF-2 (RULE-2): <How to verify compliance>
references/commit_conventions.md: spec(sfc): create anchor <name>Process categories in dependency order: categories with fewer anchor dependencies first.
For each category:
Deep code reading: If the category spans 5+ source files, launch an Explore sub-agent (Agent tool, subagent_type: Explore) to read the relevant source. For smaller categories, read files directly.
Validate references before writing each spec:
Scope validation: Before writing > Scope:, verify each file path exists on disk. If a file was detected in Phase 1 exploration but has since been deleted or moved, exclude it from the Scope line. Do not write broken scope references.
Requires validation (blocking): Before writing > Requires:, glob specs/**/<name>.md for EACH reference. A reference is valid only if it (a) already exists on disk from a prior category or anchor generation, or (b) is listed in the taxonomy and queued for generation in a later category. If a reference would be broken (neither exists nor queued), DO NOT write the spec with the broken reference — remove it from > Requires: and print: Removed > Requires: <name> — spec not found. Create it first with purlin:spec <name>, then add the reference back.
Scope overlap suggestions: After validating references, scan all existing anchors (all specs in specs/_anchors/). If an anchor's > Scope: patterns overlap with this feature's scope but the anchor is not in > Requires:, suggest it:
Suggested > Requires: based on file overlap:
api_rest_conventions — Scope overlaps with src/api/
Add to > Requires:? [y/n]
Global anchors (with > Global: true) are auto-applied and don't need > Requires: — note them for the user's awareness.
Existing spec migration (per feature): Before generating a spec, check if this feature has a migration candidate in .purlin/cache/sfc_existing.md (matched by name, or by file scope overlap if names differ). If one exists:
From features/ (legacy format):
features/<category>/<name>.md file in full## What it doesRead ALL companion files:
.impl.md companion — read in full. Extract:
## Implementation Notes.discoveries.md companion — read in full. Extract:
[BUG] entries with status RESOLVED) — each becomes a RULE-N protecting against regression. E.g., [BUG] M12: info bar overlaps disclaimer on mobile → RULE-N: Info bar does not overlap disclaimer on viewports below 768px(deferred). The bug description becomes the rule, and the observed-vs-expected detail goes into a comment or Implementation Notes.> Visual-Reference: metadata or @manual proof referencesFrom specs/ (non-compliant format):
specs/<category>/<name>.md file in full> Scope:, > Stack:, > Requires:)> Description:, number unnumbered rules, add missing ## Proof section, convert any Given/When/Then scenarios to Rules/Proof formatFor both sources:
<!-- Migrated by purlin:spec-from-code. Review and refine. --> instead of the standard generated headerIf no migration candidate exists, generate from code alone (standard behavior).
Data contract extraction (mandatory for ALL features): For every feature, trace data across system boundaries and capture the contracts that an engineer would get wrong in a rebuild. This is organized by the five contract categories from references/spec_quality_guide.md ("Coverage dimensions"). Apply all five to every feature — not just UI.
a) Inbound contracts — what data enters, in what shape:
Trace every external data source the feature consumes. Capture the exact field names — this is the #1 rebuild risk across all codebases.
What to trace:
user.LogoFileName not "logo field")NEXT_PUBLIC_API_URL, process.env.DATABASE_URL)Extraction depth — env vars (mandatory): Grep the feature's scope files for process.env., import.meta.env., os.environ[, os.Getenv(, System.getenv(, ENV[. Every env var the feature reads becomes a rule or references the project_environment anchor. If the feature reads 3+ env vars, verify they're all listed in the environment anchor.
Extraction depth — schema cross-reference (mandatory): If the feature consumes a typed API response or shared data structure, check whether a schema_ anchor exists with field-level rules for that type. If not, flag: "Schema anchor missing field-level rules for <TypeName> — feature uses fields <list> that aren't documented." The feature spec's inbound rules must use the same field names as the schema anchor.
Write rules specifying what the feature reads and from where:
formatImageUrl(user.LogoFileName) via GET /EdgeMobileService/EdgeService.svc/json/GetAnalysisGuidDisplay"user.FirstName + ' ' + user.LastName"DATABASE_URL from environment; falls back to localhost:5432 if unset"b) Outbound contracts — what data leaves, in what shape:
Find every place the feature emits data to an external system. Capture event names, payload shapes, and trigger conditions.
What to trace:
Write rules specifying what gets sent, when, and in what shape:
report_viewed with params {reportId, reportType, contactId} when report page loads"{items, total, paymentToken} on checkout submit"Extraction depth — event payloads (mandatory): For each analytics or event call, do NOT stop at the event name. Follow the call into the tracking/emit function and extract the full parameter object. If the function merges default params (e.g., {...defaultParams, ...eventParams}), capture both sets. The rule must include the complete payload shape, not just the event name.
c) Transformation rules — what logic converts between inbound and outbound:
Identify every place data changes shape between input and output. Capture the exact mapping, formula, or logic.
What to trace:
Write rules specifying the transformation, not the mechanism:
formatImageUrl prepends CDN base URL to user.LogoFileName; returns empty string if null"LoanProduct.IsHidden === false, sorted by LoanProduct.SortOrder"d) State transitions and initialization ordering:
Identify features with distinct states, transition rules, or bootstrap dependencies. Not every feature has these — skip if the feature is stateless and has no init ordering constraints.
What to trace:
await init() chains, module-level setup calls, useEffect dependency ordering, DOMContentLoaded / onMount sequences. If service B reads from service A, A must initialize first.Write rules specifying valid states, transitions, and init order:
e) Access contracts — who can see or do what:
Identify every gate that controls visibility or behavior based on user identity, permissions, flags, or modes.
What to trace:
Write rules specifying what each segment sees or can do:
lo=true URL hash param) shows editable benefit fields and save button"Visual reference preservation: If the code or old specs contain references to Figma files, design mockups, or screenshots:
> Visual-Reference: figma://fileKey/nodeId> Visual-Reference: ./designs/component.png@manual proofs for visual fidelity: "Visual layout matches design spec @manual"Draft and evaluate rules (mandatory): Before writing the spec file, draft all candidate rules as full RULE-N: lines and evaluate each against the rebuild test. This step applies to ALL features, not just UI.
Draft: Combine candidate rules from standard extraction (step 1's code reading) and data contract extraction (step 4). Write each as a RULE-N: line.
Evaluate each rule:
Result: A final rule list where every rule passes all three tests. This list goes into the spec file in step 6.
For each feature in the category, write specs/<category>/<name>.md:
<!-- Generated by purlin:spec-from-code. Review and refine. -->
# Feature: <name>
> Description: <One-line summary of what this feature does>
> Requires: <anchor_name> (if applicable)
> Scope: <source files>
> Stack: <language>/<framework>, <key libraries>, <patterns>
## What it does
<One paragraph describing the feature.>
## Rules
- RULE-1: <Behavioral constraint extracted from code>
- RULE-2: <Another constraint>
## Proof
- PROOF-1 (RULE-1): <Observable assertion>
- PROOF-2 (RULE-2): <Observable assertion>
## Implementation Notes
Extracted from source (include when architecturally significant):
- Design pattern: <description> (file:line)
- Caching strategy: <description> (file:line)
- Concurrency model: <description> (file:line)
- Data flow: <description> (file:line)
- Key tradeoff: <description> (file:line)
- TODO/Known issue: <description> (file:line)
> Stack: metadata: Populate from the actual imports/dependencies in the feature's source files, not the project-level tech stack. Phase 1 Agent B detects the project stack; Phase 3 narrows it per-feature by reading source imports.
Examples:
> Stack: python/stdlib, subprocess (list-only), json, hashlib> Stack: node/express, axios, redis (cache), JWT auth> Stack: shell/bash, jq, curlTier review pass (mandatory): Review every proof description just written for this category. For each proof, apply the tier heuristics from references/spec_quality_guide.md ("Tier Tags on Proofs"):
@integration@e2e@manualDo NOT present specs to the user with untagged proofs that clearly need a tier. When in doubt, tag @integration.
Inverse check (mandatory): After assigning tier tags, verify each description matches its tag per references/spec_quality_guide.md ("E2E proof descriptions"). Every @e2e proof must read as an observable flow — arrange → act → observe through the real running app — and must not name a source file or internal function. Rewrite any proof of the form "Assert <file> does X" or "Assert <internalFn> uses Y" as a boundary observation (the outbound network request, the rendered output, the storage state after a real flow). If a proof tagged @e2e could pass without launching the app, either rewrite it as a flow or retag it to the tier it actually exercises.
No test-only specs: Never generate a spec whose purpose is to be a container for tests (e.g., e2e_feature_scoped_overwrite, e2e_audit_cache_pipeline). If integration or e2e tests validate a feature's behavior, those tests should prove rules in that feature's spec — not in a separate spec. When code analysis reveals e2e test files, map their assertions to the feature spec they exercise and add rules there.
Rebuild-risk filter and coverage check (mandatory): Before presenting specs, apply two filters:
Filter 1 — Drop implementation noise: Review every rule just written. For each rule, ask: "Does this describe what the feature must do, or how the code does it?" Remove rules that specify:
::before pseudo-element, rx={h/2} for SVG)useMediaQuery")--surface-primary") — instead say what the behavior is ("follows the active theme")Filter 2 — Verify contract coverage: Verify the spec covers the applicable contract boundaries from references/spec_quality_guide.md ("Coverage dimensions"). The spec MUST have rules for each boundary the feature touches:
Filter 3 — Tier by rebuild risk: Review each rule against the rebuild risk tiers in references/spec_quality_guide.md ("Rebuild risk tiers"). Every rule should pass the test: "If an engineer rebuilt from only this spec, would they get this wrong without this rule?" If the answer is no — the rule is noise, not signal. Cut it.
Rule quality review (mandatory): For each spec just written, apply the purlin:spec --review logic internally: evaluate every rule against the rebuild/behavior/overlap tests. Fix any IMPLEMENTATION or NOISE rules before presenting to the user — don't defer quality problems to review time.
Validate generated specs (mandatory before user review): Read back every spec just written for this category. For each spec, verify:
## What it does contains at least one full sentence (not empty, not just whitespace)## Rules contains at least one RULE-N: line## Proof contains at least one PROOF-N (RULE-N): linereferences/spec_quality_guide.md ("FORBIDDEN Grep Precision").references/spec_quality_guide.md ("Edge Case Proof Specificity").references/audit_criteria.md ("E2E Proof Tier Integrity" — tier mismatch, source-constant assertion). Rewrite them as boundary observations per references/spec_quality_guide.md ("E2E proof descriptions") before presenting to the user.If any section is empty or missing content:
> Scope: line@e2e tag AND the Phase 1 inventory's e2e_capable flag is false, include the warning line shown below (omit it otherwise):━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚡ REVIEW SPECS — <category_name> (<N> specs generated)
⚠ <K> proofs tagged @e2e but no e2e runner detected — they cannot execute
until one is wired in (Playwright, Cypress, an MCP-driven browser, etc.).
See references/supported_frameworks.md ("End-to-end (browser) proofs").
[y] Approve and commit this category
[n] Discard and regenerate
[edit] I want to change specific specs
Waiting for your response...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Use AskUserQuestion to pause. Do NOT auto-approve or proceed without an explicit response.
Commit the category batch per references/commit_conventions.md: spec(sfc): generate <category_name> specs
Per-category sync check: After committing, call sync_status and check the output for the specs just generated. If sync_status reports any warnings (unnumbered rules, missing ## Rules section, structural problems), fix them immediately — edit the spec, re-commit — before moving to the next category. Do not accumulate broken specs across categories.
Update state: add category name to completed_categories.
Call sync_status to show the initial coverage state.
Migration cleanup (if applicable):
If features/ was detected and specs were migrated from it:
AskUserQuestion: Migration complete. Remove old features/ directory? The old specs have been migrated to specs/. [y/n]features/ and any companion files. Also delete old artifacts if present: pl-* symlinks, *.sh scripts at root.features/ in place. Print: Keeping features/ — you can remove it manually when ready: rm -rf features/Non-compliant specs in specs/ are overwritten in place — no cleanup needed.
Summarize results:
Generated N specs in M categories.
Anchor specs: K
Migrated: L (X from features/, Y updated in specs/)
Features with implementation notes: J
Next:
purlin:status — see what needs tests
purlin:unit-test — write proof-marked tests
purlin:spec <name> — refine a generated spec
If any generated proofs are tagged @e2e and the Phase 1 e2e_capable flag is false, append to the summary:
⚠ <K> proofs tagged @e2e but no e2e runner detected — they cannot execute until
one is wired in. See references/supported_frameworks.md ("End-to-end (browser) proofs").
Delete temporary files:
.purlin/cache/sfc_state.json.purlin/cache/sfc_inventory.md.purlin/cache/sfc_taxonomy.md.purlin/cache/sfc_existing.md (if created)Commit cleanup per references/commit_conventions.md: chore(sfc): finalize spec-from-code (Phase 4)
For quality guidelines on writing rules, proof descriptions, tier assignment, anchor detection, FORBIDDEN patterns, > Stack: metadata, > Requires: and > Scope: guidance, see references/spec_quality_guide.md.
For audit criteria (what makes a proof STRONG vs WEAK vs HOLLOW), see references/audit_criteria.md. Write proof descriptions that will pass audit the first time — avoid patterns listed as HOLLOW (mocking the thing being tested, asserting existence instead of behavior, no assertions).
Additional spec-from-code-specific guidelines:
(assumed) tag. Rules extracted from code are observed behavior, not assumptions. The code IS the specific value — timeout=500 is a fact, not an assumption.@e2e proof descriptions must read as arrange → act → observe through the real running app and must not name source files or internal functions — see references/spec_quality_guide.md ("E2E proof descriptions"). When no e2e runner exists in the project, surface the warning (step 12 / Phase 4) rather than silently emitting unrunnable proofs.<!-- Generated by purlin:spec-from-code. Review and refine. --> at the top. For migrated specs, use <!-- Migrated by purlin:spec-from-code. Review and refine. --> instead.## Implementation Notes — never in ## Rules. They inform a rebuilding engineer but are not testable behavioral constraints. A spec with 10 rules and 5 impl notes is better than a spec with 15 rules where 5 are really impl notes.@integration unless the specific proof can be unit-tested in isolation.references/spec_quality_guide.md ("Coverage dimensions"), but every rule must pass the rebuild-risk test: "Would an engineer get this wrong without this rule?" CSS pixel values, library choices, and visual polish are not rules.features/, read .impl.md and .discoveries.md in full. Extract behavioral deviations and bug regressions as rules. Architecture decisions go to ## Implementation Notes. Stale bugs and resolved cosmetic issues are not rules — they belong in git history.npx claudepluginhub rlabarca/purlin --plugin purlinProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.