Multi-project portfolio management — registry, state aggregation, dependencies, agent allocation
Core skill for multi-project portfolio management. Defines the global portfolio registry format, CRUD operations, state aggregation, cross-project dependency tracking, and agent allocation visibility.
References:
workflow-common.md (portfolio path at {adapter.global_config_dir}/portfolio.md)workflow-common.md (Read -> Update -> Write)agent-registry.md (agent metadata for allocation tracking)execution-tracker.md (progress calculation formula)The global portfolio registry lives at {adapter.global_config_dir}/portfolio.md — outside any project directory, accessible from any working directory.
# Legion Portfolio
## Projects
### {project-name}
- **Path**: {absolute-path-to-project-root}
- **Status**: {Active | Paused | Complete | Stale}
- **Registered**: {YYYY-MM-DD}
- **Description**: {one-line description from PROJECT.md}
### {another-project}
- **Path**: {absolute-path}
- **Status**: Active
- **Registered**: {YYYY-MM-DD}
- **Description**: {one-line description}
## Cross-Project Dependencies
| ID | From | To | Type | Status | Notes |
|----|------|----|------|--------|-------|
| DEP-01 | {project-a}:Phase {N} | {project-b}:Phase {M} | blocks | Active | {why} |
## Metadata
- **Last Updated**: {YYYY-MM-DD}
- **Total Projects**: {count}
- **Active Projects**: {count}
### {project-name}) — the name from its PROJECT.md### My App (my-app-v2).planning/ state{project-name}:Phase {N} notationblocks — hard dependency, the target phase cannot proceed until the source phase completesinforms — soft dependency, the source phase output is useful but not requiredInstructions for commands to follow when managing the portfolio registry. These are procedural descriptions, not function signatures.
1. Read {adapter.global_config_dir}/portfolio.md
- If file doesn't exist: create {adapter.global_config_dir} directory and initialize with empty structure:
# Legion Portfolio
## Projects
## Cross-Project Dependencies
| ID | From | To | Type | Status | Notes |
|----|------|----|------|--------|-------|
## Metadata
- **Last Updated**: {today}
- **Total Projects**: 0
- **Active Projects**: 0
2. Read the current project's .planning/PROJECT.md
- Extract project name from the first "# {name}" heading
- Extract one-line description from the "## What This Is" or first paragraph
3. Get the absolute path of the current working directory
4. Check if this project is already registered (match by Path field):
- If already registered: update name and description if changed, log "Project already in portfolio"
- If not registered: add a new ### {project-name} entry under ## Projects:
### {project-name}
- **Path**: {absolute-path}
- **Status**: Active
- **Registered**: {today}
- **Description**: {one-line description}
5. Update the Metadata section:
- Last Updated: {today}
- Total Projects: count all ### headings under ## Projects
- Active Projects: count entries where Status is Active
6. Write the updated {adapter.global_config_dir}/portfolio.md
1. Read {adapter.global_config_dir}/portfolio.md
- If not found: display "No portfolio registry found. Nothing to unregister."
2. Find the project entry matching the current working directory path
- Or match by a specified project name if provided
3. Remove the entire ### {project-name} section (heading through last bullet)
4. Remove any rows in the Cross-Project Dependencies table that reference this project
5. Update Metadata section (counts and Last Updated)
6. Write the updated {adapter.global_config_dir}/portfolio.md
1. Read {adapter.global_config_dir}/portfolio.md
- If not found: return empty list
2. Parse each ### {project-name} section under ## Projects
3. For each registered project:
a. Extract Path from the "**Path**:" field
b. Check if the directory exists (filesystem check: test -d "{path}")
c. If directory exists:
- Read {path}/.planning/STATE.md — extract:
- Current phase number and total (from "Phase: N of M")
- Status text (from "Status:" field)
- Last activity date and description (from "Last Activity:" field)
- Next action text (from "Next Action:" field)
- Read {path}/.planning/ROADMAP.md — extract:
- Phase list with completion status
- Progress table: plans completed vs total per phase
- Calculate overall completion % using execution-tracker Section 5 formula
- Assess health using Section 3 rules
d. If directory doesn't exist:
- Mark project status as Stale in the registry
- Set health to Red
- Note: "Project directory not found at {path}"
4. Return the assembled project list with live state
How to read and aggregate cross-project state for the portfolio dashboard.
Assess each project's health based on its state:
[OK]: Phase on track, no blockers in STATE.md, progress advancing
[!!]: At risk — activity stale or issues present
[XX]: Blocking — project missing or persistent issues
Compute these from the assembled project list:
Operations for managing dependencies between projects in the portfolio.
1. Read {adapter.global_config_dir}/portfolio.md
2. Validate both projects exist in the registry
- If either project is not found: report error and abort
3. Assign the next DEP-{NN} ID
- Scan existing IDs, find the highest number, increment by 1
- If no existing dependencies: start at DEP-01
4. Add a new row to the Cross-Project Dependencies table:
| DEP-{NN} | {source-project}:Phase {N} | {target-project}:Phase {M} | {type} | Active | {notes} |
5. Write the updated {adapter.global_config_dir}/portfolio.md
1. Read {adapter.global_config_dir}/portfolio.md
2. Find the dependency row by ID (e.g., DEP-03) or by from/to match
3. Remove the row from the Cross-Project Dependencies table
4. Write the updated {adapter.global_config_dir}/portfolio.md
1. Read {adapter.global_config_dir}/portfolio.md
- Parse the Cross-Project Dependencies table
2. For each active dependency:
a. Read the "From" project's ROADMAP.md at the registered path
b. Find the referenced phase in the Phases section
c. Check if the phase is marked complete (has [x] prefix)
d. If complete: dependency status = "Resolved"
e. If not complete: dependency status = "Blocking"
f. Impact text: "{To project} Phase {M} is waiting on {From project} Phase {N}"
3. Return list of dependencies with current status and impact assessment
Dependencies use {project-name}:Phase {N} format:
My App:Phase 3 — refers to Phase 3 of the "My App" project### heading in the registryDerive agent usage patterns across projects at read-time. Since agents are personality files (not running processes), "allocation" is about visibility into usage patterns, not resource locking. Any project can use any agent at any time.
1. For each registered active project:
a. Read all directories under {path}/.planning/phases/
b. Primary source: Read CONTEXT.md files in each phase directory
- These contain explicit agent recommendations per phase
- Look for agent ID references (e.g., "engineering-senior-developer", "testing-qa-verification-specialist")
c. Secondary source: Scan plan body text and SUMMARY.md files
- Search for agent ID patterns matching {division}-{role} format
d. Note: YAML frontmatter in plan files does NOT contain agent IDs
- Do not rely on frontmatter for allocation data
2. Build a map: {agent-id} -> [{project-name}, ...]
- Each agent ID maps to the list of projects that reference it
From the agent usage map, derive:
Division counts MUST match the canonical Division Index in CLAUDE.md. Do NOT hardcode counts from any other source — re-derive them at runtime from the agents/ directory so schema drift is impossible.
Canonical counts (mirror of CLAUDE.md — update both in the same commit if either changes):
engineering — 9 agents
design — 6 agents
marketing — 4 agents
product — 4 agents
project-management — 5 agents
testing — 6 agents
support — 4 agents
spatial — 6 agents (aka "Spatial Computing")
specialized — 4 agents
─────────────────────────────
TOTAL — 48 agents
Runtime derivation (preferred — use this for any Section 5 computation):
grep -h '^division:' agents/*.md | sort | uniq -c
Integrity check: scripts/audit/validate-agent-divisions.sh MUST pass before Section 5 displays Agent Allocation metrics. If the runtime count disagrees with the table above, treat the runtime count as truth and flag the discrepancy — CLAUDE.md and this table are stale.
How to handle failures and edge cases in portfolio operations.
Missing PORTFOLIO.md: Create with empty structure on first register operation. For read operations (list, dashboard), return empty results with guidance: "No projects registered. Run /legion:start in a project to add it."
Missing project directory: Mark the project as Stale in the registry. Do not remove it automatically — the user may want to update the path or the directory may be temporarily unavailable. Display: "Project directory not found at {path}. Status set to Stale."
Corrupted STATE.md or ROADMAP.md: If a project's state files cannot be parsed, skip that project in aggregation. Report: "Unable to read state for {project-name} — skipping in dashboard."
Permission errors on ~/.claude/: Report clearly: "Cannot create portfolio registry at {adapter.global_config_dir}. Check directory permissions."
Name collisions: When two projects have the same name (from their PROJECT.md headings), append the directory basename in parentheses to disambiguate: ### My App (project-v2).
Empty portfolio: Dashboard shows: "No projects registered. Run /legion:start in a project to add it."
Stale path recovery: If a project is marked Stale, the user can:
/legion:start (re-registers with correct path){adapter.global_config_dir}/portfolio.md to update the Path field