product-init
Reverse-engineer Product Decision Records (PDRs) from an existing codebase and documentation using multi-agent feature-area analysis (brownfield). Use when documenting product decisions inferred from an already-built product.
Install
npx skills add https://github.com/tikalk/adlc-team-skills/tree/main/skills/product/product-init
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tikalk-adlc-team-skills@llmmart
git clone https://github.com/tikalk/adlc-team-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tikalk/adlc-team-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
product-init
What this skill does
Reverse-engineers product decisions from an existing product using a three-phase analysis pipeline:
- Discovery Agent: Scans each feature-area for raw product signals (code, docs, pricing)
- Pattern Agent: Classifies signals into PDR categories, scores strategic importance
- Synthesis Agent: Cross-feature-area analysis, flags inconsistencies
Output: Individual PDR-{NNN}.md files (status Discovered) in .adlc/drafts/pdr/ with an auto-generated pdr.md index.
When to use
- Existing product with no formal PDRs
- Brownfield codebase needs product documentation
- Team onboarding — walking through product rationale
- Post-acquisition or inherited codebase
When NOT to use
- New product (use
/product-specifyinstead) - Minor PDR updates (use
/product-clarifyinstead)
Execution Steps
Phase 0: Environment Setup
Run setup script to resolve paths and detect feature-areas:
sh: scripts/bash/setup-product-init.sh [--json]
ps: scripts/powershell/setup-product-init.ps1
Setup output (JSON):
{
"REPO_ROOT": "/path/to/project",
"PDR_DRAFTS_DIR": "/path/to/project/.adlc/drafts/pdr",
"PRD_FILE": "/path/to/project/PRD.md",
"feature_areas": ["core", "business", "growth"],
"next_pdr": "001"
}
Create directories:
mkdir -p "{REPO_ROOT}/.adlc/drafts/pdr"
mkdir -p "{REPO_ROOT}/.adlc/product"
Phase 1: Feature-Area Detection
Detect feature-areas from three sources:
| Source | Detection Pattern |
|---|---|
| Directory Structure | src/auth/, features/payments/, modules/ |
| Documentation | README sections, existing PRD, ROADMAP |
| Pricing Tiers | Starter/Pro/Enterprise feature mapping |
Present detected areas:
## Detected Feature-Areas
| # | Feature-Area | Sources | Evidence |
|---|--------------|---------|----------|
| 1 | **Core** | Directory + Docs | src/users/, README "Core Features" |
| 2 | **Business** | Directory + Pricing | src/billing/, pricing.md tiers |
Reply: Y to confirm, n for monolithic, or suggest changes.
Threshold Logic:
- ≤3 areas: Auto-approve
- 4-6 areas: Confirm with user
-
6 areas: Suggest grouping
Phase 2: Discovery Agent (Per Feature-Area)
For each feature-area in order:
- Scan directory for monetization signals, user flows, features
- Analyze documentation (README, PRD, pricing)
- Identify pricing tier mappings
- Document evidence
Progress report per area:
Discovery Agent: business feature-area
├── Directory signals: 4
├── Documentation signals: 2
├── Pricing signals: 1
└── Status: ✓ Completed
Phase 3: Pattern Agent (Per Feature-Area)
For each feature-area:
- Categorize signals into PDR categories (Problem, Persona, Scope, Metric, etc.)
- Score strategic importance (0.0-1.0)
- Check for duplicates against existing PDRs
- Identify cross-area candidates
Phase 4: Synthesis Agent (Cross-Feature-Area)
1. Cross-Area Pattern Detection:
{
"pattern_id": "P001",
"pattern_name": "Admin Persona",
"feature_area_presence": {"core": true, "business": true},
"is_cross_area": true
}
2. Inconsistency Detection:
- Priority conflicts
- Duplicate problems
- Inconsistent metrics
- Generate flags (embedded in affected PDRs)
3. PDR Generation: High-strategic patterns (>0.7) and cross-area patterns (≥2 areas)
Phase 5: Write Individual PDR Files
For each discovered PDR:
- Assign ID: PDR- using next available number
- Write file:
{REPO_ROOT}/.adlc/drafts/pdr/PDR-{NNN}.md
PDR file format (individual file, YAML frontmatter — use the shared template):
Use the template at {REPO_ROOT}/.agents/skills/product/../templates/pdr-template.md as the canonical PDR format. The template includes YAML frontmatter (status, date, owner, category, feature-area, title) as the single source of truth for index generation, plus the standard PDR body sections.
---
status: discovered
date: YYYY-MM-DD
owner: [Inferred from codebase/authors]
category: [Problem | Persona | Scope | Metric | Prioritization | Business Model | Feature | NFR]
feature-area: [core | business | growth | ...]
title: [Decision Title]
---
# PDR-001: [Decision Title]
### Cross-Feature-Area Metadata
- **Appears in**: [business, growth]
- **Cross-area count**: 2
- **Is cross-area pattern**: ✓
### ⚠️ Inconsistency Flags
*None* (or flag details if detected)
### Context
**Problem/Opportunity:**
[Clear description]
**Market Forces:**
- [Market factor 1]
- [Customer feedback]
### Decision
**Decision Statement:**
[Clear statement]
**Rationale:**
[Why this option]
### Consequences
#### Positive
- [Benefit 1]
#### Negative
- [Trade-off 1]
#### Risks
- [Risk with mitigation]
### Success Metrics
| Metric | Target | Measurement Method |
|--------|--------|-------------------|
| [Metric] | [Target] | [Method] |
### Alternatives Considered
#### Option A: [Alternative Name]
**Description:** [Brief description]
**Trade-offs:** [Neutral comparison]
Phase 6: Regenerate PDR Index
Generate {REPO_ROOT}/.adlc/drafts/pdr/pdr.md from all PDR-*.md files:
# For each PDR-*.md file, extract ID, title, status, date
# Build index table
Index format:
# Product Decision Records
## PDR Index
| ID | Feature-Area | Category | Status | Date | Owner |
|----|--------------|----------|--------|------|-------|
| PDR-001 | business | Business Model | Discovered | 2026-01-20 | [Inferred] |
| PDR-002 | core | Persona | Discovered | 2026-01-20 | [Inferred] |
---
## Cross-Feature-Area Analysis Summary
### Cross-Area Patterns
| Pattern | Feature-Areas | PDR |
|---------|---------------|-----|
| Admin Persona | core, business | PDR-002 |
### Inconsistencies Flagged
| Flag ID | Type | PDRs Affected | Severity |
|---------|------|---------------|----------|
| FLG-001 | Priority Conflict | PDR-003 | Medium |
---
*Individual PDR files are in this directory (PDR-*.md)*
Phase 7: Output Summary
## Product Init Complete ✓
### Execution Stats
- **Feature-areas analyzed**: 3
- **Discovery Agent runs**: 3
- **Pattern Agent runs**: 3
- **Synthesis Agent runs**: 1
### PDRs Generated
| Category | Count | Cross-Area |
|----------|-------|------------|
| Business Model | 2 | ✓ |
| Persona | 3 | ✓ |
| Problem | 2 | |
| Prioritization | 1 | |
| **With Inconsistency Flags** | **2** | |
### Next Steps
1. Review PDRs: `{REPO_ROOT}/.adlc/drafts/pdr/`
2. **Resolve inconsistencies**: Run `/product-clarify`
3. Generate PRD: Run `/product-implement`
PDR Numbering Rules
- Scan
{REPO_ROOT}/.adlc/drafts/pdr/for existingPDR-*.mdfiles - Extract numeric suffix, find maximum
- Next PDR = max + 1, zero-padded to 3 digits (PDR-001, PDR-002, ...)
- Never reuse numbers, even for deleted PDRs
State Management
State file: {REPO_ROOT}/.adlc/product/state.json
{
"version": "1.0",
"command": "product-init",
"created_at": "2026-01-20T10:00:00Z",
"phase": "completed",
"feature_areas": [
{"id": "core", "name": "Core", "progress": {"discovery": "completed", "pattern": "completed"}}
],
"pdrs_generated": 9,
"cross_area_patterns": 5,
"inconsistencies": 2
}
Configuration
PDR_DRAFTS_DIR—{REPO_ROOT}/.adlc/drafts/pdr(individual PDR files)PDR_INDEX—{REPO_ROOT}/.adlc/drafts/pdr/pdr.md(auto-generated index)PRD_FILE—{REPO_ROOT}/PRD.mdSTATE_FILE—{REPO_ROOT}/.adlc/product/state.json
12-Factor Alignment
- Factor III (Mission Definition): Discovers the "what & why" behind existing code
- Factor IX (Traceability): Every inferred decision is documented with evidence
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "The code is self-documenting." | Code shows how, not why. PDRs capture rationale that code cannot. |
| "I'll just read the README." | READMEs describe features, not decisions. PDRs capture the decision tree. |
| "Brownfield products don't need PDRs." | Every product has implicit decisions. Making them explicit prevents repeated mistakes. |
Red Flags
- Generating PDRs without codebase evidence — brownfield PDRs must be grounded in actual code/docs, not fabricated rationales.
- Skipping inconsistency flags — cross-area conflicts are real signals, not noise.
- Assigning "Proposed" status to discovered PDRs — brownfield PDRs are Discovered, not proposed.
Verification
- Setup script returns valid JSON with all paths
-
.adlc/drafts/pdr/directory exists - At least one
PDR-*.mdfile created with status "Discovered" -
pdr.mdindex auto-generated with correct table - State file written to
.adlc/product/state.json - Inconsistency flags embedded in affected PDRs (not separate files)
- Cross-feature-area analysis summary in index
- No duplicate PDR IDs
Files (adlc-team-skills)
-
scripts
-
bash
-
pdr-lib.sh 17.8 KB
#!/usr/bin/env bash # # pdr-lib.sh — Shared PDR lifecycle library for adlc-skills product-* skills. # # Mirrors the ADR-side tooling in setup-architect.sh (generate_adr_index, move_adr, # parse_fm_field, parse_fm_title) so that the PDR side has the same structural # robustness: script-driven index generation, atomic move promotion, and a # frontmatter-primary + heading-fallback parser that handles both legacy H2 # (## Status) and current H3 (### Status) metadata. # # Bundled with each product-* skill so it works standalone. # Sourced by setup-product-*.sh scripts and callable directly. # # Usage: # source pdr-lib.sh # load functions # generate_pdr_index memory # regenerate .adlc/memory/pdr/pdr.md # generate_pdr_index drafts # regenerate .adlc/drafts/pdr/pdr.md # move_pdr 020 drafts memory # atomically move PDR-020 drafts→memory # migrate_pdr_to_frontmatter .adlc/memory/pdr/PDR-001.md # one-time legacy migration # set -euo pipefail # ============================================================================ # Project root resolution (mirror of common.sh _get_project_root) # ============================================================================ _get_project_root() { local dir dir="$(pwd)" while [ "$dir" != "/" ]; do if [ -d "$dir/.adlc" ] || [ -d "$dir/.git" ]; then echo "$dir" return 0 fi dir="$(dirname "$dir")" done pwd } # Resolve REPO_ROOT if not already set by the caller. if [ -z "${REPO_ROOT:-}" ]; then REPO_ROOT="$(_get_project_root)" fi export REPO_ROOT # ============================================================================ # YAML frontmatter parser (copied from setup-architect.sh parse_fm_field) # ============================================================================ # Parse a YAML frontmatter field from a markdown file. # Usage: parse_fm_field "file" "fieldname" # Returns the field value with quotes, inline comments, and surrounding whitespace stripped. parse_fm_field() { local file="$1" local field="$2" [ -f "$file" ] || return 0 awk -v fld="^[[:space:]]*${field}:[[:space:]]*" ' /^---[[:space:]]*$/ { fm++; next } fm == 1 && $0 ~ fld { sub(fld, "") sub(/[[:space:]]+#.*$/, "") gsub(/^["'\'']|["'\'']$/, "") gsub(/^\[|\]$/, "") sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, "") print exit } ' "$file" } # Extract the H1 title (first "# " line after frontmatter) from a markdown file. # Usage: parse_fm_title "file" parse_fm_title() { [ -f "$1" ] || return 0 awk '/^---[[:space:]]*$/ { fm++; next } fm >= 2 && /^#[[:space:]]+/ { sub(/^#[[:space:]]+/, ""); sub(/[[:space:]]+$/, ""); print; exit }' "$1" } # ============================================================================ # Heading-based fallback parser (handles legacy H2 and current H3 PDR files) # ============================================================================ # Parse a metadata field from Markdown headings (## or ###). # Looks for a heading line matching "^###+ <field>$", returns the next non-empty # non-heading line with ** bold markers stripped. # Usage: parse_pdr_heading_field "file" "Status" (case-sensitive field name) parse_pdr_heading_field() { local file="$1" local field="$2" [ -f "$file" ] || return 0 awk -v fld="$field" ' $0 ~ "^#+[[:space:]]*" fld "[[:space:]]*$" { found=1; next } found && /^#+/ { exit } found && NF > 0 { gsub(/\*\*/, "") sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, "") print exit } ' "$file" } # Extract the title from a "# PDR-NNN: <title>" or "## PDR-NNN: <title>" heading. # Strips the heading marker and "PDR-NNN:" prefix. # Usage: parse_pdr_heading_title "file" parse_pdr_heading_title() { [ -f "$1" ] || return 0 awk ' $0 ~ "^#+[[:space:]]*PDR-[0-9]+:[[:space:]]*" { sub(/^#+[[:space:]]*PDR-[0-9]+:[[:space:]]*/, "") sub(/[[:space:]]+$/, "") print exit } ' "$1" } # ============================================================================ # Combined parsers (frontmatter primary, heading fallback) # ============================================================================ # Parse a PDR metadata field: tries YAML frontmatter first, falls back to headings. # Usage: parse_pdr_field "file" "status" (frontmatter field name, lowercase hyphenated) # parse_pdr_field "file" "Status" (heading field name, Title Case) # Note: frontmatter uses lowercase hyphenated keys (status, feature-area); # headings use Title Case (Status, Feature-Area). This function tries both. parse_pdr_field() { local file="$1" local field="$2" local value="" # Try frontmatter (lowercase the field name for YAML key matching) local fm_field fm_field=$(echo "$field" | tr '[:upper:]' '[:lower:]') value=$(parse_fm_field "$file" "$fm_field") if [ -n "$value" ]; then echo "$value" return fi # Fallback: heading-based (Title Case the field name for heading matching) # Accept both "Feature-Area" and "feature-area" heading styles local title_field title_field=$(echo "$field" | awk -F'-' '{for (i=1; i<=NF; i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1' OFS='-') value=$(parse_pdr_heading_field "$file" "$title_field") if [ -n "$value" ]; then echo "$value" return fi # Try the raw field name (handles "Feature-Area" where only first letter caps) value=$(parse_pdr_heading_field "$file" "$field") if [ -n "$value" ]; then echo "$value" return fi # Empty string — caller should apply defaults echo "" } # Extract the PDR title: tries frontmatter "title" field, falls back to H1 parsing. # Usage: parse_pdr_title "file" parse_pdr_title() { [ -f "$1" ] || return 0 local value value=$(parse_fm_field "$1" "title") if [ -n "$value" ]; then echo "$value" return fi # Fallback: strip "PDR-NNN:" prefix from H1/H2 value=$(parse_pdr_heading_title "$1") if [ -n "$value" ]; then echo "$value" return fi # Last resort: bare H1 (no PDR-NNN prefix) value=$(parse_fm_title "$1") echo "$value" } # ============================================================================ # Index generation (mirrors generate_adr_index from setup-architect.sh) # ============================================================================ # Generate pdr.md index from individual PDR files. # Usage: generate_pdr_index [scope] (scope = drafts | memory; default: drafts) # Writes to: $REPO_ROOT/.adlc/{scope}/pdr/pdr.md (INSIDE the pdr/ directory) # Schema: 7 columns — ID | Feature-Area | Category | Status | Date | Owner | Title # Fails loudly on blank cells (warns to stderr; applies defaults so no row is blank). generate_pdr_index() { local scope="${1:-drafts}" local pdr_dir="$REPO_ROOT/.adlc/$scope/pdr" local index_file="$pdr_dir/pdr.md" if [ ! -d "$pdr_dir" ]; then return 0 fi local index_content="# Product Decision Records" if [ "$scope" = "memory" ]; then index_content="$index_content (Memory) > Auto-generated by /product-implement. Accepted PDRs only. > Source: .adlc/$scope/pdr/PDR-*.md" else index_content="$index_content (Drafts) > Auto-generated by /product-clarify. Proposed PDRs only. > Source: .adlc/$scope/pdr/PDR-*.md" fi index_content="$index_content ## PDR Index | ID | Feature-Area | Category | Status | Date | Owner | Title | |----|--------------|----------|--------|------|-------|-------| " # Sort PDR files numerically local f fname id title status date owner category feature_area padded_id local blank_warnings="" for f in $(ls -1 "$pdr_dir"/PDR-*.md 2>/dev/null | sort -t'-' -k2 -n); do fname=$(basename "$f") id=$(echo "$fname" | sed -E 's/PDR-([0-9]+)\.md/\1/') padded_id=$(printf "%03d" "$((10#$id))") title=$(parse_pdr_title "$f") status=$(parse_pdr_field "$f" "status") date=$(parse_pdr_field "$f" "date") owner=$(parse_pdr_field "$f" "owner") category=$(parse_pdr_field "$f" "category") feature_area=$(parse_pdr_field "$f" "feature-area") # Defaults — no silent blank cells [ -z "$status" ] && { status="Unknown"; blank_warnings="$blank_warnings\n - PDR-$padded_id: Status"; } [ -z "$date" ] && { date="YYYY-MM-DD"; blank_warnings="$blank_warnings\n - PDR-$padded_id: Date"; } [ -z "$owner" ] && { owner="Unknown"; blank_warnings="$blank_warnings\n - PDR-$padded_id: Owner"; } [ -z "$category" ] && { category="Unknown"; blank_warnings="$blank_warnings\n - PDR-$padded_id: Category"; } [ -z "$feature_area" ] && { feature_area="system"; blank_warnings="$blank_warnings\n - PDR-$padded_id: Feature-Area"; } [ -z "$title" ] && { title="PDR-$padded_id"; blank_warnings="$blank_warnings\n - PDR-$padded_id: Title"; } index_content="$index_content| PDR-$padded_id | $feature_area | $category | $status | $date | $owner | $title | " done # Write the index printf '%s\n' "$index_content" > "$index_file" # Warn on any blank cells that needed defaults if [ -n "$blank_warnings" ]; then echo "[WARN] generate_pdr_index ($scope): the following PDRs had blank metadata cells;" >&2 echo " defaults were applied. Run /product-clarify to fix the source files." >&2 printf '%b\n' "$blank_warnings" >&2 fi } # ============================================================================ # Atomic move (mirrors move_adr from setup-architect.sh) # ============================================================================ # Move a PDR from one scope to another (e.g., drafts -> memory). # Usage: move_pdr <pdr_id> [from_scope] [to_scope] # Performs an atomic mv, then regenerates both scopes' indexes. # Fails if the source file does not exist. Verifies no duplicates remain. move_pdr() { local pdr_id="$1" local from_scope="${2:-drafts}" local to_scope="${3:-memory}" local from_dir="$REPO_ROOT/.adlc/$from_scope/pdr" local to_dir="$REPO_ROOT/.adlc/$to_scope/pdr" local numeric_id numeric_id=$(echo "$pdr_id" | sed -E 's/[^0-9]//g') local padded_id padded_id=$(printf "%03d" "$((10#$numeric_id))") mkdir -p "$to_dir" if [ -f "$from_dir/PDR-$padded_id.md" ]; then mv "$from_dir/PDR-$padded_id.md" "$to_dir/PDR-$padded_id.md" else echo "[WARN] move_pdr: source file not found: $from_dir/PDR-$padded_id.md" >&2 return 1 fi # Duplicate check — the source must be gone if [ -f "$from_dir/PDR-$padded_id.md" ]; then echo "[ERROR] move_pdr: duplicate detected — PDR-$padded_id still exists in $from_scope after move" >&2 return 1 fi # Regenerate both scopes generate_pdr_index "$from_scope" generate_pdr_index "$to_scope" } # ============================================================================ # One-time migration: heading-based metadata → YAML frontmatter # ============================================================================ # Migrate a legacy PDR file (heading-based metadata) to YAML frontmatter. # Reads Status/Date/Owner/Category/Feature-Area/Title from headings, prepends # frontmatter, and preserves the body unchanged. Skips files that already have # frontmatter. Strips ** bold markers from Status. # Usage: migrate_pdr_to_frontmatter <file> migrate_pdr_to_frontmatter() { local file="$1" [ -f "$file" ] || { echo "[WARN] migrate: file not found: $file" >&2; return 1; } # Skip if already has frontmatter if head -1 "$file" | grep -q '^---[[:space:]]*$'; then return 0 fi local title status date owner category feature_area title=$(parse_pdr_heading_title "$file") status=$(parse_pdr_heading_field "$file" "Status") date=$(parse_pdr_heading_field "$file" "Date") owner=$(parse_pdr_heading_field "$file" "Owner") category=$(parse_pdr_heading_field "$file" "Category") feature_area=$(parse_pdr_heading_field "$file" "Feature-Area") # Strip ** bold markers from status status="${status//\*\*/}" # Build frontmatter local fm="--- status: ${status:-Unknown} date: ${date:-YYYY-MM-DD} owner: ${owner:-Unknown} category: ${category:-Unknown} feature-area: ${feature_area:-system} title: ${title:-Untitled} --- " # Prepend frontmatter to the original body printf '%s\n%s\n' "$fm" "$(cat "$file")" > "$file.tmp" && mv "$file.tmp" "$file" } # Migrate all PDR files in a directory. # Usage: migrate_pdr_dir <dir> migrate_pdr_dir() { local dir="$1" [ -d "$dir" ] || { echo "[WARN] migrate_pdr_dir: dir not found: $dir" >&2; return 1; } local f count=0 skipped=0 for f in "$dir"/PDR-*.md; do [ -f "$f" ] || continue if head -1 "$f" | grep -q '^---[[:space:]]*$'; then skipped=$((skipped + 1)) else migrate_pdr_to_frontmatter "$f" && count=$((count + 1)) fi done echo "[INFO] migrate_pdr_dir: migrated $count file(s), skipped $skipped (already had frontmatter)" } # ============================================================================ # Fix frontmatter: re-extract metadata from body headings and update frontmatter # ============================================================================ # Fix PDR frontmatter by re-reading heading-based metadata from the body. # For files that were migrated with incorrect/blank frontmatter (e.g., due to # a parser bug), this function re-extracts Status/Date/Owner/Category/Feature-Area/Title # from the body headings and rewrites the frontmatter block. # Usage: fix_pdr_frontmatter <file> fix_pdr_frontmatter() { local file="$1" [ -f "$file" ] || { echo "[WARN] fix_pdr_frontmatter: file not found: $file" >&2; return 1; } # Must have frontmatter to fix if ! head -1 "$file" | grep -q '^---[[:space:]]*$'; then return 0 # Not migrated yet — skip fi # Extract metadata from body headings (these are always present, even after migration) local title status date owner category feature_area title=$(parse_pdr_heading_title "$file") status=$(parse_pdr_heading_field "$file" "Status") date=$(parse_pdr_heading_field "$file" "Date") owner=$(parse_pdr_heading_field "$file" "Owner") category=$(parse_pdr_heading_field "$file" "Category") feature_area=$(parse_pdr_heading_field "$file" "Feature-Area") # Strip ** bold markers status="${status//\*\*/}" # Skip if all fields are empty (can't fix) if [ -z "$status" ] && [ -z "$date" ] && [ -z "$owner" ] && [ -z "$category" ] && [ -z "$feature_area" ] && [ -z "$title" ]; then return 0 fi # Read current frontmatter values to preserve any non-empty ones local fm_status fm_date fm_owner fm_category fm_feature_area fm_title fm_status=$(parse_fm_field "$file" "status") fm_date=$(parse_fm_field "$file" "date") fm_owner=$(parse_fm_field "$file" "owner") fm_category=$(parse_fm_field "$file" "category") fm_feature_area=$(parse_fm_field "$file" "feature-area") fm_title=$(parse_fm_field "$file" "title") # Use heading value if non-empty, else keep frontmatter value, else default [ -n "$status" ] && fm_status="$status" [ -n "$date" ] && fm_date="$date" [ -n "$owner" ] && fm_owner="$owner" [ -n "$category" ] && fm_category="$category" [ -n "$feature_area" ] && fm_feature_area="$feature_area" [ -n "$title" ] && fm_title="$title" # Defaults for any remaining blanks [ -z "$fm_status" ] && fm_status="Unknown" [ -z "$fm_date" ] && fm_date="YYYY-MM-DD" [ -z "$fm_owner" ] && fm_owner="Unknown" [ -z "$fm_category" ] && fm_category="Unknown" [ -z "$fm_feature_area" ] && fm_feature_area="system" [ -z "$fm_title" ] && fm_title="Untitled" # Build new frontmatter local new_fm="--- status: $fm_status date: $fm_date owner: $fm_owner category: $fm_category feature-area: $fm_feature_area title: $fm_title ---" # Replace the old frontmatter block (everything between the first and second `---`) # with the new frontmatter, keeping the body unchanged. local body body=$(awk ' BEGIN { fm_count = 0; printing = 0 } /^---[[:space:]]*$/ { fm_count++; if (fm_count == 2) { printing = 1; next } else { next } } fm_count >= 2 && printing { print } ' "$file") printf '%s\n\n%s\n' "$new_fm" "$body" > "$file.tmp" && mv "$file.tmp" "$file" } # Fix all PDR files in a directory. # Usage: fix_pdr_dir <dir> fix_pdr_dir() { local dir="$1" [ -d "$dir" ] || { echo "[WARN] fix_pdr_dir: dir not found: $dir" >&2; return 1; } local f count=0 for f in "$dir"/PDR-*.md; do [ -f "$f" ] || continue fix_pdr_frontmatter "$f" && count=$((count + 1)) done echo "[INFO] fix_pdr_dir: fixed $count file(s)" } # ============================================================================ # Level-agnostic Accepted counter (replaces the H3-hardcoded grep) # ============================================================================ # Count PDRs with Accepted status in a directory, handling both YAML frontmatter # and heading-based metadata (H2 or H3). Replaces setup-product-clarify.sh:14 # which hardcoded '^### Status'. # Usage: count_pdr_accepted <dir> count_pdr_accepted() { local dir="$1" [ -d "$dir" ] || return 0 local count=0 f status for f in "$dir"/PDR-*.md; do [ -f "$f" ] || continue status=$(parse_pdr_field "$f" "status") # Normalize: strip ** bold, lowercase, trim status=$(echo "$status" | sed 's/\*\*//g' | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ "$status" = "accepted" ] || [ "$status" = "completed" ]; then count=$((count + 1)) fi done echo "$count" } -
setup-product-init.sh 3.3 KB
#!/bin/bash # product-init setup script # Self-contained — resolves paths, detects feature areas, finds next PDR number set -euo pipefail JSON_MODE=false for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; esac done source "$(dirname "${BASH_SOURCE[0]}")/pdr-lib.sh" 2>/dev/null || true REPO_ROOT="${REPO_ROOT:-$(_get_project_root)}" PDR_DRAFTS_DIR="$REPO_ROOT/.adlc/drafts/pdr" PRD_FILE="$REPO_ROOT/PRD.md" mkdir -p "$PDR_DRAFTS_DIR" mkdir -p "$REPO_ROOT/.adlc/product" # Detect feature areas from codebase structure detect_feature_areas() { local areas=() local dir="$REPO_ROOT" # Check common directory patterns for pattern in src features modules apps packages; do if [[ -d "$dir/$pattern" ]]; then for sub in "$dir/$pattern"/*/; do if [[ -d "$sub" ]]; then local name name=$(basename "$sub") # Skip utility dirs if [[ "$name" != "utils" && "$name" != "common" && "$name" != "lib" && "$name" != "shared" && "$name" != "core" && "$name" != "test" && "$name" != "tests" && "$name" != "__tests__" ]]; then areas+=("$name") fi fi done fi done # Docker compose services for compose in "$dir/docker-compose.yml" "$dir/docker-compose.yaml"; do if [[ -f "$compose" ]]; then while IFS= read -r line; do if [[ "$line" =~ ^[[:space:]]*([a-zA-Z0-9_-]+):[[:space:]]*$ ]]; then local svc="${BASH_REMATCH[1]}" if [[ "$svc" != "version" && "$svc" != "services" && "$svc" != "networks" && "$svc" != "volumes" ]]; then areas+=("$svc") fi fi done < "$compose" fi done # Deduplicate local unique=() for a in "${areas[@]}"; do local found=false for u in "${unique[@]}"; do if [[ "$u" == "$a" ]]; then found=true; break; fi done if [[ "$found" == "false" ]]; then unique+=("$a"); fi done if [[ ${#unique[@]} -eq 0 ]]; then echo "Product" else printf '%s\n' "${unique[@]}" fi } # Find next PDR number next_pdr_number() { local max=0 if [[ -d "$PDR_DRAFTS_DIR" ]]; then for f in "$PDR_DRAFTS_DIR"/PDR-*.md; do if [[ -f "$f" ]]; then local num num=$(basename "$f" | sed 's/PDR-//' | sed 's/\.md//') if [[ "$num" =~ ^[0-9]+$ ]]; then ((10#$num > max)) && max=$((10#$num)) fi fi done fi printf '%03d' $((max + 1)) } FEATURE_AREAS=$(detect_feature_areas) NEXT_PDR=$(next_pdr_number) # Build feature_areas JSON array FEATURE_AREAS_JSON="[" first=true while IFS= read -r area; do if [[ "$first" == "true" ]]; then first=false else FEATURE_AREAS_JSON+="," fi id=$(echo "$area" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') FEATURE_AREAS_JSON+="{\"id\":\"$id\",\"name\":\"$area\"}" done <<< "$FEATURE_AREAS" FEATURE_AREAS_JSON+="]" if $JSON_MODE; then cat <<EOF { "REPO_ROOT": "$REPO_ROOT", "PDR_DRAFTS_DIR": "$PDR_DRAFTS_DIR", "PRD_FILE": "$PRD_FILE", "feature_areas": $FEATURE_AREAS_JSON, "next_pdr": "$NEXT_PDR", "pdr_count": $(find "$PDR_DRAFTS_DIR" -name 'PDR-*.md' 2>/dev/null | wc -l) } EOF else echo "[INFO] product-init setup" echo " REPO_ROOT: $REPO_ROOT" echo " PDR_DRAFTS_DIR: $PDR_DRAFTS_DIR" echo " Next PDR: PDR-$NEXT_PDR" echo " Feature areas:" while IFS= read -r area; do echo " - $area" done <<< "$FEATURE_AREAS" fi
-
-
powershell
-
pdr-lib.ps1 10.7 KB · in bundle
-
setup-product-init.ps1 2.9 KB · in bundle
-
-
-
SKILL.md 9 KB
--- name: product-init description: Use when documenting product decisions inferred from an already-built product (brownfield) via multi-agent feature-area analysis. disable-model-invocation: true --- # product-init ## What this skill does Reverse-engineers product decisions from an **existing product** using a **three-phase analysis pipeline**: 1. **Discovery Agent**: Scans each feature-area for raw product signals (code, docs, pricing) 2. **Pattern Agent**: Classifies signals into PDR categories, scores strategic importance 3. **Synthesis Agent**: Cross-feature-area analysis, flags inconsistencies **Output**: Individual `PDR-{NNN}.md` files (status **Discovered**) in `.adlc/drafts/pdr/` with an auto-generated `pdr.md` index. ## When to use - Existing product with no formal PDRs - Brownfield codebase needs product documentation - Team onboarding — walking through product rationale - Post-acquisition or inherited codebase ## When NOT to use - New product (use `/product-specify` instead) - Minor PDR updates (use `/product-clarify` instead) ## Execution Steps ### Phase 0: Environment Setup **Run setup script** to resolve paths and detect feature-areas: ```bash sh: scripts/bash/setup-product-init.sh [--json] ps: scripts/powershell/setup-product-init.ps1 ``` **Setup output** (JSON): ```json { "REPO_ROOT": "/path/to/project", "PDR_DRAFTS_DIR": "/path/to/project/.adlc/drafts/pdr", "PRD_FILE": "/path/to/project/PRD.md", "feature_areas": ["core", "business", "growth"], "next_pdr": "001" } ``` **Create directories**: ```bash mkdir -p "{REPO_ROOT}/.adlc/drafts/pdr" mkdir -p "{REPO_ROOT}/.adlc/product" ``` ### Phase 1: Feature-Area Detection Detect feature-areas from **three sources**: | Source | Detection Pattern | |---|---| | Directory Structure | `src/auth/`, `features/payments/`, `modules/` | | Documentation | README sections, existing PRD, ROADMAP | | Pricing Tiers | Starter/Pro/Enterprise feature mapping | **Present detected areas**: ```markdown ## Detected Feature-Areas | # | Feature-Area | Sources | Evidence | |---|--------------|---------|----------| | 1 | **Core** | Directory + Docs | src/users/, README "Core Features" | | 2 | **Business** | Directory + Pricing | src/billing/, pricing.md tiers | Reply: Y to confirm, n for monolithic, or suggest changes. ``` **Threshold Logic**: - ≤3 areas: Auto-approve - 4-6 areas: Confirm with user - >6 areas: Suggest grouping ### Phase 2: Discovery Agent (Per Feature-Area) For each feature-area in order: 1. **Scan directory** for monetization signals, user flows, features 2. **Analyze documentation** (README, PRD, pricing) 3. **Identify pricing tier mappings** 4. **Document evidence** **Progress report per area**: ``` Discovery Agent: business feature-area ├── Directory signals: 4 ├── Documentation signals: 2 ├── Pricing signals: 1 └── Status: ✓ Completed ``` ### Phase 3: Pattern Agent (Per Feature-Area) For each feature-area: 1. **Categorize signals** into PDR categories (Problem, Persona, Scope, Metric, etc.) 2. **Score strategic importance** (0.0-1.0) 3. **Check for duplicates** against existing PDRs 4. **Identify cross-area candidates** ### Phase 4: Synthesis Agent (Cross-Feature-Area) **1. Cross-Area Pattern Detection**: ```json { "pattern_id": "P001", "pattern_name": "Admin Persona", "feature_area_presence": {"core": true, "business": true}, "is_cross_area": true } ``` **2. Inconsistency Detection**: - Priority conflicts - Duplicate problems - Inconsistent metrics - Generate flags (embedded in affected PDRs) **3. PDR Generation**: High-strategic patterns (>0.7) and cross-area patterns (≥2 areas) ### Phase 5: Write Individual PDR Files For each discovered PDR: 1. **Assign ID**: PDR-{NNN} using next available number 2. **Write file**: `{REPO_ROOT}/.adlc/drafts/pdr/PDR-{NNN}.md` **PDR file format** (individual file, YAML frontmatter — use the shared template): Use the template at `{REPO_ROOT}/.agents/skills/product/../templates/pdr-template.md` as the canonical PDR format. The template includes YAML frontmatter (status, date, owner, category, feature-area, title) as the single source of truth for index generation, plus the standard PDR body sections. ```markdown --- status: discovered date: YYYY-MM-DD owner: [Inferred from codebase/authors] category: [Problem | Persona | Scope | Metric | Prioritization | Business Model | Feature | NFR] feature-area: [core | business | growth | ...] title: [Decision Title] --- # PDR-001: [Decision Title] ### Cross-Feature-Area Metadata - **Appears in**: [business, growth] - **Cross-area count**: 2 - **Is cross-area pattern**: ✓ ### ⚠️ Inconsistency Flags *None* (or flag details if detected) ### Context **Problem/Opportunity:** [Clear description] **Market Forces:** - [Market factor 1] - [Customer feedback] ### Decision **Decision Statement:** [Clear statement] **Rationale:** [Why this option] ### Consequences #### Positive - [Benefit 1] #### Negative - [Trade-off 1] #### Risks - [Risk with mitigation] ### Success Metrics | Metric | Target | Measurement Method | |--------|--------|-------------------| | [Metric] | [Target] | [Method] | ### Alternatives Considered #### Option A: [Alternative Name] **Description:** [Brief description] **Trade-offs:** [Neutral comparison] ``` ### Phase 6: Regenerate PDR Index Generate `{REPO_ROOT}/.adlc/drafts/pdr/pdr.md` from all `PDR-*.md` files: ```bash # For each PDR-*.md file, extract ID, title, status, date # Build index table ``` **Index format**: ```markdown # Product Decision Records ## PDR Index | ID | Feature-Area | Category | Status | Date | Owner | |----|--------------|----------|--------|------|-------| | PDR-001 | business | Business Model | Discovered | 2026-01-20 | [Inferred] | | PDR-002 | core | Persona | Discovered | 2026-01-20 | [Inferred] | --- ## Cross-Feature-Area Analysis Summary ### Cross-Area Patterns | Pattern | Feature-Areas | PDR | |---------|---------------|-----| | Admin Persona | core, business | PDR-002 | ### Inconsistencies Flagged | Flag ID | Type | PDRs Affected | Severity | |---------|------|---------------|----------| | FLG-001 | Priority Conflict | PDR-003 | Medium | --- *Individual PDR files are in this directory (PDR-*.md)* ``` ### Phase 7: Output Summary ```markdown ## Product Init Complete ✓ ### Execution Stats - **Feature-areas analyzed**: 3 - **Discovery Agent runs**: 3 - **Pattern Agent runs**: 3 - **Synthesis Agent runs**: 1 ### PDRs Generated | Category | Count | Cross-Area | |----------|-------|------------| | Business Model | 2 | ✓ | | Persona | 3 | ✓ | | Problem | 2 | | | Prioritization | 1 | | | **With Inconsistency Flags** | **2** | | ### Next Steps 1. Review PDRs: `{REPO_ROOT}/.adlc/drafts/pdr/` 2. **Resolve inconsistencies**: Run `/product-clarify` 3. Generate PRD: Run `/product-implement` ``` ## PDR Numbering Rules - Scan `{REPO_ROOT}/.adlc/drafts/pdr/` for existing `PDR-*.md` files - Extract numeric suffix, find maximum - Next PDR = max + 1, zero-padded to 3 digits (PDR-001, PDR-002, ...) - Never reuse numbers, even for deleted PDRs ## State Management **State file**: `{REPO_ROOT}/.adlc/product/state.json` ```json { "version": "1.0", "command": "product-init", "created_at": "2026-01-20T10:00:00Z", "phase": "completed", "feature_areas": [ {"id": "core", "name": "Core", "progress": {"discovery": "completed", "pattern": "completed"}} ], "pdrs_generated": 9, "cross_area_patterns": 5, "inconsistencies": 2 } ``` ## Configuration - `PDR_DRAFTS_DIR` — `{REPO_ROOT}/.adlc/drafts/pdr` (individual PDR files) - `PDR_INDEX` — `{REPO_ROOT}/.adlc/drafts/pdr/pdr.md` (auto-generated index) - `PRD_FILE` — `{REPO_ROOT}/PRD.md` - `STATE_FILE` — `{REPO_ROOT}/.adlc/product/state.json` ## 12-Factor Alignment - **Factor III (Mission Definition)**: Discovers the "what & why" behind existing code - **Factor IX (Traceability)**: Every inferred decision is documented with evidence ## Common Rationalizations | Rationalization | Reality | |-----------------|---------| | "The code is self-documenting." | Code shows *how*, not *why*. PDRs capture rationale that code cannot. | | "I'll just read the README." | READMEs describe features, not decisions. PDRs capture the decision tree. | | "Brownfield products don't need PDRs." | Every product has implicit decisions. Making them explicit prevents repeated mistakes. | ## Red Flags - **Generating PDRs without codebase evidence** — brownfield PDRs must be grounded in actual code/docs, not fabricated rationales. - **Skipping inconsistency flags** — cross-area conflicts are real signals, not noise. - **Assigning "Proposed" status to discovered PDRs** — brownfield PDRs are **Discovered**, not proposed. ## Verification - [ ] Setup script returns valid JSON with all paths - [ ] `.adlc/drafts/pdr/` directory exists - [ ] At least one `PDR-*.md` file created with status "Discovered" - [ ] `pdr.md` index auto-generated with correct table - [ ] State file written to `.adlc/product/state.json` - [ ] Inconsistency flags embedded in affected PDRs (not separate files) - [ ] Cross-feature-area analysis summary in index - [ ] No duplicate PDR IDs
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.