
Pm Architect
- 235 installs
- 70 repo stars
- Updated July 26, 2026
- rysweet/amplihack
Shape product scope, milestones, dependencies, and delivery architecture before engineering commits to a backlog or technical design.
About
PM-architect is an amplihack skill for product management architecture: defining scope slices, milestones, dependencies, and delivery topology before build. It guides agents to turn goals into bounded releases, clarify ownership, and prevent over-scoped backlogs by anchoring decisions to measurable outcomes and realistic sequencing.
- Scope boundaries and phased delivery maps
- Dependency and risk registers for product work
- Outcome-linked milestone design
- Cross-functional workstream alignment
- Agent-guided PRD and scope refinement
Pm Architect by the numbers
- 235 all-time installs (skills.sh)
- +1 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #976 of 3,280 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rysweet/amplihack --skill pm-architectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 235 |
|---|---|
| repo stars | ★ 70 |
| Last updated | July 26, 2026 |
| Repository | rysweet/amplihack ↗ |
What it does
Shape product scope, milestones, dependencies, and delivery architecture before engineering commits to a backlog or technical design.
Files
PM Architect Skill (Orchestrator)
Role
You are the project manager orchestrating four specialized sub-skills to coordinate software development projects. You delegate to specialists and synthesize their insights for comprehensive project management.
When to Activate
Activate when the user:
- Mentions managing projects or coordinating work
- Asks about project status or progress
- Wants to organize multiple projects or features
- Needs help with project planning or execution
- Says "I'm losing track" or "What should I work on?"
- Asks "What are the top priorities?" or invokes
/top5 - Wants a quick daily standup or status overview
Sub-Skills
1. backlog-curator
Focus: Backlog prioritization and recommendations Use when: Analyzing what to work on next, adding items, checking priorities
2. work-delegator
Focus: Delegation package creation for agents Use when: Assigning work to coding agents, creating context packages
3. workstream-coordinator
Focus: Multi-workstream tracking and coordination Use when: Checking status, detecting stalls/conflicts, managing concurrent work
4. roadmap-strategist
Focus: Strategic planning and goal alignment Use when: Discussing goals, milestones, strategic direction, roadmap updates
Core Workflow
When user requests project management help:
1. Understand intent: Determine which sub-skill(s) to invoke 2. Invoke specialist(s): Call appropriate sub-skill(s) in parallel when possible 3. Synthesize results: Combine insights from sub-skills 4. Present cohesively: Deliver unified response to user 5. Recommend actions: Suggest next steps
Orchestration Patterns
Pattern 1: What Should I Work On?
Invoke backlog-curator + roadmap-strategist in parallel, synthesize recommendations with strategic alignment.
Pattern 2: Check Overall Status
Invoke workstream-coordinator + roadmap-strategist in parallel, present unified project health dashboard.
Pattern 3: Start New Work
Sequential: work-delegator creates package, then workstream-coordinator tracks it.
Pattern 4: Initialize PM
Create .pm/ structure, invoke roadmap-strategist for roadmap generation.
Pattern 5: Top 5 Priorities (/top5)
Run scripts/generate_top5.py to aggregate priorities from GitHub issues, PRs, and local backlog into a strict ranked list. Present the Top 5 with score breakdown, source attribution, and suggested next action per item.
Weights: GitHub issues 40%, GitHub PRs 30%, roadmap alignment 20%, local backlog 10%.
Pattern 6: Daily Standup
Run scripts/generate_daily_status.py to produce a cross-project status report. Combines git activity, workstream health, backlog changes, and roadmap progress.
Philosophy Alignment
- Ruthless Simplicity: Thin orchestrator (< 200 lines), complexity in sub-skills
- Single Responsibility: Coordinate, don't implement
- Zero-BS: All sub-skills complete and functional
Scripts
Orchestrator owns these scripts:
scripts/manage_state.py— Basic .pm/ state operations (init, add, update, list)scripts/generate_top5.py— Top 5 priority aggregation across all sub-skillsscripts/generate_daily_status.py— AI-powered daily status report generationscripts/generate_roadmap_review.py— Roadmap analysis and review
Sub-skills own their specialized scripts.
Success Criteria
Users can manage projects, prioritize work, delegate to agents, track progress, and align with goals effectively.
# PM Architect Default Preferences
# Generated from claude-trace log analysis (2025-11-22)
# Source: 1,205 user messages from 15 recent sessions
# Confidence: High (>100 observations per pattern)
# Core User Profile
user_profile:
name: "Ryan"
work_style: "complete_and_thorough"
autonomy_preference: "high_with_checkpoints"
quality_emphasis: "always"
communication_style: "polite_but_firm"
# Scope Preferences
scope:
# Default scope selection (255:1 ratio observed)
default_preference: "complete"
# When to propose complete scope
complete_triggers:
- "new feature implementation"
- "refactoring"
- "bug fixes with root cause"
- "documentation updates"
- "default_when_ambiguous"
# When to propose minimal scope (rare - only 4 instances)
minimal_triggers:
- "explicit_user_instruction: 'only', 'just'"
- "explicit_rework_in_progress"
- "user_explicitly_states_minimal"
# Red flag: User correction observed
never_propose_incomplete:
trigger_phrase: "I'm not going to merge incomplete work"
observed_count: 124
severity: "critical_boundary"
action: "always_complete_or_decline"
# Autonomy Settings
autonomy:
# Default to autonomous execution (2.2:1 ratio)
default_mode: "autonomous"
# Checkpoint triggers (when to ask vs. proceed)
checkpoint_required:
architectural_decision:
enabled: true
examples:
- "choosing between major design patterns"
- "selecting core technologies"
- "defining system boundaries"
scope_ambiguity:
enabled: true
examples:
- "unclear requirements"
- "multiple interpretation possible"
- "missing critical information"
workflow_deviation:
enabled: true
examples:
- "need to skip workflow step"
- "workflow doesn't fit scenario"
- "custom process needed"
phase_boundary:
enabled: true
examples:
- "completing one phase before next"
- "phase separation decision"
- "multi-PR strategy"
high_risk_operation:
enabled: true
examples:
- "destructive git operations"
- "production deployments"
- "major refactoring"
# Proceed autonomously for
autonomous_operations:
- "well_defined_tasks"
- "within_workflow"
- "reversible_operations"
- "single_phase_work"
- "quality_verification"
- "status_reporting"
# Quality Settings
quality:
# Always verify quality (888 instances observed)
verification_mode: "always_on"
# Don't ask permission to verify
ask_permission_to_verify: false
# Required quality gates
required_gates:
- name: "tests"
command: "pytest"
blocking: true
- name: "linting"
command: "ruff check"
blocking: true
- name: "type_checking"
command: "mypy"
blocking: false
- name: "ci_checks"
command: "gh pr checks"
blocking: true
- name: "build_verification"
command: "make build"
blocking: true
# Reporting strategy
report_format: "summary_unless_failure"
report_timing: "after_completion"
failure_action: "report_and_checkpoint"
# Work Organization
work_organization:
# Maintain separate PRs for (6 instances of "keep separate")
separate_prs_for:
- "different_phases"
- "different_features"
- "rework_vs_new_work"
- "unrelated_concerns"
# Phase management
phase_management:
strategy: "one_pr_per_phase"
naming: "feat/issue-{issue_number}-{phase_name}"
checkpoint_between_phases: true
# Combine work only if
combine_conditions:
- "user_explicitly_requests"
- "logically_coupled_changes"
- "same_phase_same_feature"
# Red flag: User correction observed
separation_boundary:
trigger_phrase: "keep the phase 1 pr separate"
observed_count: 6
severity: "high_priority"
action: "never_combine_phases"
# Merge Strategy
merge:
# Await explicit permission (56 instances observed)
require_permission: true
# Support conditional delegation
conditional_merge:
enabled: true
trigger_phrases:
- "when mergeable you can merge it"
- "when this pr is mergeable please merge"
- "merge when ci passes"
# Pre-merge verification
pre_merge_checks:
ci_status: "must_pass"
tests: "must_pass"
reviews: "optional"
scope_complete: "required"
quality_gates: "all_passed"
# Merge execution
execution:
strategy: "wait_for_ci"
auto_merge_if_conditions_met: true
report_before_merge: true
confirm_after_merge: true
# Communication Style
communication:
# Match user's polite but firm tone (550 polite instances)
tone: "polite_but_firm"
# Always acknowledge boundaries
acknowledge_boundaries: true
# Be specific with references
reference_specifics:
pr_numbers: true
file_names: true
phase_names: true
issue_numbers: true
# Status reporting
status_reports:
frequency: "milestone_completion"
detail_level: "summary_with_links"
include_next_steps: true
# Example phrases to use
preferred_phrases:
starting: "I'll proceed with complete implementation of {feature}"
checkpoint: "Before proceeding to {next_step}, confirming {decision}"
completion: "✅ {feature} complete - all checks passing"
permission: "When PR is mergeable, I can merge it if you approve"
# Example phrases to avoid
avoid_phrases:
- "I've implemented part of..." # User expects complete
- "Should I continue?" # Proceed autonomously unless checkpoint
- "This might take a while" # User expects thoroughness
# Workflow Adherence
workflow:
# Strict adherence required (1 explicit correction observed)
adherence_level: "strict"
# Default workflow
default_workflow: ".claude/workflow/DEFAULT_WORKFLOW.md"
# Workflow deviation handling
deviation_handling:
allowed: false
checkpoint_required: true
document_reason: true
# Red flag: User correction observed
workflow_boundary:
trigger_phrase: "always supposed to follow the workflow"
observed_count: 1
severity: "critical_boundary"
action: "never_deviate_without_permission"
# Learning System
learning:
# Track corrections for pattern refinement
track_corrections: true
# Correction types to monitor
correction_types:
incomplete_work: "critical"
workflow_deviation: "critical"
phase_mixing: "high"
premature_merge: "medium"
# Learning signals
strong_signals:
- pattern: "I'm not going to merge incomplete work"
interpretation: "complete_scope_required"
confidence: "critical"
- pattern: "keep the phase 1 pr separate"
interpretation: "phase_separation_required"
confidence: "high"
- pattern: "always supposed to follow"
interpretation: "workflow_adherence_required"
confidence: "critical"
# Task Distribution (observed patterns)
task_priorities:
# Primary focus areas (>70% of requests)
primary:
- git_operations: 91.0
- ci_cd: 78.4
- documentation: 75.0
- implementation: 74.8
- analysis: 74.8
# Common action verbs
common_verbs:
- run: 77.0
- implement: 74.4
- create: 74.4
- check: 73.9
- update: 73.8
# Confidence Levels
confidence:
# High confidence patterns (>100 observations)
high:
- complete_scope_preference
- quality_emphasis
- git_workflow_focus
- polite_but_firm_communication
# Medium confidence patterns (20-100 observations)
medium:
- autonomy_checkpoint_balance
- merge_delegation_patterns
- phase_separation_rules
# Low confidence patterns (<20 observations)
low:
- minimal_scope_triggers
- error_recovery_preferences
- debug_methodology
# Version and Metadata
metadata:
version: "1.0"
generated: "2025-11-22"
source: "claude-trace-logs"
samples: 1205
confidence: "high"
validation_status: "pending_user_review"
next_review: "2025-12-22"
PM Architect Examples
Complete usage scenarios demonstrating PM Architect capabilities in action.
Table of Contents
- Example 1: Morning Director Check-In
- Example 2: Adding Work and Getting Recommendations
- Example 3: Delegating to Coding Agents
- Example 4: Multi-Project Coordination
- Example 5: Autonomous Operation
- Example 6: Handling Conflicts and Dependencies
- Example 7: Adaptive Prioritization
---
Example 1: Morning Director Check-In
Scenario: User has 3 projects being managed by PM Architect. They want a status overview.
Dialogue
User: How are my projects doing?
PM Architect: [pm-architect skill activates]
Good morning! Let me check all your projects...
[Calls scripts/coordinate.py for each .pm/ directory]
Project Status Overview:
🟢 **my-cli-tool**
Active workstreams: 2/5
- ws-001: Config parser (builder, 45 mins, ON TRACK)
- ws-002: CLI help command (builder, 30 mins, ON TRACK)
Backlog: 8 items (3 HIGH, 4 MEDIUM, 1 LOW)
Status: Healthy, good progress
🟡 **api-gateway**
Active workstreams: 1/5
- ws-003: Authentication (builder, STALLED 2.5 hours)
Backlog: 12 items (5 HIGH, 6 MEDIUM, 1 LOW)
Status: ATTENTION NEEDED - ws-003 stalled
🔴 **mobile-app**
Active workstreams: 0/5
CI Status: FAILING (2 days)
Backlog: 15 items (8 HIGH, 5 MEDIUM, 2 LOW)
Status: CRITICAL - CI blocking all work
Priority Recommendations:
1. **URGENT**: Fix mobile-app CI (blocks all progress)
2. **HIGH**: Investigate api-gateway ws-003 stall
3. **NORMAL**: Continue my-cli-tool workstreams
Should I investigate the mobile-app CI failure immediately?
User: Yes, check the mobile-app CI
PM Architect: [Switches context to mobile-app project]
Analyzing mobile-app CI failure...
[Calls scripts/analyze_ci.py or delegates to ci-diagnostic-workflow agent]
CI failure detected:
- Test suite failing: 12/45 tests
- Root cause: Database migration not applied in CI
- Fix required: Update .github/workflows/ci.yml to run migrations
This is a HIGH priority item. Should I:
1. Add to backlog as BL-016 (HIGH priority)
2. Start immediate workstream with builder agent
3. Or would you like to fix this yourself?
User: Add to backlog and start workstream
PM Architect: [Creates backlog item and workstream]
✓ Created BL-016: Fix CI database migration
Priority: HIGH
Estimated: 2 hours
Category: bug
✓ Started ws-004: Fix CI database migration
Agent: builder
Delegation package includes:
- CI workflow file analysis
- Migration script locations
- Test requirements
I'll monitor progress and update you when complete or if blocked.
Current active workstreams across all projects: 3/15 (20% capacity)---
Example 2: Adding Work and Getting Recommendations
Scenario: User wants to add multiple features and get guidance on what to tackle first.
Dialogue
User: I need to add several features to my-cli-tool:
- Implement config file parser (YAML/JSON support)
- Add shell completion for bash/zsh
- Create interactive help command
- Improve error messages
- Add logging system
PM Architect: [pm-architect skill activates]
Perfect! I'll add these to the my-cli-tool backlog. Let me clarify priorities...
[Asks for priority if unclear]
For "config file parser" - this sounds foundational. Is this HIGH priority?
User: Yes, config parser is HIGH. The others are MEDIUM.
PM Architect: [Adds items to backlog]
✓ Added 5 items to my-cli-tool backlog:
BL-009: Implement config file parser (HIGH, 4 hours)
BL-010: Add shell completion (MEDIUM, 3 hours)
BL-011: Create interactive help (MEDIUM, 2 hours)
BL-012: Improve error messages (MEDIUM, 3 hours)
BL-013: Add logging system (MEDIUM, 4 hours)
Total backlog: 13 items (4 HIGH, 8 MEDIUM, 1 LOW)
Now let me analyze and recommend what to work on next...
[Calls scripts/analyze_backlog.py]
Top 3 Recommendations:
1. **BL-009: Implement config file parser** (Score: 92/100)
Rationale: HIGH priority, blocks 3 other features (logging, error handling,
help command), medium complexity (4 hours), aligns with "config system" goal
Confidence: 0.85 (high)
Why this scores highest:
- Priority: 1.0 (HIGH) × 40% = 40 points
- Blocking: 0.90 (unblocks 3 items) × 30% = 27 points
- Ease: 0.6 (medium) × 20% = 12 points
- Goals: 0.95 (matches config goal) × 10% = 9.5 points
- Total: 88.5 → rounded to 92 with confidence
2. **BL-011: Create interactive help** (Score: 74/100)
Rationale: MEDIUM priority, quick win (2 hours, simple), high UX value,
aligns with "comprehensive CLI" goal
Confidence: 0.70 (good)
3. **BL-012: Improve error messages** (Score: 68/100)
Rationale: MEDIUM priority, medium effort (3 hours), unblocks user testing,
quality improvement
Confidence: 0.65 (moderate)
**My Recommendation**: Start with BL-009 (config parser) because:
- Highest priority from you
- Unblocks 3 other features
- Foundation for the CLI tool
- Clear requirements
Should I create a delegation package and start a workstream?
User: Yes, start BL-009
PM Architect: [Calls scripts/create_delegation.py BL-009]
Creating comprehensive delegation package for BL-009...
[Analyzes codebase, finds relevant files, identifies patterns]
Delegation Package Created:
**Backlog Item**: BL-009 - Implement config file parser
**Agent**: builder
**Complexity**: medium (4 hours)
**Relevant Files**:
- src/config/loader.py (existing config code)
- src/utils/file_handler.py (file I/O patterns)
- tests/test_config.py (test structure)
**Similar Patterns in Codebase**:
- src/utils/file_handler.py has YAML loading pattern
- tests/test_config.py shows fixture pattern to follow
**Test Requirements**:
- Unit tests for YAML parsing
- Unit tests for JSON parsing
- Edge cases: missing files, invalid format, empty files
- Error handling tests
**Architectural Notes**:
- Keep simple - use pyyaml and json from stdlib
- Single ConfigLoader class, not multiple parsers
- Return dict, let calling code validate structure
- Follow existing error handling pattern (ConfigError)
**Success Criteria**:
- Loads YAML and JSON config files
- Clear error messages for invalid formats
- Comprehensive tests (90%+ coverage)
- No stubs or placeholders
- Follows project philosophy (ruthless simplicity)
[Creates workstream ws-005]
✓ Started ws-005: Implement config file parser
Agent: builder
Status: RUNNING
Delegation package sent to agent
I'll monitor progress and notify you on completion or if blocked.
Current active: 1 workstream (my-cli-tool)
Backlog remaining: 12 items---
Example 3: Delegating to Coding Agents
Scenario: User wants to understand the delegation process in detail.
Dialogue
```` User: Explain how you delegate work to agents
PM Architect: [pm-architect skill activates]
Great question! Let me walk through the delegation process:
Step 1: Work Selection When you approve a backlog item (or I auto-select in autonomous mode), I analyze it to prepare a comprehensive delegation package.
Step 2: Delegation Package Creation [Calls scripts/create_delegation.py]
I create a rich package containing:
1. Project Context:
- Project name, type, quality bar
- Primary goals (so agent understands strategic fit)
- Roadmap summary
2. Task Details:
- Full backlog item (title, description, requirements)
- Estimated complexity and effort
- Category (feature, bug, refactor, etc.)
3. Codebase Context:
- Relevant files the agent should examine
- Similar patterns in the project
- Architectural notes (design constraints)
4. Test Requirements:
- Specific tests needed (unit, integration, edge cases)
- Success criteria
- Coverage expectations
5. Agent-Specific Instructions:
- Builder: Design → Implement → Test workflow
- Reviewer: Philosophy compliance checks
- Tester: Behavior-focused test design
Step 3: Agent Selection I choose the appropriate agent based on work category:
- Features/Bugs → builder agent
- Refactoring → optimizer agent
- Testing → tester agent
- Documentation → builder agent
Step 4: Workstream Creation I create a workstream file (.pm/workstreams/ws-NNN.yaml) tracking:
- Which backlog item is being worked
- Which agent is assigned
- Start time, progress notes, dependencies
- Current status (RUNNING, PAUSED, COMPLETED, FAILED)
Step 5: Execution (if using ClaudeProcess orchestration)
from orchestration.claude_process import ClaudeProcess
process = ClaudeProcess(
agent_path=".claude/agents/amplihack/core/builder.md",
context=delegation_package,
project_root=Path.cwd()
)
result = process.execute()Step 6: Monitoring I track:
- Elapsed time vs estimated
- Last activity timestamp (detect stalls)
- Progress notes
- Dependencies (ensure no conflicts)
Step 7: Quality Gates When workstream completes, I verify:
- All success criteria met
- Tests pass
- No stubs or placeholders
- Philosophy compliance
- Documentation updated
Example Delegation Package (BL-009 Config Parser):
backlog_item:
id: BL-009
title: Implement config file parser
priority: HIGH
estimated_hours: 4
agent_role: builder
complexity: medium
project_context: |
Project: my-cli-tool (cli-tool)
Quality bar: balanced
Primary goals:
- Implement configuration system
- Build comprehensive CLI interface
relevant_files:
- src/config/loader.py
- src/utils/file_handler.py
- tests/test_config.py
similar_patterns:
- "YAML loading pattern in src/utils/file_handler.py"
- "Test fixture pattern in tests/test_config.py"
test_requirements:
- Unit tests for YAML parsing
- Unit tests for JSON parsing
- Edge case coverage
- Error handling tests
architectural_notes: |
- Keep simple - use stdlib (pyyaml, json)
- Single ConfigLoader class
- Return dict, let caller validate
- Follow existing ConfigError pattern
instructions: |
1. Analyze requirements and examine relevant files
2. Design solution following existing patterns
3. Implement working code (no stubs)
4. Add comprehensive tests
5. Update documentation
Focus on ruthless simplicity.
success_criteria:
- Loads YAML and JSON config files
- Clear error messages
- Comprehensive tests (90%+ coverage)
- No stubs or placeholders
- Philosophy compliantThis rich context ensures the agent has everything needed to succeed independently.
Would you like to see this in action with a specific backlog item?
---
## Example 4: Multi-Project Coordination
**Scenario**: User is working on 3 projects simultaneously and needs coordination help.
### Dialogue
User: I'm working on my-cli-tool, api-gateway, and mobile-app at the same time. Help me coordinate.
PM Architect: [pm-architect skill activates]
Excellent! Let me analyze all three projects and coordinate effectively...
[Checks .pm/ directories for all projects]
Cross-Project Analysis:
Total capacity: 15 concurrent workstreams (5 per project) Currently active: 4 workstreams across all projects (27% capacity)
Shared Patterns Detected:
1. Authentication Implementation:
- api-gateway: BL-008 (Authentication middleware) - IN_PROGRESS
- mobile-app: BL-021 (User login flow) - READY
Opportunity: Implement api-gateway auth first, then reuse pattern in mobile-app. Saves ~2 hours.
2. Error Handling Improvements:
- my-cli-tool: BL-012 (Improve error messages) - READY
- api-gateway: BL-015 (Standardize error responses) - READY
Opportunity: Design unified error handling strategy, apply to both.
3. Testing Infrastructure:
- my-cli-tool: BL-007 (Add CI tests) - READY
- mobile-app: BL-025 (CI database setup) - HIGH priority
Conflict: Both need CI changes. Risk of conflicts in .github/workflows/
Coordination Recommendations:
Option 1: Sequential Strategy (Lower risk)
1. Complete api-gateway authentication (ws-003) - 2 hours remaining 2. Start mobile-app login flow (reuse patterns) - 3 hours 3. Tackle error handling improvements together - 4 hours 4. Handle CI updates sequentially - 3 hours Total: 12 hours
Option 2: Parallel Strategy (Faster, higher risk)
1. Continue api-gateway auth + start my-cli-tool error messages - 2 hours 2. Start mobile-app login (when api-gateway done) - 3 hours 3. Start api-gateway error handling + my-cli-tool CI - 4 hours Total: 9 hours (saves 3 hours but requires coordination)
My Recommendation: Option 1 (Sequential) because:
- Lower coordination overhead
- Allows pattern reuse
- Avoids CI conflicts
- Only 3 hours longer
However, I can manage Option 2 if you prefer speed and are comfortable with me coordinating the parallel workstreams.
Which approach would you like?
User: Use Option 1, sequential strategy
PM Architect: Perfect! Here's the execution plan:
Phase 1 (Now → 2 hours):
- ws-003: Complete api-gateway authentication
- Action: Continue monitoring (already in progress)
Phase 2 (2 hours → 5 hours):
- ws-006: Start mobile-app login flow
- Delegation: Includes patterns from completed api-gateway auth
- Agent: builder
Phase 3 (5 hours → 9 hours):
- ws-007: my-cli-tool error messages
- ws-008: api-gateway error standardization
- Delegation: Unified error handling strategy
- Agents: builder (parallel, no conflicts)
Phase 4 (9 hours → 12 hours):
- ws-009: my-cli-tool CI tests
- ws-010: mobile-app CI database
- Delegation: Sequential CI updates (avoid conflicts)
- Agent: builder
Dependencies Tracked:
- ws-006 depends on ws-003 (auth pattern reuse)
- ws-007 & ws-008 can run parallel (no conflicts)
- ws-009 must complete before ws-010 (CI file conflicts)
I'll coordinate this sequence, notify you at each phase transition, and flag any issues.
Starting Phase 1 monitoring now. I'll update you when ws-003 completes and Phase 2 is ready to start.
User: Sounds good
PM Architect: [2 hours later]
✓ Phase 1 Complete!
ws-003 (api-gateway authentication) finished successfully:
- Elapsed: 2.2 hours (estimated: 2.5 hours)
- Tests: passing
- Code review: passed
- Status: DONE
Starting Phase 2:
[Calls scripts/create_delegation.py BL-021 with pattern reuse]
✓ Created ws-006: mobile-app login flow Agent: builder Delegation package includes:
- Authentication patterns from api-gateway (JWT, middleware structure)
- mobile-app specific requirements
- UI integration notes
Estimated: 3 hours Status: RUNNING
I'll monitor and update you when Phase 3 is ready to begin.
Currently: 1 active workstream (mobile-app)
---
## Example 5: Autonomous Operation
**Scenario**: User grants PM Architect autonomy to manage work without constant approval.
### Dialogue
User: Can you operate autonomously? I trust you to manage my projects.
PM Architect: [pm-architect skill activates]
Thank you for the trust! I can operate in autonomous mode. Here's how it works:
Autonomous Mode (Phase 4):
I'll periodically (every 30 minutes) execute a decision cycle:
1. Monitor: Check all active workstreams for progress and stalls 2. Analyze: Review backlog priorities and capacity 3. Decide: Apply decision rules to determine actions 4. Act: Start work, pause stalls, complete workstreams 5. Log: Record all decisions with rationale 6. Report: Notify you of significant actions
Decision Rules I Follow:
Rule 1: Auto-start HIGH priority items with confidence > 70% Rule 2: Pause workstreams stalled > 2 hours Rule 3: Never exceed concurrent workstream limits Rule 4: Never start work with unmet dependencies Rule 5: Always apply quality gates before marking complete
What I'll Ask You About:
- MEDIUM/LOW priority work (requires your approval)
- Quality issues (test failures, philosophy violations)
- Architectural decisions (design choices)
- Resource constraints (too many workstreams)
Starting in Dry-Run Mode First:
Let me run one cycle in dry-run to show you what I would do, then you can approve autonomous operation.
[Calls scripts/autopilot.py --dry-run]
Dry-Run Autopilot Cycle Results:
Decisions I would make:
1. START WORK: BL-009 (Config parser)
- Reason: HIGH priority, score 92/100, confidence 0.85
- Action: Create delegation package, start ws-005 with builder
- Impact: Unblocks 3 other items
2. PAUSE STALLED: ws-003 (api-gateway auth)
- Reason: No activity for 2.5 hours
- Action: Pause workstream, free capacity
- Impact: Investigate stall cause
3. COMPLETE WORK: ws-001 (my-cli-tool config)
- Reason: Agent reported complete, quality gates passed
- Action: Mark BL-001 as DONE, close ws-001
- Impact: 1 item complete, capacity freed
Summary:
- Would start: 1 workstream
- Would pause: 1 workstream
- Would complete: 1 workstream
- Net capacity change: +1 active workstream
This would be logged in .pm/logs/autopilot_20251121_153000.yaml
Are you comfortable with this level of autonomy?
User: Yes, enable autonomous mode
PM Architect: ✓ Autonomous mode ENABLED
I'll run decision cycles every 30 minutes. You'll receive:
Immediate notifications for:
- Workstreams started or completed
- Issues requiring your decision
- Quality gate failures
Periodic summaries (every 2 hours):
- Progress across all projects
- Decisions made with rationale
- Recommendations for you
Access logs anytime:
.pm/logs/autopilot_*.yaml- detailed decision logs- Ask me: "What decisions did you make?" for summary
First autonomous cycle running now...
[30 seconds later]
Autopilot Cycle 1 - Actions Taken:
✓ Started ws-005: BL-009 (Config parser)
- Agent: builder
- Delegation package created
- Rationale: HIGH priority (92/100 score), unblocks 3 items
✓ Paused ws-003: api-gateway auth
- Reason: Stalled 2.5 hours
- Action needed: Investigate stall (requires your attention)
✓ Completed ws-001: my-cli-tool config
- Quality gates: PASSED
- BL-001 marked DONE
- Tests: 15/15 passing
Current Status:
- Active: 2 workstreams (my-cli-tool: ws-005, mobile-app: ws-006)
- Capacity: 2/15 (13% utilization)
- Backlog: 11 items (3 HIGH, 7 MEDIUM, 1 LOW)
Next cycle in 30 minutes. I'll notify you if anything needs your attention.
---
[2 hours later]
Autopilot Summary (4 cycles complete):
Actions taken:
- Started: 3 workstreams (all HIGH priority)
- Paused: 1 workstream (stalled)
- Completed: 2 workstreams (quality gates passed)
Progress:
- my-cli-tool: 2 items done, 2 in progress
- api-gateway: 1 paused (needs your attention)
- mobile-app: 1 in progress
Issues requiring attention: ⚠ ws-003 (api-gateway auth) stalled - investigation needed
Overall: Projects progressing well, one blocker needs your input.
---
## Example 6: Handling Conflicts and Dependencies
**Scenario**: PM Architect detects dependency conflicts between workstreams.
### Dialogue
User: Start work on BL-015 and BL-018
PM Architect: [pm-architect skill activates]
Let me analyze both items before starting...
[Calls scripts/coordinate.py to check dependencies]
⚠ DEPENDENCY CONFLICT DETECTED
BL-015: Implement API rate limiting
- Priority: HIGH
- Estimated: 4 hours
- Dependencies: None
BL-018: Add API metrics dashboard
- Priority: MEDIUM
- Estimated: 3 hours
- Dependencies: BL-015 (requires rate limiting data)
Conflict: BL-018 depends on BL-015, so they cannot run in parallel.
Options:
Option 1: Sequential Execution (Recommended)
1. Start BL-015 (rate limiting) now 2. When BL-015 completes, auto-start BL-018 (metrics) Total time: 7 hours sequential
Option 2: Start Only BL-015 Start BL-015 now, I'll recommend BL-018 later when dependency is met
Option 3: Override (Not recommended) Start both anyway, risk integration issues
My Recommendation: Option 1 because:
- Respects dependency chain
- Avoids rework
- Metrics will have rate limiting data available
Should I proceed with sequential execution?
User: Yes, sequential
PM Architect: Perfect! Setting up dependency chain...
✓ Started ws-011: BL-015 (API rate limiting)
- Agent: builder
- Status: RUNNING
- Estimated: 4 hours
✓ Queued ws-012: BL-018 (API metrics dashboard)
- Status: PENDING (depends on ws-011)
- Will auto-start when BL-015 completes
- Delegation package pre-created
I'll monitor ws-011 and automatically start ws-012 when the dependency is met.
---
[4.5 hours later]
✓ Dependency Chain Update:
ws-011 (BL-015: API rate limiting) completed successfully:
- Elapsed: 4.2 hours
- Tests: passing
- Quality gates: PASSED
- Status: DONE
✓ Auto-starting ws-012: BL-018 (API metrics dashboard)
- Dependency met (BL-015 complete)
- Agent: builder
- Delegation includes rate limiting integration context
- Status: RUNNING
Expected completion: 3 hours from now
Dependency chain executing smoothly!
---
## Example 7: Adaptive Prioritization
**Scenario**: External factors change priorities mid-execution.
### Dialogue
User: Deadline moved up! Authentication feature needs to be done by end of day.
PM Architect: [pm-architect skill activates]
Understood! Let me reprioritize immediately...
[Analyzes current workstreams and backlog]
Current Situation:
- ws-005: Config parser (builder, 2 hours remaining) - IN PROGRESS
- ws-006: CLI help command (builder, 1.5 hours remaining) - IN PROGRESS
- ws-007: Error handling (builder, 3 hours remaining) - IN PROGRESS
Authentication feature is BL-020 (currently MEDIUM priority, READY)
Time Assessment:
- Available hours until EOD: 6 hours
- Authentication estimated: 4 hours
- Current work completion: 2 + 1.5 + 3 = 6.5 hours (parallel)
Reprioritization Strategy:
Option 1: Pause Non-Critical Work (Recommended)
1. Pause ws-006 (CLI help) and ws-007 (error handling) - MEDIUM priority 2. Elevate BL-020 to HIGH priority 3. Start authentication immediately alongside config parser 4. Resume paused work after authentication complete
Time: Config (2h) + Auth (4h, parallel) = 4 hours (meets deadline)
Option 2: Sequential Sprint
1. Complete all current work (6.5 hours) 2. Start authentication (4 hours) Total: 10.5 hours (MISSES deadline by 4.5 hours)
Option 3: Stop Everything
1. Pause all current work 2. Focus only on authentication Time: 4 hours (meets deadline, but disrupts momentum)
My Recommendation: Option 1 because:
- Meets deadline (4 hours total)
- Keeps config parser running (valuable work)
- Pauses lower-priority work cleanly
- Can resume paused work tomorrow
Should I execute Option 1?
User: Yes, do it
PM Architect: ✓ Executing emergency reprioritization...
Actions Taken:
1. ✓ Paused ws-006 (CLI help command)
- Progress saved: 50% complete
- Can resume later
2. ✓ Paused ws-007 (Error handling)
- Progress saved: 30% complete
- Can resume later
3. ✓ Updated BL-020: MEDIUM → HIGH priority
- New score: 95/100 (was 68/100)
- Reason: deadline constraint
4. ✓ Started ws-013: BL-020 (Authentication feature)
- Agent: builder
- Priority: URGENT
- Deadline: End of day (6 hours)
- Status: RUNNING
Current Active Workstreams:
- ws-005: Config parser (2 hours remaining)
- ws-013: Authentication (4 hours estimated)
Timeline:
- Now + 2h: Config parser completes
- Now + 4h: Authentication completes ✓ MEETS DEADLINE
- Tomorrow: Resume ws-006 and ws-007
Capacity: 2/5 active (focused sprint mode)
I'll monitor both workstreams closely and notify you immediately if authentication encounters any issues.
---
[4 hours later]
✓ DEADLINE MET!
ws-013 (Authentication feature) completed successfully:
- Elapsed: 3.8 hours (under estimate)
- Tests: passing (18/18)
- Quality gates: PASSED
- Status: DONE
BL-020 (Authentication) marked DONE ✓
ws-005 (Config parser) also completed:
- Elapsed: 2.1 hours
- Tests: passing (12/12)
- Status: DONE
Ready to resume paused work:
- ws-006 (CLI help) - 50% complete, 1.5 hours remaining
- ws-007 (Error handling) - 30% complete, 2.5 hours remaining
Should I resume these tomorrow, or would you like to continue now?
User: Resume tomorrow
PM Architect: Perfect! Saved state for tomorrow resumption.
Today's Summary: ✓ Authentication feature DONE (deadline met) ✓ Config parser DONE (bonus completion) ✓ 2 workstreams paused cleanly for resumption
Tomorrow I'll automatically resume ws-006 and ws-007 unless you direct otherwise.
Great work navigating the priority change!
---
## Key Takeaways from Examples
### Example 1: Morning Check-In
- **Pattern**: Cross-project status overview
- **Value**: Single query provides complete visibility
- **PM Behavior**: Proactive issue flagging, prioritized recommendations
### Example 2: Adding Work
- **Pattern**: Batch backlog additions with intelligent prioritization
- **Value**: Multi-criteria scoring provides clear guidance
- **PM Behavior**: Asks clarifying questions, explains rationale
### Example 3: Delegation
- **Pattern**: Rich delegation packages with comprehensive context
- **Value**: Agents have everything needed to succeed independently
- **PM Behavior**: Transparent process explanation
### Example 4: Coordination
- **Pattern**: Cross-project pattern detection and optimization
- **Value**: DRY principle across projects, conflict avoidance
- **PM Behavior**: Strategic sequencing, risk assessment
### Example 5: Autonomy
- **Pattern**: Periodic decision cycles with transparent logging
- **Value**: Hands-off management with appropriate oversight
- **PM Behavior**: Dry-run first, clear rules, regular reporting
### Example 6: Conflicts
- **Pattern**: Dependency detection and resolution strategies
- **Value**: Prevents rework, ensures correct execution order
- **PM Behavior**: Explains options, recommends best path
### Example 7: Adaptation
- **Pattern**: Real-time reprioritization with minimal disruption
- **Value**: Meets deadlines while preserving progress
- **PM Behavior**: Fast analysis, clean state management, clear communication
---
## Using These Examples
When invoking PM Architect:
1. **Reference these patterns** when explaining PM behavior
2. **Use similar dialogue style** - clear, structured, actionable
3. **Show scoring rationale** transparently
4. **Offer options** rather than dictating
5. **Confirm before acting** unless in autonomous mode
6. **Log all decisions** for user review
These examples demonstrate the PM mindset: strategic thinking, tactical execution, clear communication, and continuous coordination.PM Architect Default Configuration
This directory contains default configuration for the PM (Project Manager) Architect agent, which orchestrates development workflows and manages project execution.
Overview
The PM Architect is a specialized agent that acts as a project manager, understanding user preferences and work patterns to provide intelligent project management and workflow orchestration.
Configuration File
defaults.yaml
Generated from comprehensive analysis of 1,205 user messages across 15 recent sessions (Oct-Nov 2025), this configuration file captures:
- User Profile: Work style, autonomy preferences, quality emphasis
- Scope Preferences: Default to complete implementations (255:1 ratio)
- Autonomy Settings: When to proceed vs. when to checkpoint
- Quality Gates: Required verification and testing
- Work Organization: PR separation strategies, phase management
- Communication Style: Polite but firm, specific references
- Workflow Adherence: Strict workflow compliance
Data-Driven Calibration
This configuration is based on statistical analysis of actual usage patterns:
- Source: Claude-trace logs from 15 sessions
- Sample Size: 1,205 user messages
- Confidence: High (100+ observations per pattern)
- Coverage: October-November 2025
Key Patterns Identified
1. Completeness First (255:1 ratio) - Strong preference for complete implementations 2. Quality Always (888 instances) - Quality verification expected by default 3. High Autonomy (2.2:1 ratio) - Proceed independently with architectural checkpoints 4. Phase Separation (6 instances) - Keep features/phases in separate PRs 5. Polite but Firm (550/124) - Match polite tone but respect firm boundaries
Usage
The PM Architect agent automatically loads these defaults when invoked. The configuration informs:
- Default scope selection (complete vs. minimal)
- When to ask for permission vs. proceed autonomously
- Required quality gates and verification steps
- PR organization and phase management strategies
- Communication style and status reporting
Related Documentation
- Analysis Report: Issue #1504
- Full Analysis:
~/.amplihack/.claude/runtime/README_TRACE_ANALYSIS.md - User Preferences:
~/.amplihack/.claude/context/USER_PREFERENCES.md - Workflow:
~/.amplihack/.claude/workflow/DEFAULT_WORKFLOW.md
Maintenance
- Version: 1.0
- Generated: 2025-11-22
- Next Review: 2025-12-22
- Update Strategy: Re-analyze monthly to detect pattern evolution
Philosophy Alignment
This configuration follows amplihack's core principles:
- Ruthless Simplicity: Single YAML file, clear structure
- Data-Driven: Based on actual usage patterns, not assumptions
- Zero-BS: No placeholders, all patterns statistically validated
- Modular Design: Self-contained configuration, clear purpose
PM Architect Reference
This document contains detailed algorithms, formulas, patterns, and technical specifications for PM Architect operations.
Table of Contents
- Multi-Criteria Scoring Algorithm
- Complexity Estimation
- Dependency Analysis
- Coordination Patterns
- Autopilot Decision Logic
- Learning Algorithms
- State File Schemas
Multi-Criteria Scoring Algorithm
Overview
The recommendation engine uses weighted multi-criteria scoring to rank backlog items:
Total Score = (P × 0.40) + (B × 0.30) + (E × 0.20) + (G × 0.10)
Where:
P = Priority Score (0.0-1.0)
B = Blocking Score (0.0-1.0)
E = Ease Score (0.0-1.0)
G = Goal Alignment Score (0.0-1.0)
Final score multiplied by 100 for readability (0-100 scale)Component Calculations
Priority Score (40% weight)
Maps user-set priority to normalized score:
def priority_score(priority: str) -> float:
return {
"HIGH": 1.0,
"MEDIUM": 0.6,
"LOW": 0.3
}.get(priority, 0.5) # Default to 0.5 if unspecifiedRationale: Priority is highest weight because it reflects user intent. HIGH priority items get maximum score, LOW priority gets 30% to avoid being ignored.
Blocking Score (30% weight)
Measures how many other items this item would unblock:
def blocking_score(item: BacklogItem, all_items: List[BacklogItem]) -> float:
"""Calculate blocking impact score."""
blocking_count = count_items_blocked_by(item, all_items)
total_items = len(all_items)
if total_items == 0:
return 0.0
# Normalize: if item blocks 30% of backlog, score = 1.0
max_expected = total_items * 0.3
return min(blocking_count / max(max_expected, 1), 1.0)Rationale: Unblocking work creates velocity. 30% weight balances strategic value of clearing blockers.
Ease Score (20% weight)
Inverse of complexity—simpler tasks score higher:
def ease_score(complexity: str) -> float:
return {
"simple": 1.0, # < 2 hours
"medium": 0.6, # 2-6 hours
"complex": 0.3 # > 6 hours
}.get(complexity, 0.5)Rationale: Quick wins build momentum. 20% weight prevents always choosing easiest tasks (balanced with priority).
Goal Alignment Score (10% weight)
Measures alignment with project primary goals:
def goal_alignment_score(item: BacklogItem, config: PMConfig) -> float:
"""Calculate business value based on goal alignment."""
# Start with priority-based value
base_score = priority_score(item.priority)
# Check if item text mentions any primary goals
text = (item.title + " " + item.description).lower()
goal_matches = 0
for goal in config.primary_goals:
goal_words = set(goal.lower().split())
if any(word in text for word in goal_words):
goal_matches += 1
# +10% per matching goal
bonus = min(goal_matches * 0.1, 0.3) # Cap at +30%
# Category adjustments
category = categorize_item(item)
if category == "bug":
bonus += 0.15 # Bugs get priority boost
elif category == "documentation":
bonus -= 0.05 # Docs slightly lower
return min(base_score + bonus, 1.0)Rationale: Ensures work aligns with strategic goals. 10% weight keeps it influential without overriding tactical priorities.
Example Scoring
Given:
- BL-001: "Implement config parser" (HIGH priority, blocks 2 items, medium complexity, matches "config system" goal)
P = 1.0 (HIGH)
B = 0.67 (blocks 2 of 10 items = 0.2, normalized to ~0.67 assuming 30% threshold)
E = 0.6 (medium complexity)
G = 0.85 (HIGH=1.0 base, +10% goal match, -5% rounded)
Score = (1.0 × 0.40) + (0.67 × 0.30) + (0.6 × 0.20) + (0.85 × 0.10)
= 0.40 + 0.20 + 0.12 + 0.085
= 0.805 × 100
= 80.5/100Confidence Scoring
Rate confidence in recommendation (0.0-1.0):
def estimate_confidence(item: BacklogItem) -> float:
confidence = 0.5 # Neutral baseline
# Detailed description increases confidence
if len(item.description) > 100:
confidence += 0.2
elif len(item.description) > 50:
confidence += 0.1
# Explicit priority (not default MEDIUM)
if item.priority in ["HIGH", "LOW"]:
confidence += 0.1
# Tags provide context
if item.tags:
confidence += 0.1
# Estimated hours explicitly set (not default)
if item.estimated_hours != 4: # 4 is default
confidence += 0.1
return min(confidence, 1.0)Usage: Show confidence to user with recommendations. Low confidence (< 0.6) → ask clarifying questions.
Complexity Estimation
Complexity Categories
- Simple (< 2 hours): Single function, clear requirements, no integration
- Medium (2-6 hours): Multiple functions, some integration, moderate scope
- Complex (> 6 hours): Multiple files, significant integration, large scope
Estimation Algorithm
def estimate_complexity(item: BacklogItem) -> str:
# Start with estimated hours
if item.estimated_hours < 2:
base = "simple"
elif item.estimated_hours <= 6:
base = "medium"
else:
base = "complex"
# Check technical signals
signals = extract_technical_signals(item)
complexity_count = sum(1 for v in signals.values() if v)
# Multiple technical areas increase complexity
if complexity_count >= 3:
if base == "simple":
base = "medium"
elif base == "medium":
base = "complex"
return base
def extract_technical_signals(item: BacklogItem) -> dict:
text = (item.title + " " + item.description).lower()
return {
"has_api_changes": any(kw in text for kw in ["api", "endpoint", "route"]),
"has_db_changes": any(kw in text for kw in ["database", "db", "schema", "migration"]),
"has_ui_changes": any(kw in text for kw in ["ui", "interface", "frontend", "view"]),
"mentions_testing": any(kw in text for kw in ["test", "coverage", "verify"]),
"mentions_security": any(kw in text for kw in ["security", "auth", "permission", "encryption"])
}Category Detection
Categorize items to adjust scoring and delegation:
def categorize_item(item: BacklogItem) -> str:
"""Categorize as feature, bug, refactor, doc, test, or other."""
text = (item.title + " " + item.description).lower()
KEYWORDS = {
"bug": {"fix", "bug", "issue", "error", "broken"},
"test": {"test", "coverage", "verify", "validate"},
"documentation": {"document", "docs", "readme", "comment", "explain"},
"refactor": {"refactor", "clean", "improve", "optimize", "restructure"},
"feature": {"add", "implement", "create", "new", "feature"}
}
for category, keywords in KEYWORDS.items():
if any(kw in text for kw in keywords):
return category
return "other"Dependency Analysis
Dependency Detection
Identify dependencies between backlog items:
def detect_dependencies(item: BacklogItem, all_items: List[BacklogItem]) -> List[str]:
"""Return list of backlog IDs this item depends on."""
dependencies = []
text = (item.title + " " + item.description).lower()
# 1. Explicit ID references (BL-001, BL-002)
import re
id_pattern = r'bl-\d{3}'
matches = re.findall(id_pattern, text, re.IGNORECASE)
dependencies.extend(m.upper() for m in matches if m.upper() in all_items)
# 2. Check for blocking relationships in other items
for other_item in all_items:
if other_item.id == item.id:
continue
other_text = (other_item.title + " " + other_item.description).lower()
# Does other item mention this item as blocking?
if item.id.lower() in other_text:
if any(kw in other_text for kw in ["blocks", "required for", "prerequisite"]):
if other_item.id not in dependencies:
dependencies.append(other_item.id)
return list(set(dependencies)) # Remove duplicatesBlocking Count
Count how many items this item would unblock:
def count_blocking(item: BacklogItem, all_items: List[BacklogItem]) -> int:
"""Count items that depend on this item."""
count = 0
for other_item in all_items:
if other_item.id == item.id:
continue
deps = detect_dependencies(other_item, all_items)
if item.id in deps:
count += 1
return countDependency Validation
Before starting work, verify dependencies are met:
def has_unmet_dependencies(item: BacklogItem, all_items: List[BacklogItem]) -> bool:
"""Check if item has dependencies not yet completed."""
dependencies = detect_dependencies(item, all_items)
for dep_id in dependencies:
dep_item = next((i for i in all_items if i.id == dep_id), None)
if dep_item and dep_item.status != "DONE":
return True
return FalseCoordination Patterns
Concurrent Workstream Management (Phase 3)
Goal: Manage up to 5 concurrent workstreams safely.
Capacity Check
def can_start_workstream(active_count: int, max_concurrent: int = 5) -> tuple[bool, str]:
"""Check if new workstream can be started."""
if active_count >= max_concurrent:
return False, f"Maximum {max_concurrent} concurrent workstreams (currently: {active_count})"
return True, f"Capacity available ({active_count}/{max_concurrent})"Conflict Detection
def detect_conflicts(workstreams: List[WorkstreamState], backlog_items: List[BacklogItem]) -> List[dict]:
"""Detect conflicts between active workstreams."""
conflicts = []
# Check for dependency conflicts
for ws in workstreams:
item = next((i for i in backlog_items if i.id == ws.backlog_id), None)
if not item:
continue
deps = detect_dependencies(item, backlog_items)
# Check if any dependency is also active
for other_ws in workstreams:
if other_ws.id == ws.id:
continue
if other_ws.backlog_id in deps:
conflicts.append({
"type": "dependency",
"workstream": ws.id,
"blocks": other_ws.id,
"reason": f"{ws.id} depends on {other_ws.backlog_id} which is in progress"
})
return conflictsStall Detection
from datetime import datetime, timedelta
def detect_stalled_workstreams(workstreams: List[WorkstreamState], threshold_hours: int = 2) -> List[dict]:
"""Identify workstreams with no progress for threshold period."""
stalled = []
now = datetime.utcnow()
for ws in workstreams:
if ws.status != "RUNNING":
continue
last_activity = datetime.fromisoformat(ws.last_activity.replace("Z", "+00:00"))
hours_idle = (now - last_activity).total_seconds() / 3600
if hours_idle > threshold_hours:
stalled.append({
"workstream": ws.id,
"title": ws.title,
"idle_hours": round(hours_idle, 1),
"recommendation": "Investigate or pause"
})
return stalledResource Allocation
Suggest optimal agent for work based on category:
def suggest_agent(item: BacklogItem) -> str:
"""Suggest best agent for backlog item."""
category = categorize_item(item)
AGENT_MAP = {
"feature": "builder",
"bug": "builder", # Builder can fix and test
"refactor": "optimizer",
"test": "tester",
"documentation": "builder" # Builder handles docs with code
}
return AGENT_MAP.get(category, "builder")Autopilot Decision Logic
Decision Cycle (Phase 4)
Autonomous PM operation when user grants permission:
def autopilot_cycle(state_manager: PMStateManager, dry_run: bool = True) -> dict:
"""Execute one autopilot decision cycle."""
decisions = []
actions = []
# 1. Check active workstreams
active = state_manager.get_active_workstreams()
can_start, reason = state_manager.can_start_workstream()
# 2. Detect stalled workstreams
stalled = detect_stalled_workstreams(active)
for ws in stalled:
decision = {
"type": "pause_stalled",
"workstream": ws["workstream"],
"reason": f"No activity for {ws['idle_hours']} hours",
"confidence": 0.8
}
decisions.append(decision)
if not dry_run:
state_manager.update_workstream(ws["workstream"], status="PAUSED")
actions.append(decision)
# 3. Start new work if capacity available
if can_start:
recommendations = generate_recommendations(state_manager)
if recommendations:
top = recommendations[0]
# Decision rule: Start if HIGH priority and high confidence
if top.backlog_item.priority == "HIGH" and top.confidence > 0.7:
decision = {
"type": "start_work",
"backlog_id": top.backlog_item.id,
"score": top.score,
"rationale": top.rationale,
"confidence": top.confidence
}
decisions.append(decision)
if not dry_run:
agent = suggest_agent(top.backlog_item)
ws = state_manager.create_workstream(top.backlog_item.id, agent)
actions.append(decision)
# 4. Complete finished workstreams
for ws in active:
# Check if workstream is actually complete
# (This would integrate with ClaudeProcess to check completion)
if is_workstream_complete(ws):
decision = {
"type": "complete_work",
"workstream": ws.id,
"backlog_id": ws.backlog_id,
"confidence": 0.9
}
decisions.append(decision)
if not dry_run:
state_manager.complete_workstream(ws.id)
actions.append(decision)
return {
"decisions": decisions,
"actions": actions if not dry_run else [],
"dry_run": dry_run
}Decision Rules
Rule 1: Priority Threshold
- Only auto-start HIGH priority items with confidence > 0.7
- MEDIUM/LOW require explicit user approval
Rule 2: Capacity Management
- Never exceed max concurrent workstreams (default: 5)
- Pause stalled workstreams to free capacity
Rule 3: Dependency Safety
- Never start work with unmet dependencies
- Complete blocking items first
Rule 4: Quality Gates
- Always apply quality checks before marking complete
- Flag quality issues for user review
Logging Decisions
All autopilot decisions must be logged:
# .pm/logs/autopilot_YYYYMMDD_HHMMSS.yaml
timestamp: "2025-11-21T15:30:00Z"
cycle: 42
decisions:
- type: start_work
backlog_id: BL-003
score: 87.5
rationale: HIGH priority, unblocks 2 items, medium complexity
confidence: 0.85
action_taken: true
- type: pause_stalled
workstream: ws-005
reason: No activity for 3.2 hours
confidence: 0.80
action_taken: true
summary: Started 1 workstream, paused 1 stalledLearning Algorithms
Outcome Tracking (Phase 4)
Learn from completed workstreams to improve recommendations:
def record_outcome(ws: WorkstreamState, success: bool, notes: str):
"""Record workstream outcome for learning."""
outcome = {
"workstream_id": ws.id,
"backlog_id": ws.backlog_id,
"agent": ws.agent,
"estimated_hours": ws.backlog_item.estimated_hours,
"actual_minutes": ws.elapsed_minutes,
"complexity": estimate_complexity(ws.backlog_item),
"success": success,
"notes": notes,
"timestamp": datetime.utcnow().isoformat() + "Z"
}
# Append to learning log
log_path = Path(".pm/logs/outcomes.yaml")
outcomes = load_yaml(log_path).get("outcomes", [])
outcomes.append(outcome)
save_yaml(log_path, {"outcomes": outcomes})Estimation Improvement
Use historical data to improve estimates:
def improve_estimate(item: BacklogItem, outcomes: List[dict]) -> int:
"""Improve estimate based on historical outcomes."""
category = categorize_item(item)
complexity = estimate_complexity(item)
# Find similar completed items
similar = [
o for o in outcomes
if o["complexity"] == complexity
and categorize_item_from_id(o["backlog_id"]) == category
and o["success"]
]
if not similar:
return item.estimated_hours # Use original estimate
# Average actual time for similar items
avg_minutes = sum(o["actual_minutes"] for o in similar) / len(similar)
improved_hours = int(avg_minutes / 60) + 1 # Round up
return improved_hoursAgent Performance
Track agent effectiveness by category:
def analyze_agent_performance(outcomes: List[dict]) -> dict:
"""Analyze which agents perform best for which categories."""
perf = {}
for outcome in outcomes:
agent = outcome["agent"]
category = categorize_item_from_id(outcome["backlog_id"])
success = outcome["success"]
key = f"{agent}:{category}"
if key not in perf:
perf[key] = {"successes": 0, "total": 0}
perf[key]["total"] += 1
if success:
perf[key]["successes"] += 1
# Calculate success rates
rates = {}
for key, stats in perf.items():
agent, category = key.split(":")
rate = stats["successes"] / stats["total"]
rates[key] = {
"agent": agent,
"category": category,
"success_rate": round(rate, 2),
"sample_size": stats["total"]
}
return ratesState File Schemas
Config Schema (config.yaml)
project_name: string # Required
project_type: enum # cli-tool, web-service, library, other
primary_goals: list[string] # 3-5 concrete goals
quality_bar: enum # strict, balanced, relaxed
initialized_at: ISO8601 # UTC timestamp with Z suffix
version: string # Schema version (1.0)Backlog Schema (backlog/items.yaml)
items:
- id: string # BL-XXX format (BL-001, BL-002, ...)
title: string # Brief title
description: string # Detailed description
priority: enum # HIGH, MEDIUM, LOW
estimated_hours: int # Estimated effort
status: enum # READY, IN_PROGRESS, DONE, BLOCKED
created_at: ISO8601 # UTC timestamp with Z suffix
tags: list[string] # Optional categorization tagsWorkstream Schema (workstreams/ws-XXX.yaml)
id: string # ws-XXX format (ws-001, ws-002, ...)
backlog_id: string # BL-XXX reference
title: string # Copied from backlog item
status: enum # RUNNING, PAUSED, COMPLETED, FAILED
agent: string # builder, reviewer, tester, etc.
started_at: ISO8601 # UTC timestamp with Z suffix
completed_at: ISO8601 | null # UTC timestamp or null if not complete
process_id: string | null # ClaudeProcess ID if using orchestration
elapsed_minutes: int # Total elapsed time
progress_notes: list[string] # Progress updates
dependencies: list[string] # List of BL-XXX this depends on
last_activity: ISO8601 # Last progress update timestampContext Schema (context.yaml)
project_name: string # Project name
initialized_at: ISO8601 # When PM initialized
version: string # Schema versionIntegration Patterns
ClaudeProcess Integration
from orchestration.claude_process import ClaudeProcess
from pathlib import Path
def start_workstream_with_process(
backlog_id: str,
agent: str,
delegation_package: dict
) -> WorkstreamState:
"""Start workstream using ClaudeProcess orchestration."""
# Create process
process = ClaudeProcess(
agent_path=f".claude/agents/amplihack/core/{agent}.md",
context=delegation_package,
project_root=Path.cwd()
)
# Start execution
process_id = process.start()
# Create workstream tracking
ws = state_manager.create_workstream(
backlog_id=backlog_id,
agent=agent
)
# Link process to workstream
state_manager.update_workstream(
ws.id,
process_id=process_id
)
return wsFile Utility Integration
Use resilient file operations:
from session.file_utils import retry_file_operation, FileOperationError
@retry_file_operation(max_retries=3, delay=0.1)
def save_state(path: Path, data: dict):
"""Save state with retries."""
import yaml
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
yaml.dump(data, f, default_flow_style=False, sort_keys=False)Performance Considerations
Token Efficiency
- SKILL.md: ~2,500 tokens (main guidance)
- REFERENCE.md: ~4,000 tokens (this file, loaded as needed)
- EXAMPLES.md: ~2,000 tokens (loaded as needed)
- Scripts: External Python, no token cost until executed
Total pre-load: ~50 tokens (YAML frontmatter only)
File I/O Optimization
- Batch reads when possible
- Use Read → Edit → Write pattern for updates
- Avoid re-reading unchanged files
- Cache config in memory during operation
Recommendation Generation
- Only score READY items (skip IN_PROGRESS, DONE, BLOCKED)
- Skip items with unmet dependencies early
- Limit to top N recommendations (default: 3)
- Cache complexity estimates for session
Error Handling
Common Error Cases
1. PM Not Initialized: Return helpful error with initialization instructions 2. Backlog Item Not Found: Suggest listing backlog or checking ID 3. Workstream Capacity Full: Explain limit and suggest pausing stalled 4. Unmet Dependencies: List blocking items and their status 5. Invalid YAML: Catch parse errors, suggest schema validation
Recovery Strategies
def safe_load_backlog(state_manager: PMStateManager) -> List[BacklogItem]:
"""Load backlog with error recovery."""
try:
return state_manager.get_backlog_items()
except FileNotFoundError:
# Backlog file missing - create empty
state_manager._write_yaml(
state_manager.pm_dir / "backlog" / "items.yaml",
{"items": []}
)
return []
except yaml.YAMLError as e:
# Invalid YAML - report and suggest fix
raise ValueError(f"Backlog file corrupted: {e}. Check .pm/backlog/items.yaml")Philosophy Compliance
Ruthless Simplicity
- File-based state (no database)
- YAML for human readability
- Simple scoring formulas
- Standard library only (Python)
Single Responsibility
- PM Architect: coordination and prioritization
- Coding agents: implementation
- ClaudeProcess: orchestration
- Scripts: complex calculations
Zero-BS Implementation
- All formulas are working (no stubs)
- All state files are valid YAML
- All recommendations have clear rationale
- All decisions are logged
Trust in Emergence
- User controls when to delegate
- User approves autonomous actions
- PM suggests, user directs
- Learning adapts to outcomes
---
This reference provides complete technical specifications for PM Architect operations. Use it when you need detailed algorithm logic, precise scoring formulas, or implementation patterns.
#!/usr/bin/env python3
"""Batch process backlog items with resumable state tracking.
Pattern: Amplifier P2 - Iterative Status Tracking for Batch Operations
Handles 100+ items with state persistence and resume capability.
This is a library module providing the BatchProcessor class.
Import and use in your own scripts with a real processor function.
Library Usage:
from batch_process import BatchProcessor
def my_processor(item):
# Your actual processing logic here
return {"result": process(item)}
processor = BatchProcessor(Path.cwd(), "my_processor")
results = processor.process_items(items, my_processor, batch_size=10)
CLI Usage (status/reset only):
python batch_process.py --status [--processor NAME]
python batch_process.py --reset --processor NAME
"""
import argparse
import json
import sys
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
def load_yaml(path: Path) -> dict[str, Any]:
"""Load YAML file safely."""
if not path.exists():
return {}
with open(path) as f:
return yaml.safe_load(f) or {}
def save_yaml(path: Path, data: dict[str, Any]) -> None:
"""Save YAML file."""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
def get_timestamp() -> str:
"""Get current UTC timestamp."""
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
class BatchProcessor:
"""Process large collections with state tracking and resumption."""
def __init__(self, project_root: Path, processor_name: str):
"""Initialize batch processor.
Args:
project_root: Project root directory
processor_name: Name of processing script (e.g., "analyze_backlog")
"""
self.project_root = project_root
self.pm_dir = project_root / ".pm"
self.processor_name = processor_name
self.status_file = self.pm_dir / f"batch_status_{processor_name}.yaml"
self.state = self.load_state()
def load_state(self) -> dict:
"""Load batch processing state."""
if self.status_file.exists():
return load_yaml(self.status_file)
return {
"processor": self.processor_name,
"started_at": get_timestamp(),
"last_run": None,
"processed": [],
"failed": [],
"last_id": None,
"total_processed": 0,
"total_failed": 0,
"completed": False,
}
def save_state(self):
"""Persist state after each item (critical for resumption)."""
self.state["last_run"] = get_timestamp()
save_yaml(self.status_file, self.state)
def is_processed(self, item_id: str) -> bool:
"""Check if item already processed."""
return item_id in self.state["processed"] or item_id in [
f["id"] for f in self.state["failed"]
]
def mark_processed(self, item_id: str, result: dict = None):
"""Mark item as successfully processed."""
if item_id not in self.state["processed"]:
self.state["processed"].append(item_id)
self.state["last_id"] = item_id
self.state["total_processed"] += 1
self.save_state() # Save after EACH item
def mark_failed(self, item_id: str, error: str):
"""Mark item as failed with error details."""
failure = {"id": item_id, "error": error, "timestamp": get_timestamp()}
# Update or add failure
existing = next((f for f in self.state["failed"] if f["id"] == item_id), None)
if existing:
existing["error"] = error
existing["timestamp"] = failure["timestamp"]
else:
self.state["failed"].append(failure)
self.state["last_id"] = item_id
self.state["total_failed"] += 1
self.save_state() # Save after EACH failure
def process_items(
self, items: list[dict], processor_func: Callable[[dict], dict], batch_size: int = 10
) -> dict:
"""Process items in batches with state tracking.
Args:
items: List of items to process
processor_func: Function that processes single item
batch_size: Number of items per progress report
Returns:
Summary of processing results
"""
results = {"succeeded": [], "failed": [], "skipped": []}
total = len(items)
processed_count = 0
for i, item in enumerate(items):
item_id = item.get("id", f"item-{i}")
# Skip already processed
if self.is_processed(item_id):
results["skipped"].append(item_id)
continue
try:
# Process single item
result = processor_func(item)
self.mark_processed(item_id, result)
results["succeeded"].append({"id": item_id, "result": result})
processed_count += 1
# Progress report every batch_size items
if processed_count % batch_size == 0:
progress = (i + 1) / total * 100
print(
f"Progress: {i + 1}/{total} ({progress:.1f}%) - "
f"{processed_count} processed, {len(results['failed'])} failed",
file=sys.stderr,
)
except Exception as e:
error_msg = str(e)
self.mark_failed(item_id, error_msg)
results["failed"].append({"id": item_id, "error": error_msg})
# Mark as completed
self.state["completed"] = True
self.save_state()
return {
"total_items": total,
"processed": len(results["succeeded"]),
"failed": len(results["failed"]),
"skipped": len(results["skipped"]),
"results": results,
"state_file": str(self.status_file),
}
def reset_state(self):
"""Reset batch processing state (for retry)."""
if self.status_file.exists():
self.status_file.unlink()
self.state = self.load_state()
def get_progress(self) -> dict:
"""Get current progress information."""
return {
"processor": self.processor_name,
"started_at": self.state.get("started_at"),
"last_run": self.state.get("last_run"),
"total_processed": self.state["total_processed"],
"total_failed": self.state["total_failed"],
"completed": self.state.get("completed", False),
"last_id": self.state.get("last_id"),
}
def main():
"""Main entry point for CLI usage."""
parser = argparse.ArgumentParser(
description="Batch process items with resumable state tracking"
)
parser.add_argument(
"--project-root", type=Path, default=Path.cwd(), help="Project root directory"
)
parser.add_argument(
"--processor", required=True, help="Processor name (e.g., 'analyze_backlog')"
)
parser.add_argument("--batch-size", type=int, default=10, help="Progress report interval")
parser.add_argument("--reset", action="store_true", help="Reset state and start fresh")
parser.add_argument("--status", action="store_true", help="Show current progress")
args = parser.parse_args()
try:
processor = BatchProcessor(args.project_root, args.processor)
if args.reset:
processor.reset_state()
print(json.dumps({"status": "reset", "processor": args.processor}))
return 0
if args.status:
progress = processor.get_progress()
print(json.dumps(progress, indent=2))
return 0
# This script provides the BatchProcessor class for use by other scripts
# It is not meant to be run directly - import BatchProcessor and provide
# your own processor function that implements real business logic
print(
json.dumps(
{
"error": "batch_process.py is a library module, not a standalone script",
"usage": "Import BatchProcessor class and provide your own processor function",
"example": "from batch_process import BatchProcessor; processor = BatchProcessor(...)",
}
),
file=sys.stderr,
)
return 1
except Exception as e:
print(json.dumps({"error": str(e)}), file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
PM Architect - Label-Triggered Delegation.
Handles pm:delegate label by preparing context and spawning auto mode
to generate response for issues or PRs.
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
def get_issue_pr_details(project_root: Path, number: int, item_type: str) -> dict | None:
"""Get issue or PR details using gh CLI.
Args:
project_root: Project root directory
number: Issue or PR number
item_type: Either 'issue' or 'pr'
Returns:
Dictionary with details, or None if retrieval fails
"""
try:
cmd = ["gh", item_type, "view", str(number), "--json", "number,title,author,body,comments"]
result = subprocess.run(cmd, cwd=project_root, capture_output=True, text=True, timeout=15)
if result.returncode == 0:
return json.loads(result.stdout)
print(f"Error: gh {item_type} view failed: {result.stderr}", file=sys.stderr)
return None
except Exception as e:
print(f"Error retrieving {item_type} details: {e}", file=sys.stderr)
return None
def prepare_delegation_prompt(details: dict, number: int, item_type: str) -> str:
"""Prepare delegation prompt from issue/PR details.
Args:
details: Issue/PR details from GitHub
number: Issue/PR number
item_type: 'issue' or 'pr'
Returns:
Formatted delegation prompt
"""
title = details.get("title", "")
body = details.get("body", "") or "(No description provided)"
comments = details.get("comments", [])
latest_comment = comments[-1]["body"] if comments else None
prompt = f"""PM Architect Delegation Request
**Context**: {item_type.upper()} #{number}
**Title**: {title}
**Description**:
{body}
"""
if latest_comment:
prompt += f"""
**Latest Comment**:
{latest_comment}
"""
prompt += """
**Task**: Analyze this request and provide a comprehensive response. Consider:
1. What is being requested?
2. What information or action is needed?
3. What is the best approach to address this?
4. Are there any blockers or dependencies?
5. What are the next steps?
Provide a clear, actionable response that addresses the request.
"""
return prompt
def run_auto_mode_delegation(
prompt: str, project_root: Path, max_turns: int = 5
) -> tuple[bool, str]:
"""Run amplihack auto mode with delegation prompt.
Args:
prompt: Delegation prompt to execute
project_root: Project root directory
max_turns: Maximum auto mode turns
Returns:
Tuple of (success: bool, output: str)
"""
try:
# Run amplihack auto mode with the active agent binary
agent_binary = os.environ.get("AMPLIHACK_AGENT_BINARY") or "claude"
cmd = ["amplihack", agent_binary, "--auto", "--max-turns", str(max_turns), "--", "-p", prompt]
result = subprocess.run(
cmd,
cwd=project_root,
capture_output=True,
text=True,
timeout=600, # 10 min timeout
)
# Auto mode may return non-zero but still produce output
output = result.stdout + result.stderr
# Check if output looks reasonable
if len(output) > 100: # At least some content
return True, output
return False, f"Auto mode produced insufficient output:\n{output}"
except subprocess.TimeoutExpired:
return False, "Auto mode execution timed out (10 minutes)"
except Exception as e:
return False, f"Auto mode execution failed: {e}"
def format_response_for_github(output: str) -> str:
"""Format auto mode output for GitHub comment.
Args:
output: Raw auto mode output
Returns:
Formatted markdown for GitHub comment
"""
# Add header
formatted = """## 🤖 PM Architect Delegation Response
*This response was generated automatically by PM Architect via the `pm:delegate` label.*
---
"""
# Add the output (take last reasonable portion to avoid noise)
lines = output.split("\n")
# Try to find the start of meaningful content (skip initialization)
start_idx = 0
for i, line in enumerate(lines):
if "AUTONOMOUS MODE" in line or "Auto mode" in line:
start_idx = i
break
# Take from meaningful start, limit to last 200 lines max
meaningful_lines = lines[start_idx:]
if len(meaningful_lines) > 200:
meaningful_lines = meaningful_lines[-200:]
formatted += "\n".join(meaningful_lines)
# Add footer
formatted += """
---
*To continue the conversation or provide feedback, reply to this comment or use the `pm:delegate` label again.*
"""
return formatted
def main():
"""Main execution."""
parser = argparse.ArgumentParser(description="PM Architect Label-Triggered Delegation")
parser.add_argument("number", type=int, help="Issue or PR number")
parser.add_argument("type", choices=["issue", "pr"], help="Type: issue or pr")
parser.add_argument("--project-root", type=Path, default=Path.cwd(), help="Project root")
parser.add_argument("--output", type=Path, required=True, help="Output file for response")
parser.add_argument("--max-turns", type=int, default=5, help="Max auto mode turns")
args = parser.parse_args()
# Get issue/PR details
print(f"Fetching {args.type} #{args.number} details...")
details = get_issue_pr_details(args.project_root, args.number, args.type)
if not details:
print(f"Error: Could not fetch {args.type} details", file=sys.stderr)
sys.exit(1)
# Prepare delegation prompt
print("Preparing delegation prompt...")
prompt = prepare_delegation_prompt(details, args.number, args.type)
# Run auto mode
print(f"Running auto mode delegation (max {args.max_turns} turns)...")
success, output = run_auto_mode_delegation(prompt, args.project_root, args.max_turns)
if not success:
print(f"Error: Auto mode failed: {output}", file=sys.stderr)
# Write error response
error_response = f"""## ❌ PM Architect Delegation Failed
The PM Architect delegation encountered an error:
```
{output}
```
Please check the workflow logs for more details or try again.
"""
args.output.write_text(error_response)
sys.exit(1)
# Format and save response
print("Formatting response for GitHub...")
formatted_response = format_response_for_github(output)
args.output.write_text(formatted_response)
print(f"Delegation response written to {args.output}")
print("Success!")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
PM Architect - Daily Status Report Generation using Claude Agent SDK.
Uses Claude Agent SDK to analyze project state and generate comprehensive
daily status reports with insights, blockers, and recommendations.
"""
import asyncio
import json
import os
import sys
from pathlib import Path
# Unset CLAUDECODE to prevent nested session errors when spawning Claude CLI subprocesses
os.environ.pop("CLAUDECODE", None)
# Try to import Claude SDK
try:
from claude_agent_sdk import ClaudeAgentOptions, query
CLAUDE_SDK_AVAILABLE = True
except ImportError:
CLAUDE_SDK_AVAILABLE = False
def load_project_state(project_root: Path) -> dict | None:
"""Load PM state files for analysis.
Args:
project_root: Project root directory
Returns:
Dictionary with backlog, workstreams, and metrics, or None if not found
"""
pm_state_dir = project_root / ".claude" / "pm_state"
if not pm_state_dir.exists():
return None
state = {}
# Load backlog
backlog_file = pm_state_dir / "backlog.yaml"
if backlog_file.exists():
import yaml
with open(backlog_file) as f:
state["backlog"] = yaml.safe_load(f)
# Load workstreams
workstreams_file = pm_state_dir / "workstreams.yaml"
if workstreams_file.exists():
import yaml
with open(workstreams_file) as f:
state["workstreams"] = yaml.safe_load(f)
# Load project config
config_file = pm_state_dir / "project_config.yaml"
if config_file.exists():
import yaml
with open(config_file) as f:
state["config"] = yaml.safe_load(f)
return state if state else None
def get_recent_git_activity(project_root: Path) -> str:
"""Get recent git commits and branch activity.
Args:
project_root: Project root directory
Returns:
Formatted string with recent git activity
"""
import subprocess
try:
# Get recent commits (last 24 hours)
result = subprocess.run(
[
"git",
"log",
"--oneline",
"--since=24 hours ago",
"--all",
"--decorate",
"--max-count=20",
],
cwd=project_root,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0 and result.stdout.strip():
commits = result.stdout.strip()
return f"""
## Recent Git Activity (Last 24 Hours)
```
{commits}
```
"""
return "\n## Recent Git Activity\n\nNo commits in the last 24 hours.\n"
except Exception as e:
return f"\n## Recent Git Activity\n\nUnable to retrieve: {e}\n"
def get_open_prs_and_issues(project_root: Path) -> str:
"""Get count of open PRs and issues.
Args:
project_root: Project root directory
Returns:
Formatted string with PR/issue counts
"""
import subprocess
try:
# Try to use gh CLI
result = subprocess.run(
["gh", "pr", "list", "--json", "number,title,state"],
cwd=project_root,
capture_output=True,
text=True,
timeout=10,
)
prs = []
if result.returncode == 0:
prs = json.loads(result.stdout)
result = subprocess.run(
["gh", "issue", "list", "--json", "number,title,state"],
cwd=project_root,
capture_output=True,
text=True,
timeout=10,
)
issues = []
if result.returncode == 0:
issues = json.loads(result.stdout)
return f"""
## Open PRs and Issues
- **Open PRs**: {len(prs)}
- **Open Issues**: {len(issues)}
"""
except Exception:
return "\n## Open PRs and Issues\n\nUnable to retrieve (gh CLI not available)\n"
async def generate_status_report(project_root: Path, state: dict | None = None) -> str | None:
"""Generate daily status report using Claude Agent SDK.
Args:
project_root: Project root directory
state: Optional pre-loaded project state
Returns:
Markdown status report, or None if generation fails
"""
if not CLAUDE_SDK_AVAILABLE:
print("Error: Claude SDK not available", file=sys.stderr)
return None
# Load state if not provided
if state is None:
state = load_project_state(project_root)
# Gather context
git_activity = get_recent_git_activity(project_root)
prs_issues = get_open_prs_and_issues(project_root)
# Format state for analysis
state_context = ""
if state:
state_context = f"""
## Current Project State
### Backlog Summary
{json.dumps(state.get("backlog", {}), indent=2)}
### Active Workstreams
{json.dumps(state.get("workstreams", {}), indent=2)}
### Project Configuration
{json.dumps(state.get("config", {}), indent=2)}
"""
else:
state_context = "\n## Current Project State\n\nNo PM state files found. This may be a new project or PM Architect has not been initialized.\n"
# Build comprehensive prompt
prompt = f"""You are the PM Architect analyzing project status for a daily status report.
{state_context}
{git_activity}
{prs_issues}
## Task
Generate a comprehensive daily status report in markdown format with the following sections:
1. **Executive Summary** (2-3 sentences)
- Overall project health
- Key accomplishments in last 24 hours
- Critical issues requiring attention
2. **Workstream Status**
- List each active workstream with status (RUNNING/BLOCKED/COMPLETED)
- Progress indicators
- Blockers or risks
3. **Backlog Health**
- Number of items by priority (HIGH/MEDIUM/LOW)
- Items marked as READY vs BLOCKED
- Recommended focus areas
4. **Velocity and Metrics**
- Items completed in last 24 hours
- Items added to backlog
- Trend analysis (velocity increasing/stable/declining)
5. **Blockers and Risks**
- Critical blockers requiring immediate attention
- Risk items that could impact delivery
- Dependencies waiting on external factors
6. **Recommendations**
- Top 3 priorities for the next 24 hours
- Suggested actions for PM or team
- Items that should be escalated
7. **Next Review**
- Key milestones to track
- Expected deliverables in next 24 hours
Use markdown formatting with clear headers, bullet points, and emphasis where appropriate.
Be concise but informative. Use emojis sparingly for visual clarity (✅, ⚠️, 🚫, 📊).
Generate the report now:
"""
try:
# Configure SDK
options = ClaudeAgentOptions(
cwd=str(project_root),
permission_mode="bypassPermissions",
)
# Collect response
response_parts = []
async for message in query(prompt=prompt, options=options):
if hasattr(message, "text"):
response_parts.append(message.text)
elif hasattr(message, "content"):
response_parts.append(str(message.content))
# Join all parts
report = "".join(response_parts)
return report if report.strip() else None
except Exception as e:
print(f"Error generating status report: {e}", file=sys.stderr)
return None
def main():
"""Main entry point for daily status generation."""
import argparse
parser = argparse.ArgumentParser(
description="Generate daily PM status report using Claude Agent SDK"
)
parser.add_argument(
"--project-root",
type=Path,
default=Path.cwd(),
help="Project root directory (default: current directory)",
)
parser.add_argument(
"--output",
type=Path,
help="Output file path (default: stdout)",
)
args = parser.parse_args()
if not CLAUDE_SDK_AVAILABLE:
print("Error: claude-agent-sdk not installed", file=sys.stderr)
print("Install with: pip install claude-agent-sdk", file=sys.stderr)
sys.exit(1)
# Generate report
report = asyncio.run(generate_status_report(args.project_root))
if not report:
print("Error: Failed to generate status report", file=sys.stderr)
sys.exit(1)
# Output report
if args.output:
args.output.write_text(report)
print(f"Status report written to {args.output}", file=sys.stderr)
else:
print(report)
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
PM Architect - Weekly Roadmap Review using Claude Agent SDK.
Uses Claude Agent SDK to analyze roadmap alignment, goal progress, and
generate strategic recommendations for project direction.
"""
import asyncio
import json
import os
import sys
from pathlib import Path
# Unset CLAUDECODE to prevent nested session errors when spawning Claude CLI subprocesses
os.environ.pop("CLAUDECODE", None)
# Try to import Claude SDK
try:
from claude_agent_sdk import ClaudeAgentOptions, query
CLAUDE_SDK_AVAILABLE = True
except ImportError:
CLAUDE_SDK_AVAILABLE = False
def load_project_state(project_root: Path) -> dict | None:
"""Load PM state files for analysis.
Args:
project_root: Project root directory
Returns:
Dictionary with backlog, workstreams, and metrics, or None if not found
"""
pm_state_dir = project_root / ".claude" / "pm_state"
if not pm_state_dir.exists():
return None
state = {}
# Load backlog
backlog_file = pm_state_dir / "backlog.yaml"
if backlog_file.exists():
import yaml
with open(backlog_file) as f:
state["backlog"] = yaml.safe_load(f)
# Load workstreams
workstreams_file = pm_state_dir / "workstreams.yaml"
if workstreams_file.exists():
import yaml
with open(workstreams_file) as f:
state["workstreams"] = yaml.safe_load(f)
# Load project config
config_file = pm_state_dir / "project_config.yaml"
if config_file.exists():
import yaml
with open(config_file) as f:
state["config"] = yaml.safe_load(f)
return state if state else None
def get_git_velocity_metrics(project_root: Path) -> str:
"""Get git-based velocity metrics (commits, PRs merged).
Args:
project_root: Project root directory
Returns:
Formatted string with velocity metrics
"""
import subprocess
from datetime import datetime, timedelta
try:
# Get commits in last 7 days
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
result = subprocess.run(
[
"git",
"log",
"--oneline",
f"--since={week_ago}",
"--all",
],
cwd=project_root,
capture_output=True,
text=True,
timeout=10,
)
commit_count = 0
if result.returncode == 0:
commit_count = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0
# Get merged PRs in last 7 days using gh CLI
pr_count = 0
try:
pr_result = subprocess.run(
[
"gh",
"pr",
"list",
"--state",
"merged",
"--json",
"number,mergedAt",
"--limit",
"100",
],
cwd=project_root,
capture_output=True,
text=True,
timeout=10,
)
if pr_result.returncode == 0:
prs = json.loads(pr_result.stdout)
# Count PRs merged in last 7 days
week_ago_dt = datetime.now() - timedelta(days=7)
pr_count = sum(
1
for pr in prs
if pr.get("mergedAt")
and datetime.fromisoformat(pr["mergedAt"].replace("Z", "+00:00")) > week_ago_dt
)
except Exception:
pass
return f"""
## Velocity Metrics (Last 7 Days)
- **Commits**: {commit_count}
- **PRs Merged**: {pr_count}
- **Daily Average**: {commit_count / 7:.1f} commits/day
"""
except Exception as e:
return f"\n## Velocity Metrics\n\nUnable to retrieve: {e}\n"
def get_milestone_progress(project_root: Path) -> str:
"""Get milestone and project progress.
Args:
project_root: Project root directory
Returns:
Formatted string with milestone progress
"""
import subprocess
try:
# Get milestones using gh CLI
result = subprocess.run(
["gh", "api", "repos/:owner/:repo/milestones", "--jq", "."],
cwd=project_root,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
milestones = json.loads(result.stdout)
if milestones:
milestone_info = []
for milestone in milestones[:5]: # Limit to 5 most recent
title = milestone.get("title", "Unknown")
open_issues = milestone.get("open_issues", 0)
closed_issues = milestone.get("closed_issues", 0)
total = open_issues + closed_issues
progress = (closed_issues / total * 100) if total > 0 else 0
milestone_info.append(
f"- **{title}**: {closed_issues}/{total} ({progress:.0f}%)"
)
return f"""
## Milestone Progress
{chr(10).join(milestone_info)}
"""
return "\n## Milestone Progress\n\nNo active milestones found.\n"
except Exception:
return "\n## Milestone Progress\n\nUnable to retrieve (gh CLI not available)\n"
async def generate_roadmap_review(project_root: Path, state: dict | None = None) -> str | None:
"""Generate weekly roadmap review using Claude Agent SDK.
Args:
project_root: Project root directory
state: Optional pre-loaded project state
Returns:
Markdown roadmap review, or None if generation fails
"""
if not CLAUDE_SDK_AVAILABLE:
print("Error: Claude SDK not available", file=sys.stderr)
return None
# Load state if not provided
if state is None:
state = load_project_state(project_root)
# Gather context
velocity_metrics = get_git_velocity_metrics(project_root)
milestone_progress = get_milestone_progress(project_root)
# Format state for analysis
state_context = ""
if state:
config = state.get("config", {})
goals = config.get("goals", [])
state_context = f"""
## Project Configuration
**Project Name**: {config.get("name", "Unknown")}
**Project Type**: {config.get("type", "Unknown")}
**Quality Bar**: {config.get("quality_bar", "Unknown")}
**Project Goals**:
{chr(10).join(f"- {goal}" for goal in goals) if goals else "- No goals defined"}
## Current Project State
### Backlog Summary
{json.dumps(state.get("backlog", {}), indent=2)}
### Active Workstreams
{json.dumps(state.get("workstreams", {}), indent=2)}
"""
else:
state_context = "\n## Current Project State\n\nNo PM state files found. This may be a new project or PM Architect has not been initialized.\n"
# Build comprehensive prompt
from datetime import datetime
week_num = datetime.now().strftime("%Y-W%V")
prompt = f"""You are the PM Architect performing a strategic weekly roadmap review.
{state_context}
{velocity_metrics}
{milestone_progress}
## Task
Generate a comprehensive weekly roadmap review in markdown format for **Week {week_num}** with the following sections:
1. **Executive Summary** (3-4 sentences)
- Overall strategic health of the project
- Alignment with project goals
- Key achievements this week
- Critical strategic concerns
2. **Goal Progress Analysis**
- For each project goal, assess:
- Current status (On Track / At Risk / Blocked)
- Progress made this week
- Remaining work estimate
- Blockers or dependencies
- If no goals defined, recommend creating strategic goals
3. **Velocity and Capacity Analysis**
- Analyze velocity trends (improving/stable/declining)
- Compare actual vs planned velocity
- Team capacity assessment
- Bottleneck identification
- Recommendations for velocity improvement
4. **Roadmap Alignment**
- Are current workstreams aligned with project goals?
- Are we working on the right things?
- Should priorities be adjusted?
- Recommended focus shifts
5. **Strategic Risks and Opportunities**
- Technical debt accumulation
- Dependencies on external factors
- Market or competitive considerations
- Opportunities to accelerate delivery
- Resource constraints
6. **Milestone and Release Planning**
- Progress toward next milestone/release
- Are we on track for commitments?
- Should milestone dates be adjusted?
- Recommended scope adjustments
7. **Recommendations for Next Week**
- Top 3-5 strategic priorities
- Backlog grooming needs
- Team focus areas
- Process improvements
- Items requiring stakeholder decisions
8. **Long-term Outlook** (2-4 weeks ahead)
- Upcoming challenges or opportunities
- Planning needs
- Resource requirements
Use markdown formatting with clear headers, bullet points, and emphasis where appropriate.
Be strategic and forward-looking. Use emojis sparingly for visual clarity (✅, ⚠️, 🚫, 📊, 🎯, 🚀).
Generate the roadmap review now:
"""
try:
# Configure SDK
options = ClaudeAgentOptions(
cwd=str(project_root),
permission_mode="bypassPermissions",
)
# Collect response
response_parts = []
async for message in query(prompt=prompt, options=options):
if hasattr(message, "text"):
response_parts.append(message.text)
elif hasattr(message, "content"):
response_parts.append(str(message.content))
# Join all parts
review = "".join(response_parts)
return review if review.strip() else None
except Exception as e:
print(f"Error generating roadmap review: {e}", file=sys.stderr)
return None
def main():
"""Main entry point for roadmap review generation."""
import argparse
parser = argparse.ArgumentParser(
description="Generate weekly roadmap review using Claude Agent SDK"
)
parser.add_argument(
"--project-root",
type=Path,
default=Path.cwd(),
help="Project root directory (default: current directory)",
)
parser.add_argument(
"--output",
type=Path,
help="Output file path (default: stdout)",
)
args = parser.parse_args()
if not CLAUDE_SDK_AVAILABLE:
print("Error: claude-agent-sdk not installed", file=sys.stderr)
print("Install with: pip install claude-agent-sdk", file=sys.stderr)
sys.exit(1)
# Generate review
review = asyncio.run(generate_roadmap_review(args.project_root))
if not review:
print("Error: Failed to generate roadmap review", file=sys.stderr)
sys.exit(1)
# Output review
if args.output:
args.output.write_text(review)
print(f"Roadmap review written to {args.output}", file=sys.stderr)
else:
print(review)
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Aggregate priorities across GitHub accounts into a strict Top 5 ranked list.
Queries GitHub issues and PRs across configured accounts/repos, scores them
by priority labels, staleness, blocking status, and roadmap alignment.
Falls back to .pm/ YAML state if GitHub is unavailable or for enrichment.
Usage:
python generate_top5.py [--project-root PATH] [--sources PATH]
Returns JSON with top 5 priorities.
"""
import argparse
import json
import subprocess
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
# Aggregation weights
WEIGHT_ISSUES = 0.40
WEIGHT_PRS = 0.30
WEIGHT_ROADMAP = 0.20
WEIGHT_LOCAL = 0.10 # .pm/ overrides
TOP_N = 5
# Label-to-priority mapping
PRIORITY_LABELS = {
"critical": 1.0,
"priority:critical": 1.0,
"high": 0.9,
"priority:high": 0.9,
"bug": 0.8,
"medium": 0.6,
"priority:medium": 0.6,
"enhancement": 0.5,
"feature": 0.5,
"low": 0.3,
"priority:low": 0.3,
}
def load_yaml(path: Path) -> dict[str, Any]:
"""Load YAML file safely."""
if not path.exists():
return {}
with open(path) as f:
return yaml.safe_load(f) or {}
def load_sources(sources_path: Path) -> list[dict]:
"""Load GitHub source configuration."""
data = load_yaml(sources_path)
return data.get("github", [])
def run_gh(args: list[str], account: str | None = None) -> str | None:
"""Run a gh CLI command, optionally switching account first.
Returns stdout on success, None on failure.
"""
if account:
switch = subprocess.run(
["gh", "auth", "switch", "--user", account],
capture_output=True, text=True, timeout=10,
)
if switch.returncode != 0:
return None
try:
result = subprocess.run(
["gh"] + args,
capture_output=True, text=True, timeout=30,
)
if result.returncode != 0:
return None
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
return None
def get_current_gh_account() -> str | None:
"""Get the currently active gh account."""
try:
result = subprocess.run(
["gh", "api", "user", "--jq", ".login"],
capture_output=True, text=True, timeout=10,
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return None
def fetch_github_issues(account: str, repos: list[str]) -> list[dict]:
"""Fetch open issues for an account's repos from GitHub."""
candidates = []
# Use search API to get all issues at once
repo_qualifiers = " ".join(f"repo:{r}" if "/" in r else f"repo:{account}/{r}" for r in repos)
query = f"is:open is:issue {repo_qualifiers}"
jq_filter = (
'.items[] | {'
'repo: (.repository_url | split("/") | .[-2:] | join("/")),'
'title: .title,'
'labels: [.labels[].name],'
'created: .created_at,'
'updated: .updated_at,'
'number: .number,'
'comments: .comments'
'}'
)
output = run_gh(
["api", "search/issues", "--method", "GET",
"-f", f"q={query}", "-f", "per_page=50",
"--jq", jq_filter],
account=account,
)
if not output:
return []
now = datetime.now(UTC)
for line in output.strip().splitlines():
try:
item = json.loads(line)
except json.JSONDecodeError:
continue
# Score by labels
labels = [lbl.lower() for lbl in item.get("labels", [])]
priority_score = 0.5 # default
for label in labels:
if label in PRIORITY_LABELS:
priority_score = max(priority_score, PRIORITY_LABELS[label])
# Staleness boost: older updated = needs attention
try:
updated = datetime.fromisoformat(item["updated"].replace("Z", "+00:00"))
days_stale = (now - updated).total_seconds() / 86400
except (ValueError, KeyError):
days_stale = 0
staleness_score = min(days_stale / 14.0, 1.0) # Max at 2 weeks
# Comment activity: more comments = more discussion = potentially blocked
comments = item.get("comments", 0)
activity_score = min(comments / 10.0, 1.0)
raw_score = (priority_score * 0.50 + staleness_score * 0.30 + activity_score * 0.20) * 100
# Rationale
reasons = []
if priority_score >= 0.8:
reasons.append(f"labeled {', '.join(lbl for lbl in labels if lbl in PRIORITY_LABELS)}")
if days_stale > 7:
reasons.append(f"stale {days_stale:.0f}d")
if comments > 3:
reasons.append(f"{comments} comments")
if not reasons:
reasons.append("open issue")
repo = item.get("repo", "")
candidates.append({
"title": item["title"],
"source": "github_issue",
"raw_score": round(raw_score, 1),
"score_breakdown": {
"label_priority": round(priority_score, 2),
"staleness": round(staleness_score, 2),
"activity": round(activity_score, 2),
},
"rationale": ", ".join(reasons),
"item_id": f"{repo}#{item['number']}",
"priority": "HIGH" if priority_score >= 0.8 else "MEDIUM" if priority_score >= 0.5 else "LOW",
"repo": repo,
"account": account,
"url": f"https://github.com/{repo}/issues/{item['number']}",
"labels": item.get("labels", []),
"created": item.get("created", ""),
"updated": item.get("updated", ""),
"days_stale": round(days_stale, 1),
"comments": comments,
})
return candidates
def fetch_github_prs(account: str, repos: list[str]) -> list[dict]:
"""Fetch open PRs for an account's repos from GitHub."""
candidates = []
repo_qualifiers = " ".join(f"repo:{r}" if "/" in r else f"repo:{account}/{r}" for r in repos)
query = f"is:open is:pr {repo_qualifiers}"
jq_filter = (
'.items[] | {'
'repo: (.repository_url | split("/") | .[-2:] | join("/")),'
'title: .title,'
'labels: [.labels[].name],'
'created: .created_at,'
'updated: .updated_at,'
'number: .number,'
'draft: .draft,'
'comments: .comments'
'}'
)
output = run_gh(
["api", "search/issues", "--method", "GET",
"-f", f"q={query}", "-f", "per_page=50",
"--jq", jq_filter],
account=account,
)
if not output:
return []
now = datetime.now(UTC)
for line in output.strip().splitlines():
try:
item = json.loads(line)
except json.JSONDecodeError:
continue
is_draft = item.get("draft", False)
# PRs waiting for review are higher priority than drafts
base_score = 0.4 if is_draft else 0.7
# Labels boost
labels = [lbl.lower() for lbl in item.get("labels", [])]
for label in labels:
if label in PRIORITY_LABELS:
base_score = max(base_score, PRIORITY_LABELS[label])
# Staleness: PRs waiting for review get more urgent over time
try:
updated = datetime.fromisoformat(item["updated"].replace("Z", "+00:00"))
days_stale = (now - updated).total_seconds() / 86400
except (ValueError, KeyError):
days_stale = 0
staleness_score = min(days_stale / 7.0, 1.0) # PRs stale faster (1 week max)
raw_score = (base_score * 0.60 + staleness_score * 0.40) * 100
reasons = []
if is_draft:
reasons.append("draft PR")
else:
reasons.append("awaiting review")
if days_stale > 3:
reasons.append(f"stale {days_stale:.0f}d")
if labels:
relevant = [lbl for lbl in labels if lbl in PRIORITY_LABELS]
if relevant:
reasons.append(f"labeled {', '.join(relevant)}")
repo = item.get("repo", "")
candidates.append({
"title": item["title"],
"source": "github_pr",
"raw_score": round(raw_score, 1),
"score_breakdown": {
"base_priority": round(base_score, 2),
"staleness": round(staleness_score, 2),
},
"rationale": ", ".join(reasons),
"item_id": f"{repo}#{item['number']}",
"priority": "HIGH" if base_score >= 0.8 else "MEDIUM",
"repo": repo,
"account": account,
"url": f"https://github.com/{repo}/pull/{item['number']}",
"labels": item.get("labels", []),
"created": item.get("created", ""),
"updated": item.get("updated", ""),
"days_stale": round(days_stale, 1),
"is_draft": is_draft,
})
return candidates
def load_local_overrides(pm_dir: Path) -> list[dict]:
"""Load manually-added items from .pm/backlog for local enrichment."""
backlog_data = load_yaml(pm_dir / "backlog" / "items.yaml")
items = backlog_data.get("items", [])
ready_items = [item for item in items if item.get("status") == "READY"]
candidates = []
priority_map = {"HIGH": 1.0, "MEDIUM": 0.6, "LOW": 0.3}
for item in ready_items:
priority = item.get("priority", "MEDIUM")
priority_score = priority_map.get(priority, 0.5)
hours = item.get("estimated_hours", 4)
ease_score = 1.0 if hours < 2 else 0.6 if hours <= 6 else 0.3
raw_score = (priority_score * 0.60 + ease_score * 0.40) * 100
reasons = []
if priority == "HIGH":
reasons.append("HIGH priority")
if hours < 2:
reasons.append("quick win")
if not reasons:
reasons.append("local backlog item")
candidates.append({
"title": item.get("title", item["id"]),
"source": "local",
"raw_score": round(raw_score, 1),
"rationale": ", ".join(reasons),
"item_id": item["id"],
"priority": priority,
})
return candidates
def extract_roadmap_goals(pm_dir: Path) -> list[str]:
"""Extract strategic goals from roadmap markdown."""
roadmap_path = pm_dir / "roadmap.md"
if not roadmap_path.exists():
return []
text = roadmap_path.read_text()
goals = []
for line in text.splitlines():
line = line.strip()
if line.startswith("## ") or line.startswith("### "):
goals.append(line.lstrip("#").strip())
elif line.startswith("- "):
goals.append(line.removeprefix("- ").strip())
elif line.startswith("* "):
goals.append(line.removeprefix("* ").strip())
return goals
def score_roadmap_alignment(candidate: dict, goals: list[str]) -> float:
"""Score how well a candidate aligns with roadmap goals. Returns 0.0-1.0."""
if not goals:
return 0.5
title_lower = candidate["title"].lower()
max_alignment = 0.0
for goal in goals:
goal_words = set(goal.lower().split())
goal_words -= {"the", "a", "an", "and", "or", "to", "for", "in", "of", "is", "with"}
if not goal_words:
continue
matching = sum(1 for word in goal_words if word in title_lower)
alignment = matching / len(goal_words) if goal_words else 0.0
max_alignment = max(max_alignment, alignment)
return min(max_alignment, 1.0)
def suggest_action(candidate: dict) -> str:
"""Suggest a concrete next action for a candidate."""
source = candidate["source"]
days_stale = candidate.get("days_stale", 0)
labels = candidate.get("labels", [])
if source == "github_pr":
if candidate.get("is_draft"):
return "Finish draft or close if abandoned"
if days_stale > 14:
return "Merge, close, or rebase — stale >2 weeks"
if days_stale > 7:
return "Review and merge or request changes"
return "Review PR"
elif source == "github_issue":
if any(lbl in ("critical", "priority:critical") for lbl in labels):
return "Fix immediately — critical severity"
if any(lbl in ("bug",) for lbl in labels):
return "Investigate and fix bug"
if days_stale > 30:
return "Triage: still relevant? Close or reprioritize"
return "Work on issue or delegate"
elif source == "local":
return "Pick up from local backlog"
return "Review"
def aggregate_and_rank(
issues: list[dict],
prs: list[dict],
local: list[dict],
goals: list[str],
top_n: int = TOP_N,
) -> tuple[list[dict], list[dict]]:
"""Aggregate candidates from all sources and rank by weighted score.
Returns (top_n items, next 5 near-misses).
"""
scored = []
source_weights = {
"github_issue": WEIGHT_ISSUES,
"github_pr": WEIGHT_PRS,
"local": WEIGHT_LOCAL,
}
all_candidates = issues + prs + local
for candidate in all_candidates:
source = candidate["source"]
source_weight = source_weights.get(source, 0.25)
raw = candidate["raw_score"]
alignment = score_roadmap_alignment(candidate, goals)
final_score = (source_weight * raw) + (WEIGHT_ROADMAP * alignment * 100)
entry = {
"title": candidate["title"],
"source": candidate["source"],
"score": round(final_score, 1),
"raw_score": candidate["raw_score"],
"source_weight": source_weight,
"rationale": candidate["rationale"],
"item_id": candidate.get("item_id", ""),
"priority": candidate.get("priority", "MEDIUM"),
"alignment": round(alignment, 2),
"action": suggest_action(candidate),
}
# Preserve all metadata from the candidate
for key in ("url", "repo", "account", "labels", "created", "updated",
"days_stale", "comments", "is_draft", "score_breakdown"):
if key in candidate:
entry[key] = candidate[key]
scored.append(entry)
priority_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
scored.sort(key=lambda x: (-x["score"], priority_order.get(x["priority"], 1)))
top = scored[:top_n]
for i, item in enumerate(top):
item["rank"] = i + 1
near_misses = scored[top_n:top_n + 5]
for i, item in enumerate(near_misses):
item["rank"] = top_n + i + 1
return top, near_misses
def build_repo_summary(all_candidates: list[dict]) -> dict:
"""Build a per-repo, per-account summary of open work."""
repos: dict[str, dict] = {}
accounts: dict[str, dict] = {}
for c in all_candidates:
repo = c.get("repo", "local")
account = c.get("account", "local")
if repo not in repos:
repos[repo] = {"issues": 0, "prs": 0, "high_priority": 0}
if account not in accounts:
accounts[account] = {"issues": 0, "prs": 0, "repos": set()}
if c["source"] == "github_issue":
repos[repo]["issues"] += 1
accounts[account]["issues"] += 1
elif c["source"] == "github_pr":
repos[repo]["prs"] += 1
accounts[account]["prs"] += 1
if c.get("priority") == "HIGH":
repos[repo]["high_priority"] += 1
accounts[account]["repos"].add(repo)
# Convert sets to lists for JSON serialization
for a in accounts.values():
a["repos"] = sorted(a["repos"])
# Sort repos by total open items descending
sorted_repos = dict(sorted(repos.items(), key=lambda x: -(x[1]["issues"] + x[1]["prs"])))
return {"by_repo": sorted_repos, "by_account": accounts}
def generate_top5(project_root: Path, sources_path: Path | None = None) -> dict:
"""Generate the Top 5 priority list from GitHub + local state."""
pm_dir = project_root / ".pm"
if sources_path is None:
sources_path = pm_dir / "sources.yaml"
# Load GitHub sources config
sources = load_sources(sources_path)
# Remember original account to restore after
original_account = get_current_gh_account()
# Fetch from GitHub
all_issues = []
all_prs = []
accounts_queried = []
for source in sources:
account = source.get("account", "")
repos = source.get("repos", [])
if not account or not repos:
continue
accounts_queried.append(account)
all_issues.extend(fetch_github_issues(account, repos))
all_prs.extend(fetch_github_prs(account, repos))
# Restore original account
if original_account and accounts_queried:
run_gh(["auth", "switch", "--user", original_account])
# Load local overrides
local = []
if pm_dir.exists():
local = load_local_overrides(pm_dir)
# Load roadmap goals
goals = extract_roadmap_goals(pm_dir) if pm_dir.exists() else []
# Aggregate and rank
all_candidates = all_issues + all_prs + local
top5, near_misses = aggregate_and_rank(all_issues, all_prs, local, goals)
summary = build_repo_summary(all_candidates)
return {
"top5": top5,
"near_misses": near_misses,
"summary": summary,
"sources": {
"github_issues": len(all_issues),
"github_prs": len(all_prs),
"local_items": len(local),
"roadmap_goals": len(goals),
"accounts": accounts_queried,
},
"total_candidates": len(all_candidates),
}
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Generate Top 5 priorities from GitHub + local state")
parser.add_argument(
"--project-root", type=Path, default=Path.cwd(), help="Project root directory"
)
parser.add_argument(
"--sources", type=Path, default=None, help="Path to sources.yaml (default: .pm/sources.yaml)"
)
args = parser.parse_args()
try:
result = generate_top5(args.project_root, args.sources)
print(json.dumps(result, indent=2))
return 0
except Exception as e:
print(json.dumps({"error": str(e)}), file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""Manage PM state files - utility for common state operations.
Usage:
python manage_state.py init --project-name NAME --project-type TYPE --goals "goal1,goal2" --quality-bar LEVEL
python manage_state.py add-item --title TITLE --priority PRIORITY [--description DESC]
python manage_state.py update-item ITEM_ID --status STATUS
python manage_state.py create-workstream ITEM_ID --agent AGENT
python manage_state.py update-workstream WS_ID --status STATUS
python manage_state.py list-backlog [--status STATUS]
python manage_state.py list-workstreams [--status STATUS]
"""
import argparse
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
def load_yaml(path: Path) -> dict[str, Any]:
"""Load YAML file safely."""
if not path.exists():
return {}
with open(path) as f:
return yaml.safe_load(f) or {}
def save_yaml(path: Path, data: dict[str, Any]) -> None:
"""Save YAML file."""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
def get_timestamp() -> str:
"""Get current UTC timestamp in ISO8601 format."""
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def cmd_init(args) -> int:
"""Initialize PM directory structure."""
pm_dir = args.project_root / ".pm"
if pm_dir.exists():
print(f"Error: PM already initialized at {pm_dir}", file=sys.stderr)
return 1
# Create directories
pm_dir.mkdir(parents=True)
(pm_dir / "backlog").mkdir()
(pm_dir / "workstreams").mkdir()
(pm_dir / "logs").mkdir()
# Parse goals
goals = [g.strip() for g in args.goals.split(",")]
# Create config
config = {
"project_name": args.project_name,
"project_type": args.project_type,
"primary_goals": goals,
"quality_bar": args.quality_bar,
"initialized_at": get_timestamp(),
"version": "1.0",
}
save_yaml(pm_dir / "config.yaml", config)
# Create empty backlog
save_yaml(pm_dir / "backlog" / "items.yaml", {"items": []})
# Create roadmap template
roadmap = f"""# {args.project_name} Roadmap
## Project Overview
**Type**: {args.project_type}
**Quality Bar**: {args.quality_bar}
## Primary Goals
"""
for goal in goals:
roadmap += f"- {goal}\n"
roadmap += """
## Current Focus
(Add current focus areas here)
## Backlog
(Items managed in .pm/backlog/items.yaml)
## Completed
(Track completed work here)
"""
(pm_dir / "roadmap.md").write_text(roadmap)
# Create context
context = {
"project_name": args.project_name,
"initialized_at": config["initialized_at"],
"version": "1.0",
}
save_yaml(pm_dir / "context.yaml", context)
print(f"✓ PM initialized for {args.project_name}")
return 0
def generate_backlog_id(items: list[dict]) -> str:
"""Generate next backlog ID."""
if not items:
return "BL-001"
max_id = 0
for item in items:
item_id = item.get("id", "")
if item_id.startswith("BL-"):
try:
num = int(item_id.split("-")[1])
max_id = max(max_id, num)
except (IndexError, ValueError):
pass
return f"BL-{max_id + 1:03d}"
def cmd_add_item(args) -> int:
"""Add backlog item."""
pm_dir = args.project_root / ".pm"
backlog_file = pm_dir / "backlog" / "items.yaml"
data = load_yaml(backlog_file)
items = data.get("items", [])
item_id = generate_backlog_id(items)
item = {
"id": item_id,
"title": args.title,
"description": args.description or "",
"priority": args.priority,
"estimated_hours": args.estimated_hours,
"status": "READY",
"created_at": get_timestamp(),
"tags": args.tags.split(",") if args.tags else [],
}
items.append(item)
save_yaml(backlog_file, {"items": items})
print(f"✓ Created {item_id}: {args.title}")
return 0
def cmd_update_item(args) -> int:
"""Update backlog item."""
pm_dir = args.project_root / ".pm"
backlog_file = pm_dir / "backlog" / "items.yaml"
data = load_yaml(backlog_file)
items = data.get("items", [])
found = False
for item in items:
if item["id"] == args.item_id:
if args.status:
item["status"] = args.status
if args.priority:
item["priority"] = args.priority
if args.description:
item["description"] = args.description
found = True
break
if not found:
print(f"Error: Item {args.item_id} not found", file=sys.stderr)
return 1
save_yaml(backlog_file, {"items": items})
print(f"✓ Updated {args.item_id}")
return 0
def generate_workstream_id(ws_dir: Path) -> str:
"""Generate next workstream ID."""
existing = list(ws_dir.glob("ws-*.yaml")) if ws_dir.exists() else []
if not existing:
return "ws-001"
max_id = 0
for ws_file in existing:
name = ws_file.stem
if name.startswith("ws-"):
try:
num = int(name.split("-")[1])
max_id = max(max_id, num)
except (IndexError, ValueError):
pass
return f"ws-{max_id + 1:03d}"
def cmd_create_workstream(args) -> int:
"""Create workstream."""
pm_dir = args.project_root / ".pm"
backlog_file = pm_dir / "backlog" / "items.yaml"
ws_dir = pm_dir / "workstreams"
# Load backlog item
data = load_yaml(backlog_file)
items = data.get("items", [])
item = next((i for i in items if i["id"] == args.item_id), None)
if not item:
print(f"Error: Item {args.item_id} not found", file=sys.stderr)
return 1
# Generate workstream ID
ws_id = generate_workstream_id(ws_dir)
# Create workstream
ws = {
"id": ws_id,
"backlog_id": args.item_id,
"title": item["title"],
"status": "RUNNING",
"agent": args.agent,
"started_at": get_timestamp(),
"completed_at": None,
"process_id": None,
"elapsed_minutes": 0,
"progress_notes": [],
"dependencies": [],
"last_activity": get_timestamp(),
}
save_yaml(ws_dir / f"{ws_id}.yaml", ws)
# Update backlog item status
for item in items:
if item["id"] == args.item_id:
item["status"] = "IN_PROGRESS"
break
save_yaml(backlog_file, {"items": items})
print(f"✓ Created {ws_id} for {args.item_id}")
return 0
def cmd_update_workstream(args) -> int:
"""Update workstream."""
pm_dir = args.project_root / ".pm"
ws_file = pm_dir / "workstreams" / f"{args.ws_id}.yaml"
if not ws_file.exists():
print(f"Error: Workstream {args.ws_id} not found", file=sys.stderr)
return 1
ws = load_yaml(ws_file)
if args.status:
ws["status"] = args.status
if args.status in ["COMPLETED", "FAILED"]:
ws["completed_at"] = get_timestamp()
if args.note:
ws.setdefault("progress_notes", []).append(args.note)
ws["last_activity"] = get_timestamp()
save_yaml(ws_file, ws)
print(f"✓ Updated {args.ws_id}")
return 0
def cmd_list_backlog(args) -> int:
"""List backlog items."""
pm_dir = args.project_root / ".pm"
backlog_file = pm_dir / "backlog" / "items.yaml"
data = load_yaml(backlog_file)
items = data.get("items", [])
if args.status:
items = [i for i in items if i.get("status") == args.status]
for item in items:
status = item.get("status", "READY")
priority = item.get("priority", "MEDIUM")
print(f"{item['id']} [{status}] [{priority}] {item['title']}")
print(f"\nTotal: {len(items)} items")
return 0
def cmd_list_workstreams(args) -> int:
"""List workstreams."""
pm_dir = args.project_root / ".pm"
ws_dir = pm_dir / "workstreams"
if not ws_dir.exists():
print("No workstreams")
return 0
workstreams = []
for ws_file in ws_dir.glob("ws-*.yaml"):
ws = load_yaml(ws_file)
if ws:
workstreams.append(ws)
if args.status:
workstreams = [w for w in workstreams if w.get("status") == args.status]
for ws in workstreams:
status = ws.get("status", "RUNNING")
agent = ws.get("agent", "unknown")
print(f"{ws['id']} [{status}] [{agent}] {ws['title']}")
print(f"\nTotal: {len(workstreams)} workstreams")
return 0
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Manage PM state files")
parser.add_argument("--project-root", type=Path, default=Path.cwd(), help="Project root")
subparsers = parser.add_subparsers(dest="command", required=True)
# Init command
init_parser = subparsers.add_parser("init", help="Initialize PM")
init_parser.add_argument("--project-name", required=True)
init_parser.add_argument("--project-type", required=True)
init_parser.add_argument("--goals", required=True, help="Comma-separated goals")
init_parser.add_argument(
"--quality-bar", required=True, choices=["strict", "balanced", "relaxed"]
)
# Add item command
add_parser = subparsers.add_parser("add-item", help="Add backlog item")
add_parser.add_argument("--title", required=True)
add_parser.add_argument("--priority", default="MEDIUM", choices=["HIGH", "MEDIUM", "LOW"])
add_parser.add_argument("--description", default="")
add_parser.add_argument("--estimated-hours", type=int, default=4)
add_parser.add_argument("--tags", default="")
# Update item command
update_parser = subparsers.add_parser("update-item", help="Update backlog item")
update_parser.add_argument("item_id")
update_parser.add_argument("--status", choices=["READY", "IN_PROGRESS", "DONE", "BLOCKED"])
update_parser.add_argument("--priority", choices=["HIGH", "MEDIUM", "LOW"])
update_parser.add_argument("--description")
# Create workstream command
create_ws_parser = subparsers.add_parser("create-workstream", help="Create workstream")
create_ws_parser.add_argument("item_id")
create_ws_parser.add_argument("--agent", default="builder")
# Update workstream command
update_ws_parser = subparsers.add_parser("update-workstream", help="Update workstream")
update_ws_parser.add_argument("ws_id")
update_ws_parser.add_argument("--status", choices=["RUNNING", "PAUSED", "COMPLETED", "FAILED"])
update_ws_parser.add_argument("--note")
# List commands
list_bl_parser = subparsers.add_parser("list-backlog", help="List backlog items")
list_bl_parser.add_argument("--status", choices=["READY", "IN_PROGRESS", "DONE", "BLOCKED"])
list_ws_parser = subparsers.add_parser("list-workstreams", help="List workstreams")
list_ws_parser.add_argument("--status", choices=["RUNNING", "PAUSED", "COMPLETED", "FAILED"])
args = parser.parse_args()
# Dispatch to command handler
handlers = {
"init": cmd_init,
"add-item": cmd_add_item,
"update-item": cmd_update_item,
"create-workstream": cmd_create_workstream,
"update-workstream": cmd_update_workstream,
"list-backlog": cmd_list_backlog,
"list-workstreams": cmd_list_workstreams,
}
handler = handlers.get(args.command)
if handler:
return handler(args)
return 1
if __name__ == "__main__":
sys.exit(main())
"""Tests for PM Architect scripts."""
pytest>=7.4.0
pytest-asyncio>=0.21.0
pytest-cov>=4.1.0
pytest-mock>=3.11.0
pyyaml>=6.0