diff --git a/.changeset/apm-integration.md b/.changeset/apm-integration.md new file mode 100644 index 000000000..4d8169003 --- /dev/null +++ b/.changeset/apm-integration.md @@ -0,0 +1,7 @@ +--- +"squad": minor +--- + +feat: APM integration — squad skill publish/install + apm.yml in init + +Implements #824. Adds `squad skill publish/install/list` commands and generates `apm.yml` on `squad init`. diff --git a/.squad-templates/squad.agent.md b/.squad-templates/squad.agent.md index 1a3813a33..df1b1347a 100644 --- a/.squad-templates/squad.agent.md +++ b/.squad-templates/squad.agent.md @@ -915,7 +915,8 @@ If the user says "I need a designer" or "add someone for DevOps": 4. **Update `.squad/casting/registry.json`** with the new agent entry. 5. Add to team.md roster. 6. Add routing entries to routing.md. -7. Say: *"✅ {CastName} joined the team as {Role}."* +7. **Wire enforcement (if applicable).** If the new member's role involves gating other agents' work (reviewer, design approver, quality gate), add a numbered enforcement rule to `routing.md` → Rules section. A routing table entry (step 6) only handles explicit requests — enforcement rules are required for automatic gates. Read `.squad/templates/workflow-wiring-guide.md` for the full wiring process, including walkthroughs for common role types (code reviewer, documenter). Check `.squad/templates/issue-lifecycle.md` for lifecycle integration if the project uses PR-gated workflows. +8. Say: *"✅ {CastName} joined the team as {Role}."* ### Removing Team Members diff --git a/.squad-templates/workflow-wiring-appendix-a-code-reviewer.md b/.squad-templates/workflow-wiring-appendix-a-code-reviewer.md new file mode 100644 index 000000000..324e0ef60 --- /dev/null +++ b/.squad-templates/workflow-wiring-appendix-a-code-reviewer.md @@ -0,0 +1,131 @@ +# Appendix A: Wiring a Code Reviewer — Complete Walkthrough + +> End-to-end example of adding a code reviewer to your squad and wiring their gate so it actually gets enforced. This walkthrough addresses a common failure: a reviewer is on the roster but never reviews a single PR because the gate wasn't wired. + +## The Problem This Solves + +Adding a reviewer to `team.md` gives them an identity. It does NOT: +- Tell the coordinator to route PRs to them +- Prevent PRs from being merged without their approval +- Prevent issues from being closed before review happens + +**What goes wrong without enforcement:** A reviewer can be on the roster as "Reviewer" from day one. Their charter says they review PRs. The routing table says "PR code review → {ReviewerName}." But PRs get merged and issues get closed without them ever being spawned. Why? + +Because the routing table says WHO handles what — it's for incoming requests ("review PR #42"). It does NOT say "after every agent completes work, route their output to {ReviewerName}." The coordinator routes work TO agents, but nothing tells it to route COMPLETED work to a reviewer. The "After Agent Work" flow in `squad.agent.md` says: collect results → present → spawn Scribe. No review step. + +**The fix has three layers:** + +| Layer | What it does | Where it lives | +|-------|-------------|----------------| +| Identity | Reviewer exists and knows how to review | `team.md` roster + `charter.md` | +| Routing | User can explicitly request "review this" | `routing.md` routing table | +| **Enforcement** | Coordinator MUST route every PR to reviewer before merge | `routing.md` Rules section + `issue-lifecycle.md` post-work steps | + +Most squads get layers 1 and 2 right. Layer 3 — enforcement — is what's usually missing. + +## Step-by-Step Walkthrough + +### Step 1: Create the reviewer's identity + +Create `.squad/agents/{name}/charter.md`: + +```markdown +# {Name} — Code Reviewer + +## Identity +- **Name:** {Name} +- **Role:** Code Reviewer +- **Expertise:** Code quality, correctness, test coverage, security, patterns +- **Style:** Thorough, fair, specific. Provides actionable feedback. + +## What I Own +- Reviewing PRs for code quality, correctness, and test coverage +- Identifying bugs, security issues, and design problems +- Providing specific, actionable feedback (not vague suggestions) + +## How I Review +1. Read the PR diff completely +2. Check: does it do what the issue asked for? +3. Check: are there tests? Do they cover the important cases? +4. Check: are there bugs, edge cases, or security issues? +5. Check: does it follow project patterns and conventions? +6. Verdict: APPROVE or REJECT with specific feedback + +## Boundaries +**I handle:** Code review, PR review, quality gates +**I don't handle:** Implementation, design, research, documentation + +## On REJECT +I provide specific feedback: what's wrong, why, and what to do instead. +The original author fixes their work. I re-review after fixes. +``` + +Create `.squad/agents/{name}/history.md` seeded with project context. + +### Step 2: Add to team.md roster + +```markdown +| 👑 {Name} | Code Reviewer | `.squad/agents/{name}/charter.md` | ✅ Active | +``` + +### Step 3: Add routing table entry + +In `routing.md` → routing table: + +```markdown +| PR code review | 👑 {Name} | — | "Review PR #42", code quality, finding reports | +``` + +**⚠️ This is necessary but NOT sufficient.** This only handles explicit review requests. It does NOT enforce automatic review of every PR. + +### Step 4: Add enforcement rule (THIS IS THE CRITICAL STEP) + +In `routing.md` → `## Rules` section, add a numbered rule: + +```markdown +N. **{Name} PR Gate** — every PR created by any agent MUST be reviewed by {Name} + before merge. The coordinator spawns {Name} (sync) with the PR diff after + the author pushes and creates the PR. On REJECT, the original author addresses + feedback. On APPROVE, the coordinator merges via `gh pr merge`. No PR merges + without {Name}'s approval. +``` + +**Why this works when the routing table alone didn't:** The routing table is for matching incoming work to agents. Rules are behavioral constraints the coordinator must follow AFTER work completes. The rule says "after a PR exists, you MUST do X before proceeding." The routing table says "if someone asks for a review, route to X." + +### Step 5: Wire into issue-lifecycle.md + +In `.squad/templates/issue-lifecycle.md`, the "Coordinator Post-Work Steps" section should reference your reviewer by name: + +```markdown +4. **Route to reviewer.** Spawn {Name} (sync) with the PR diff for code review. +``` + +This is the operational detail — the step-by-step instructions the coordinator follows after an agent completes issue work. The routing rule (Step 4) is the mandate; the lifecycle template is the procedure. + +### Step 6: Add to casting registry + +Update `.squad/casting/registry.json` with the new entry. + +### Step 7: Verify + +Ask yourself these questions: + +- [ ] If a clean session coordinator reads `routing.md` Rules, will it know to route PRs to this reviewer? → Check rule N exists. +- [ ] If an agent completes work and pushes a PR, does the coordinator's post-work flow include a review step? → Check `issue-lifecycle.md` step 4. +- [ ] Can the coordinator merge a PR without the reviewer's approval? → The rule should say "No PR merges without {Name}'s approval." +- [ ] Can the coordinator close an issue without a merged PR? → Check the issue closure rule exists. + +If any answer is wrong, you have a gap. + +## What Each File Controls (Summary) + +| File | What it contributes to the reviewer gate | +|------|----------------------------------------| +| `charter.md` | WHO the reviewer is and HOW they review | +| `team.md` | That the reviewer EXISTS on the team | +| `routing.md` routing table | That explicit review requests go to this reviewer | +| `routing.md` Rules section | That the coordinator MUST route EVERY PR to this reviewer (enforcement) | +| `issue-lifecycle.md` | The step-by-step procedure for the post-work review flow | +| `casting/registry.json` | Persistent name tracking | + +**Remove any one of these and the gate has a hole.** The most commonly missed piece is the Rules section entry (Step 4). diff --git a/.squad-templates/workflow-wiring-appendix-b-documenter.md b/.squad-templates/workflow-wiring-appendix-b-documenter.md new file mode 100644 index 000000000..870e61ccb --- /dev/null +++ b/.squad-templates/workflow-wiring-appendix-b-documenter.md @@ -0,0 +1,140 @@ +# Appendix B: Wiring a Documenter/Librarian — Complete Walkthrough + +> End-to-end example of adding a documenter role that ensures significant changes are documented. This is a FOLLOW-UP TRIGGER pattern — not a gate (which blocks), but an automatic downstream task that fires after work completes. + +## The Problem This Solves + +Your project has agents building features, fixing bugs, and writing tools. But nobody documents what was built, how to use it, or what changed. Documentation happens only when someone explicitly asks — and by then, the context is lost. + +A documenter/librarian role solves this by automatically evaluating whether completed work needs documentation and producing it if so. + +## Gate vs Follow-Up Trigger + +| Pattern | Blocks work? | When it runs | Example | +|---------|-------------|-------------|---------| +| **Gate** (Appendix A) | Yes — work cannot proceed without approval | Before merge | Code reviewer must approve PR | +| **Follow-up trigger** | No — work proceeds, documentation happens in parallel | After merge | Documenter evaluates if docs are needed | + +A documenter is typically a follow-up trigger, not a gate. You don't want documentation review to block a hotfix from merging. But you DO want documentation to happen automatically after significant changes. + +## Step-by-Step Walkthrough + +### Step 1: Create the documenter's identity + +Create `.squad/agents/{name}/charter.md`: + +```markdown +# {Name} — Documenter + +## Identity +- **Name:** {Name} +- **Role:** Documenter / Librarian +- **Expertise:** Documentation, guides, READMEs, changelogs, knowledge management +- **Style:** Clear, thorough, user-focused. Makes complex things understandable. + +## What I Own +- Evaluating whether completed work needs documentation +- Writing/updating READMEs, guides, and runbooks +- Maintaining a docs index so nothing gets lost +- Summarizing design decisions and architectural changes + +## How I Work +1. Read the PR diff or agent output +2. Assess: does this change user-facing behavior? Add a new feature? Change configuration? +3. If yes: write or update the relevant documentation +4. If no: report "no docs needed" with brief justification + +## Boundaries +**I handle:** Documentation, guides, READMEs, summaries, knowledge management +**I don't handle:** Code implementation, code review, research, operations +``` + +Create `.squad/agents/{name}/history.md` seeded with project context. + +### Step 2: Add to team.md roster + +```markdown +| 📝 {Name} | Documenter | `.squad/agents/{name}/charter.md` | ✅ Active | +``` + +### Step 3: Add routing table entry + +In `routing.md` → routing table: + +```markdown +| Documentation, reports, summaries | 📝 {Name} | `docs/` | "Write docs for X", "Summarize this", guides, READMEs | +``` + +### Step 4: Add follow-up trigger rule + +In `routing.md` → `## Rules` section, add a numbered rule: + +```markdown +N. **Documentation follow-up** — after any PR is merged that adds or modifies + user-facing features, scripts, tools, or configuration, the coordinator + spawns {Name} (background) to evaluate whether documentation is needed. + {Name} reads the merged PR diff and either writes/updates docs or reports + "no docs needed." This is a follow-up, not a gate — it does not block + the merge. +``` + +**Why a rule and not a ceremony:** Ceremonies are structured multi-participant meetings. This is a single-agent follow-up task. A routing rule is simpler and more appropriate. + +**Why background, not sync:** Documentation doesn't block other work. The documenter runs in parallel with whatever comes next. + +### Step 5: Wire into the coordinator's post-merge flow + +This is the trickiest part. The coordinator's After Agent Work flow doesn't currently have a "post-merge" hook. You wire this through the issue-lifecycle template. + +In `.squad/templates/issue-lifecycle.md`, after the merge step, add: + +```markdown +8. **Documentation follow-up.** After merge, check routing.md Rules for + documentation follow-up rule. If present, spawn the documenter (background) + with the merged PR diff to evaluate whether docs are needed. +``` + +Alternatively, you can wire this as an `after` ceremony in `ceremonies.md`: + +```yaml +- name: "Documentation Check" + when: "after" + condition: "PR merged that adds features, scripts, tools, or config changes" + facilitator: "{DocumenterName}" + participants: ["{DocumenterName}"] + output: "Docs written/updated, or 'no docs needed' with justification" +``` + +### Step 6: Worktree for doc changes + +If the documenter produces files, they need a worktree — docs are files too. The coordinator should: +1. Create a worktree for the doc update (e.g., `squad/{issue}-docs`) +2. The documenter commits and pushes +3. A PR is created for the docs +4. The docs PR goes through the normal review flow (including the code reviewer if you have one) + +This means doc changes also get reviewed. The documenter is not exempt from the review gate. + +### Step 7: Add to casting registry + +Update `.squad/casting/registry.json` with the new entry. + +### Step 8: Verify + +- [ ] After a feature PR merges, does the coordinator spawn the documenter? → Check the routing rule exists. +- [ ] Does the documenter get a worktree for their work? → Check the worktree rule covers docs. +- [ ] Do doc changes go through the review gate? → They should — docs are files, files need PRs, PRs need review. +- [ ] Is the follow-up non-blocking? → The documenter should be background, not sync. + +## What Each File Controls (Summary) + +| File | What it contributes | +|------|-------------------| +| `charter.md` | WHO the documenter is and HOW they evaluate | +| `team.md` | That the documenter EXISTS | +| `routing.md` routing table | That explicit doc requests go to this member | +| `routing.md` Rules section | That the coordinator MUST spawn docs evaluation after merges (enforcement) | +| `issue-lifecycle.md` or `ceremonies.md` | The procedural hook: when exactly the follow-up fires | +| `casting/registry.json` | Persistent name tracking | + +**The most commonly missed piece:** The Rules section entry (Step 4). Without it, the documenter only runs when someone explicitly says "write docs for X." The whole point is that it runs automatically. diff --git a/.squad-templates/workflow-wiring-guide.md b/.squad-templates/workflow-wiring-guide.md new file mode 100644 index 000000000..e6b775ae1 --- /dev/null +++ b/.squad-templates/workflow-wiring-guide.md @@ -0,0 +1,276 @@ +# Squad Workflow Wiring Guide + +> How to wire up new team members, reviewer gates, and custom workflows so they actually get enforced by the coordinator — even in a clean session with no prior memory. + +## Why This Guide Exists + +The Squad framework (`squad.agent.md`) provides generic orchestration primitives. **It does not prescribe a specific workflow.** Your project's workflow — whether that's "all code goes through PRs and reviews" or "just commit to main" — must be wired into project-level configuration files. + +If a workflow rule exists only in someone's memory, in a chat transcript, or in `decisions.md` but NOT in a configuration file the coordinator reads at decision time — **it will not be followed in a clean session.** + +### Why Existing Patterns Aren't Enough + +The Squad framework already has concepts for routing tables, reviewer roles, and ceremonies. But having these concepts does NOT mean they work automatically: + +- **Adding a reviewer to the roster ≠ enforcing reviews.** A reviewer can be on the roster with "Reviewer" as their role and never review a single PR — because no RULE in `routing.md` tells the coordinator to route PRs to them. The roster says WHO exists. Rules say WHAT they enforce. + +- **Capturing a decision ≠ enforcing it.** `decisions.md` may contain "every change must go through a PR" and "only {ReviewerName} closes PRs." These can get buried in a large file that the coordinator reads for context but doesn't treat as enforcement rules. A decision is a historical record. A routing rule is an enforceable constraint. + +- **Describing a lifecycle ≠ wiring it.** `squad.agent.md` describes issue→branch→PR→review→merge. But if the After Agent Work section (the flow the coordinator actually follows after every agent completes) has no push/PR/review step, the lifecycle is described conceptually but never connected to the coordinator's actual decision flow. + +**The pattern that works:** A numbered rule in `routing.md` → Rules section. The coordinator reads this section, treats each rule as a constraint, and follows them. If your workflow isn't a numbered rule, it's a suggestion. + +--- + +## Configuration Surface Area + +The coordinator reads these files to decide how to behave. If your workflow isn't encoded in one of these, it doesn't exist. + +| File | What It Controls | Read When | +|------|-----------------|-----------| +| `routing.md` | WHO handles what, behavioral RULES, reviewer GATES | Every session start, before every routing decision | +| `ceremonies.md` | Auto-triggered ceremonies (before/after work batches) | Before spawning work batches, after completion | +| `templates/issue-lifecycle.md` | Git workflow: push, PR, review, merge, issue closure | When spawning agents for issue-linked work | +| Agent `charter.md` | Per-agent identity, boundaries, behavior | Inlined into every spawn prompt | +| `team.md` | Roster, member capabilities | Session start | +| `decisions.md` | Captured decisions and directives | Read by agents at spawn time | + +### How They Interact + +``` +User request arrives + → Coordinator reads routing.md (WHO handles this?) + → Coordinator checks ceremonies.md (any auto-triggered "before" ceremony?) + → Coordinator reads agent charter.md (inline into spawn prompt) + → If issue-linked: coordinator reads issue-lifecycle.md (add ISSUE CONTEXT to spawn prompt) + → Agent works + → Coordinator follows After Agent Work flow + → Coordinator checks ceremonies.md (any auto-triggered "after" ceremony?) + → Coordinator checks routing.md Rules section (any post-work rules to enforce?) +``` + +**The critical insight:** `routing.md` Rules section and `ceremonies.md` are the two enforcement mechanisms. If a rule isn't in one of these, the coordinator has no way to know about it. + +--- + +## How to Wire Up a New Team Member + +### Step 1: Create the member (files) + +``` +.squad/agents/{name}/ + charter.md ← Identity, role, boundaries, what they own + history.md ← Seeded with project context from team.md +``` + +### Step 2: Add to roster (`team.md`) + +Add a row to the `## Members` table: +``` +| {emoji} {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active | +``` + +### Step 3: Add routing entry (`routing.md`) + +Add a row to the routing table: +``` +| {Work Type} | {emoji} {Name} | {Output Location} | {Examples} | +``` + +### Step 4: Add issue routing (if applicable) + +Add to the Issue Routing table in `routing.md`: +``` +| squad:{name} | {Description of work} | {emoji} {Name} | +``` + +### Step 5: Add to casting registry + +Update `.squad/casting/registry.json` with the new entry. + +### Step 6: Wire any gates (if this member is a reviewer/gate) + +**This is the step most people miss.** If the new member should review or gate other members' work, you need to wire enforcement. See "How to Wire Up a Reviewer Gate" below. + +--- + +## How to Wire Up a Reviewer Gate + +A reviewer gate means: "Agent X must review Agent Y's output before it proceeds." The framework supports this but does NOT automatically enforce it. You must wire it. + +### Option A: Routing Rule (recommended for simple gates) + +Add to `routing.md` → `## Rules` section: + +```markdown +N. **{GateName} Gate** — Every {output type} from {Author} MUST be reviewed by {ReviewerName} before {next step}. The coordinator routes {Author}'s output to {ReviewerName} (sync spawn), collects the verdict, and only proceeds if approved. On rejection, {Author} revises based on {ReviewerName}'s feedback. +``` + +**Example — reviewer for all PRs:** +```markdown +9. **{ReviewerName} PR Gate** — Every PR created by any agent MUST be reviewed by {ReviewerName} before merge. The coordinator spawns {ReviewerName} (sync) with the PR diff, collects APPROVE/REJECT verdict. On rejection, the original author addresses feedback. +``` + +**Example — design review gate:** +```markdown +10. **{DesignReviewer} Design Gate** — Every design doc produced by the architect MUST be reviewed by {DesignReviewer} before implementation begins. {DesignReviewer} always rejects the first draft on concept/approach. Implementation is BLOCKED until {DesignReviewer} approves. +``` + +**Why this works:** The coordinator reads the Rules section before and after every work batch. Rules are behavioral constraints the coordinator must follow. + +### Option B: Ceremony (recommended for multi-participant gates) + +Add to `ceremonies.md` using the Markdown table format the file uses: + +```markdown +## Design Review + +| Field | Value | +|-------|-------| +| **Trigger** | auto | +| **When** | before | +| **Condition** | task involves implementing a design doc | +| **Facilitator** | {DesignReviewer} | +| **Participants** | Architect, {DesignReviewer} | +| **Time budget** | focused | +| **Enabled** | ✅ yes | + +**Agenda:** +1. Read the design doc +2. Challenge the premise and approach +3. Demand alternatives and evidence +4. Verdict: APPROVE or REJECT +``` + +**Why this works:** The coordinator checks ceremonies.md for `before` ceremonies whose condition matches the current task. If matched, the ceremony runs before work begins. + +### Option A vs Option B + +| Use Case | Use Routing Rule | Use Ceremony | +|----------|-----------------|--------------| +| Simple 1-on-1 review (reviewer → author) | ✅ | Overkill | +| Multi-participant alignment (3+ agents) | Too simple | ✅ | +| Needs structured facilitation | No | ✅ | +| Must run automatically before specific work | Either works | ✅ | +| One-line behavioral constraint | ✅ | Overkill | + +--- + +## How to Wire Up an Issue Lifecycle (Git Workflow) + +This is where you define what happens after an agent completes work on a GitHub issue. The framework references `.squad/templates/issue-lifecycle.md` but does NOT create it — you must create it yourself. + +> **⚠️ This file is required if your project uses GitHub Issues Mode.** Without it, the coordinator has no post-work steps for push/PR/review and will treat agent commit as "done." + +See `.squad/templates/issue-lifecycle.md` for the full template if your project already has one. If not, create it following the pattern below. + +### Step 1: Create `templates/issue-lifecycle.md` + +Create `.squad/templates/issue-lifecycle.md` with your project's git workflow. At minimum it should include: + +- An ISSUE CONTEXT block template (for spawn prompts) +- Coordinator post-work steps (verify push → verify PR → route to reviewer → merge on approval) +- Issue closure rules (PR merge auto-close vs manual close) +- Worktree requirements (if applicable) + +### Step 2: Add enforcement rules to `routing.md` + +Add numbered rules to the `## Rules` section that reference the lifecycle: + +```markdown +N. **Issue lifecycle enforcement** — all issue-linked work follows the lifecycle + in `.squad/templates/issue-lifecycle.md`. The coordinator adds the ISSUE CONTEXT + block to spawn prompts and follows the post-work steps (verify push → verify PR + → route to reviewer → merge on approval). Read `issue-lifecycle.md` before + spawning any agent for issue work. + +N+1. **{ReviewerName} PR Gate** — every PR created by any agent MUST be reviewed + by {ReviewerName} before merge. The coordinator spawns {ReviewerName} (sync) + with the PR diff. On REJECT, the original author addresses feedback. On APPROVE, + the coordinator merges. No PR merges without {ReviewerName}'s approval. + +N+2. **Issue closure restriction** — issues that produced files (code, docs, scripts, + designs, tests) close ONLY via PR merge auto-close ("Closes #N" in PR body). + Never use `gh issue close` for file-producing work. Exception: tracking/strategic + issues and superseded issues may be closed with a comment. + +N+3. **Worktree for all file-producing work** — every task that creates or modifies + files (including documentation) requires a worktree. Exceptions: read-only queries, + Scribe (.squad/ state), pure analysis producing no files. +``` + +### Step 3: Verify your wiring + +After creating both files, run the verification checklist (below) to confirm a clean session coordinator would follow the lifecycle. + +--- + +## How to Wire Up a Custom Workflow Step + +If you need something that isn't a reviewer gate or issue lifecycle — for example, "always run tests before pushing" or "docs must be reviewed by the author before merge" — here's where to put it: + +### If it's a behavioral rule the coordinator should always follow: +→ Add to `routing.md` → `## Rules` section + +### If it should trigger automatically before/after specific work: +→ Add to `ceremonies.md` as a `before` or `after` ceremony + +### If it's something agents should do as part of their work: +→ Add to the agent's `charter.md` under a new section + +### If it's something that applies only to issue-linked work: +→ Add to `templates/issue-lifecycle.md` + +### If it's a team-wide constraint that should be visible to all agents: +→ Capture as a decision in `decisions.md` (via directive or decision inbox) + +--- + +## Verification Checklist + +After wiring any new member, gate, or workflow, verify: + +- [ ] **Clean session test:** Start a new session (no memory). Give a task. Does the coordinator follow the new rule? +- [ ] **File completeness:** Is the rule/gate/workflow encoded in a file the coordinator reads? (routing.md, ceremonies.md, issue-lifecycle.md, charter.md) +- [ ] **No verbal-only rules:** Is there anything the coordinator should do that's only in chat history or your memory? If yes, it will be lost on session restart. +- [ ] **Gate enforcement:** If you added a reviewer gate, does the routing.md Rules section or ceremonies.md explicitly say the coordinator must route to the reviewer? "Having a reviewer on the roster" is not the same as "enforcing that they review." +- [ ] **Issue lifecycle:** If your project uses PRs, does `templates/issue-lifecycle.md` exist? Does routing.md reference it? + +--- + +## Common Mistakes + +1. **Adding a reviewer to the roster but not wiring a gate.** Having a reviewer on the team doesn't mean they review anything. You must add a rule in routing.md that says "route PRs to {ReviewerName}." + +2. **Closing issues via `gh issue close` instead of PR merge.** If your project uses PRs, issue closure should happen via "Closes #N" in the PR body. Wire this in issue-lifecycle.md. + +3. **Writing docs/scripts directly on main.** If your project requires branches for all changes, the worktree gate must apply to ALL file-producing work — including docs. Make this explicit in routing.md Rules. + +4. **Assuming the coordinator remembers verbal instructions.** Each session starts fresh. If you told the coordinator "always use opus" in session 1, session 2 won't know unless it's in decisions.md or routing.md. + +5. **Not creating `issue-lifecycle.md`.** The framework references it but doesn't create it. If your project uses GitHub Issues Mode, create this template. + +6. **Capturing a decision but never encoding it as a rule.** `decisions.md` is a historical record. The coordinator reads it for context but doesn't treat entries as enforceable constraints. If a decision should be enforced, it must become a numbered rule in `routing.md` Rules section. + +--- + +## Decisions Audit + +Periodically scan `decisions.md` for directives that should be routing rules but aren't: + +1. Search for phrases like "always", "never", "must", "every", "required" +2. For each match, ask: "Is this enforced by a numbered rule in routing.md?" +3. If no → either add a rule, or accept that it's advisory-only +4. If yes → verify the rule text matches the decision + +This prevents `decisions.md` from becoming a graveyard of good intentions that the coordinator reads but doesn't act on. + +--- + +## Appendices + +For detailed end-to-end walkthroughs of specific wiring scenarios, see: + +- **[Appendix A: Wiring a Code Reviewer](workflow-wiring-appendix-a-code-reviewer.md)** — Full walkthrough of adding a code reviewer member and wiring their gate so it actually gets enforced. Includes every file that needs modification with exact content. + +- **[Appendix B: Wiring a Documenter/Librarian](workflow-wiring-appendix-b-documenter.md)** — Full walkthrough of adding a documenter role that ensures all significant changes are documented. Shows a follow-up trigger pattern rather than a gate pattern. diff --git a/.squad/skills/fact-checking/SKILL.md b/.squad/skills/fact-checking/SKILL.md new file mode 100644 index 000000000..f7f7873e5 --- /dev/null +++ b/.squad/skills/fact-checking/SKILL.md @@ -0,0 +1,61 @@ +--- +name: fact-checking +description: Review and validate claims using counter-hypothesis testing. Use when verifying technical content, checking references, validating API endpoints, or performing quality assurance on deliverables. +license: MIT +compatibility: Requires access to external references and APIs +metadata: + author: squad + version: "1.0.0" + domain: quality, verification + confidence: low + last_validated: "2026-03-01" +--- + +# Skill: Fact Checking + +## Context +Codifies the challenger agent review output format and methodology so any agent performing fact-checking or review produces consistent, structured output. + +## Pattern + +### Review Methodology + +For every claim or deliverable under review: +1. Ask: "What evidence supports this? What would disprove it?" +2. Generate counter-hypotheses and test them against available data +3. Verify URLs, package names, API endpoints, and external references actually exist +4. Flag confidence levels: ✅ Verified, ⚠️ Unverified, ❌ Contradicted + +### Review Output Format + +When reviewing another agent's work, use this template: + +``` +### Fact Check — {deliverable name} +**Claims verified:** {count} +**Issues found:** {count} + +| # | Claim | Status | Evidence/Notes | +|---|-------|--------|---------------| +| 1 | {claim} | ✅/⚠️/❌ | {supporting or contradicting evidence} | + +**Counter-hypotheses tested:** +- {alternative explanation + result} + +**Verdict:** {PASS / PASS WITH NOTES / NEEDS REVISION} +``` + +### Confidence Levels + +- ✅ **Verified** — evidence confirms the claim +- ⚠️ **Unverified** — cannot confirm or deny; suggest verification method +- ❌ **Contradicted** — evidence disproves the claim + +### Ceremony Integration + +Auto-trigger this skill before any architecture decision, or when an agent claim contains superlatives or percentage thresholds (e.g., "saves 75%", "always", "never"). The coordinator spawns the challenger agent with: + +``` +Challenger — fact-check {agent}'s claim: "{claim}" +Cite evidence for every verdict. Max 3 investigation cycles. +``` \ No newline at end of file diff --git a/.squad/templates/agents/challenger.md b/.squad/templates/agents/challenger.md new file mode 100644 index 000000000..8477841e0 --- /dev/null +++ b/.squad/templates/agents/challenger.md @@ -0,0 +1,72 @@ +# Challenger — Devil's Advocate & Fact Checker + +> The trial never ends. Every claim deserves scrutiny. + +## Identity + +- **Name:** Challenger (customize: e.g., "Q", "Advocate", "Auditor") +- **Role:** Devil's Advocate & Fact Checker +- **Expertise:** Counter-hypothesis generation, fact verification, assumption challenging, hallucination detection +- **Style:** Incisive, rigorous, constructively contrarian — questions everything to strengthen, not obstruct + +## What I Own + +- Fact-checking claims, research outputs, and agent deliverables +- Running counter-hypotheses against team assumptions +- Verifying external references and sources +- Challenging decisions before they are locked in +- Detecting hallucinated facts or unsupported claims + +## How I Work + +- Read `.squad/decisions.md` before starting +- For every claim: "What evidence supports this? What would disprove it?" +- Verify URLs, package names, API endpoints actually exist +- Flag confidence: ✅ Verified, ⚠️ Unverified, ❌ Contradicted +- Write challenge notes to `.squad/decisions/inbox/challenger-{brief-slug}.md` + +## Skills + +- Review output format & methodology: `.squad/skills/fact-checking/SKILL.md` + +## Boundaries + +**I handle:** Fact-checking, counter-hypothesis testing, verification, constructive challenge +**I do not handle:** Implementation, code writing, architecture design — I review, not build +**On rejection:** Specific items needing correction + verification methods provided + +## Iterative Retrieval + +When called by the coordinator or another agent: + +1. **Max 3 investigation cycles.** Up to 3 rounds of tool calls / information gathering before returning results. Stop after cycle 3 even if partial; note what additional work would be needed. +2. **Return objective context.** Response always addresses the WHY passed by the coordinator, not just the surface task. +3. **Self-evaluate before returning.** Before replying, check: does my return satisfy the success criteria the coordinator stated? If not, do one more targeted cycle (within the 3-cycle budget) before flagging the gap. + +## Ceremony Integration + +The coordinator may auto-spawn Challenger before: +- Any architecture decision +- Claims containing superlatives or quantified savings (e.g., "saves 75%", "always", "never fails") +- Final deliverables before external publication + +**Spawn prompt pattern:** +``` +Challenger — fact-check {agent}'s claim: "{claim}" +Deliverable: {file or summary} +Cite evidence for every verdict. Flag confidence levels. Max 3 investigation cycles. +Success criteria: verdict table with ✅/⚠️/❌ for each claim. +``` + +## Prior Art + +Field-tested across 200+ issues. Caught: inflated metrics (claimed 75–90% savings, actual 20–55%), fabricated config references, wrong bottleneck assumptions. False positive rate ~15%. + +## Model + +- **Preferred:** auto (coordinator selects based on task type) +- **Rationale:** Fact-checking requires analytical depth + +## Voice + +The trial never ends. Every claim deserves scrutiny. The truth is always worth finding. \ No newline at end of file diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index 421baca46..31e96bbff 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -203,6 +203,10 @@ async function main(): Promise { console.log(` Usage: copilot [--off] [--auto-assign]`); console.log(` ${BOLD}plugin${RESET} Manage plugin marketplaces`); console.log(` Usage: plugin marketplace add|remove|list|browse`); + console.log(` ${BOLD}skill${RESET} APM (Agent Package Manager) integration`); + console.log(` Usage: skill publish [] — export to APM format`); + console.log(` skill install — install from APM registry`); + console.log(` skill list — list installed skills`); console.log(` ${BOLD}export${RESET} Export squad to a portable JSON snapshot`); console.log(` Default: squad-export.json (use --out to override)`); console.log(` ${BOLD}import${RESET} Import squad from an export file`); @@ -616,6 +620,12 @@ async function main(): Promise { return; } + if (cmd === 'skill') { + const { runSkill } = await import('./cli/commands/skill.js'); + await runSkill(process.cwd(), args.slice(1)); + return; + } + if (cmd === 'copilot') { const { runCopilot } = await import('./cli/commands/copilot.js'); const isOff = args.includes('--off'); diff --git a/packages/squad-cli/src/cli/commands/skill.ts b/packages/squad-cli/src/cli/commands/skill.ts new file mode 100644 index 000000000..cd527c0e5 --- /dev/null +++ b/packages/squad-cli/src/cli/commands/skill.ts @@ -0,0 +1,568 @@ +/** + * Skill command — APM (Agent Package Manager) integration + * + * squad skill publish [] — export a skill to APM format + * squad skill install — install a skill from APM registry + * squad skill list — list installed skills + * + * APM is package.json for AI agent context: https://github.com/microsoft/apm + */ +import { readFile, writeFile, mkdir, readdir, stat } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { join, basename } from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { success, warn, info, dim, bold, DIM, BOLD, RESET } from '../core/output.js'; +import { fatal } from '../core/errors.js'; +import { detectSquadDir } from '../core/detect-squad-dir.js'; +import { ghAvailable } from '../core/gh-cli.js'; + +const execFileAsync = promisify(execFile); + +// ── Types ───────────────────────────────────────────────────────────────────── + +/** A skill entry inside apm.yml */ +export interface ApmSkill { + name: string; + description?: string; + path: string; + version?: string; + source?: string; +} + +/** Root apm.yml structure */ +export interface ApmManifest { + name: string; + version: string; + description?: string; + skills?: ApmSkill[]; + instructions?: Array<{ path: string; target: string }>; + prompts?: Array<{ path: string; target: string }>; +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +/** Parse `---\nkey: value\n---` front-matter from a Markdown file. */ +function parseFrontMatter(content: string): Record { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!match) return {}; + const result: Record = {}; + for (const line of match[1]!.split('\n')) { + const colon = line.indexOf(':'); + if (colon === -1) continue; + const key = line.slice(0, colon).trim(); + const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, ''); + result[key] = value; + } + return result; +} + +/** Read the project name from package.json, falling back to directory name. */ +async function readProjectName(dest: string): Promise { + const pkgPath = join(dest, 'package.json'); + if (existsSync(pkgPath)) { + try { + const pkg = JSON.parse(await readFile(pkgPath, 'utf8')); + if (typeof pkg.name === 'string' && pkg.name) return pkg.name; + } catch { + // ignore + } + } + return basename(dest); +} + +/** + * Resolve the skills directory, preferring .copilot/skills/ over .squad/skills/. + * Returns { dir, relPrefix } where relPrefix is the display path (e.g. '.copilot/skills'). + */ +function resolveSkillsDir(dest: string): { dir: string; relPrefix: string } { + const copilotSkills = join(dest, '.copilot', 'skills'); + if (existsSync(copilotSkills)) { + return { dir: copilotSkills, relPrefix: '.copilot/skills' }; + } + const squadDirInfo = detectSquadDir(dest); + const squadSkills = join(squadDirInfo.path, 'skills'); + if (existsSync(squadSkills)) { + return { dir: squadSkills, relPrefix: `${squadDirInfo.name}/skills` }; + } + // Default to .copilot/skills/ for new installs + return { dir: copilotSkills, relPrefix: '.copilot/skills' }; +} + +/** Collect all skills from the skills directory. */ +async function collectSkills(skillsDir: string, relPrefix: string): Promise { + if (!existsSync(skillsDir)) return []; + const skills: ApmSkill[] = []; + try { + const entries = await readdir(skillsDir); + for (const entry of entries) { + const skillFile = join(skillsDir, entry, 'skill.md'); + if (!existsSync(skillFile)) continue; + const content = await readFile(skillFile, 'utf8'); + const fm = parseFrontMatter(content); + skills.push({ + name: fm['name'] ?? entry, + description: fm['description'], + path: `${relPrefix}/${entry}/skill.md`, + version: fm['version'], + source: fm['source'], + }); + } + } catch { + // ignore read errors + } + return skills; +} + +// ── Sub-commands ────────────────────────────────────────────────────────────── + +/** + * squad skill publish [] + * + * Exports a skill (or all skills) to APM-compatible format. + * Creates/updates apm.yml at the project root. + */ +async function publish(dest: string, skillName?: string): Promise { + const { dir: skillsDir, relPrefix } = resolveSkillsDir(dest); + + if (!existsSync(skillsDir)) { + fatal('No skills directory found. Create .copilot/skills/ first.'); + } + + const projectName = await readProjectName(dest); + + if (skillName) { + // Publish a single named skill + const skillFile = join(skillsDir, skillName, 'skill.md'); + if (!existsSync(skillFile)) { + fatal(`Skill '${skillName}' not found at ${relPrefix}/${skillName}/skill.md`); + } + + const content = await readFile(skillFile, 'utf8'); + const fm = parseFrontMatter(content); + + // Build the skill's own apm.yml inside its directory + const apmSkillPath = join(skillsDir, skillName, 'apm.yml'); + const skillApm = [ + `name: ${fm['name'] ?? skillName}`, + `version: ${fm['version'] ?? '1.0.0'}`, + fm['description'] ? `description: "${fm['description']}"` : null, + ``, + `skills:`, + ` - name: ${fm['name'] ?? skillName}`, + ` path: skill.md`, + fm['description'] ? ` description: "${fm['description']}"` : null, + ] + .filter(l => l !== null) + .join('\n'); + + await writeFile(apmSkillPath, skillApm + '\n', 'utf8'); + success(`Published skill '${skillName}' to ${relPrefix}/${skillName}/apm.yml`); + info(`${DIM}APM format ready — push to GitHub to share via APM registry${RESET}`); + return; + } + + // Publish all skills → update project-level apm.yml + const skills = await collectSkills(skillsDir, relPrefix); + const apmPath = join(dest, 'apm.yml'); + + // Read existing apm.yml to preserve manually-added fields + let existing: Partial = {}; + if (existsSync(apmPath)) { + try { + // Simple YAML parse — only top-level fields we care about + const raw = await readFile(apmPath, 'utf8'); + if (raw.includes('instructions:')) { + // Preserve instructions block (just keep existing file, re-emit skills section) + info(`${DIM}Updating skills section in existing apm.yml${RESET}`); + } + existing = { name: projectName }; + } catch { + existing = {}; + } + } + + const lines = [ + `# apm.yml — Agent Package Manager manifest`, + `# See: https://github.com/microsoft/apm`, + ``, + `name: ${existing.name ?? projectName}`, + `version: 1.0.0`, + ``, + `# Skills exported from ${relPrefix}/`, + `skills:`, + ...skills.map(s => + [ + ` - name: ${s.name}`, + s.description ? ` description: "${s.description}"` : null, + ` path: ${s.path}`, + s.version ? ` version: ${s.version}` : null, + ] + .filter(l => l !== null) + .join('\n') + ), + ``, + `# Instruction files deployed by 'apm install'`, + `instructions:`, + ` - path: .squad/copilot-instructions.md`, + ` target: .github/copilot-instructions.md`, + ``, + `# Prompts deployed by 'apm install'`, + `prompts:`, + ` - path: ${relPrefix}/*/skill.md`, + ` target: .github/prompts/`, + ]; + + await writeFile(apmPath, lines.join('\n') + '\n', 'utf8'); + + if (skills.length > 0) { + success(`Published ${skills.length} skill(s) to apm.yml`); + for (const s of skills) { + info(` ${DIM}• ${s.name}${RESET}`); + } + } else { + success(`Created apm.yml (no skills found yet — add skills to ${relPrefix}/)`); + } + info(`${DIM}Run 'apm publish' to push to the APM registry${RESET}`); +} + +/** + * squad skill install + * + * Installs a skill from an APM source. + * Source formats: + * owner/repo — install all skills from a GitHub repo + * owner/repo/skill-name — install a specific skill + * https://... — URL to a raw skill.md + */ +async function install(dest: string, source: string): Promise { + if (!source) { + fatal('Usage: squad skill install [/] | '); + } + + const { dir: skillsDir, relPrefix } = resolveSkillsDir(dest); + + if (!existsSync(skillsDir)) { + await mkdir(skillsDir, { recursive: true }); + } + + // URL-based install + if (source.startsWith('http://') || source.startsWith('https://')) { + await installFromUrl(source, skillsDir, relPrefix); + return; + } + + // GitHub-based: owner/repo or owner/repo/skill-name + const parts = source.split('/'); + if (parts.length < 2) { + fatal('Invalid source. Use: owner/repo, owner/repo/skill-name, or a URL'); + } + + const owner = parts[0]!; + const repo = parts[1]!; + const skillFilter = parts.length >= 3 ? parts.slice(2).join('/') : undefined; + + if (!ghAvailable()) { + fatal('GitHub CLI (gh) is required for APM install. Install from https://cli.github.com/'); + } + + info(`${DIM}Fetching skill(s) from ${owner}/${repo}...${RESET}`); + + // Try to find skills via gh api — look for apm.yml or .copilot/skills/ + await installFromGitHub(owner, repo, skillFilter, skillsDir, dest); +} + +async function installFromUrl(url: string, skillsDir: string, relPrefix: string): Promise { + let content: string; + try { + // Node 18+ has built-in fetch + const res = await fetch(url); + if (!res.ok) { + fatal(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); + } + content = await res.text(); + } catch (err: any) { + fatal(`Failed to fetch ${url}: ${err.message}`); + return; + } + + // Derive skill name from URL + const urlName = url + .split('/') + .filter(Boolean) + .slice(-2, -1)[0] ?? + 'imported-skill'; + const skillName = urlName.replace(/[^a-z0-9-_]/gi, '-').toLowerCase(); + const skillDir = join(skillsDir, skillName); + + await mkdir(skillDir, { recursive: true }); + await writeFile(join(skillDir, 'skill.md'), content, 'utf8'); + + success(`Installed skill '${skillName}' from URL`); + info(` ${DIM}Location: ${relPrefix}/${skillName}/skill.md${RESET}`); +} + +async function installFromGitHub( + owner: string, + repo: string, + skillFilter: string | undefined, + skillsDir: string, + dest: string, +): Promise { + // First, try to read the apm.yml from the repo to discover skills + let apmContent: string | null = null; + try { + const { stdout } = await execFileAsync('gh', [ + 'api', + `repos/${owner}/${repo}/contents/apm.yml`, + '--jq', '.content', + ]); + apmContent = Buffer.from(stdout.trim(), 'base64').toString('utf8'); + } catch { + // No apm.yml — fall back to scanning skills directories + } + + if (!apmContent) { + // Fall back: try skills directories via API + await installSkillsFromSquadDir(owner, repo, skillFilter, skillsDir); + return; + } + + // Parse the apm.yml to find skill paths + const skillPaths: Array<{ name: string; path: string }> = []; + const lines = apmContent.split('\n'); + let inSkills = false; + let currentSkill: { name?: string; path?: string } = {}; + + for (const line of lines) { + if (line.trim() === 'skills:') { + inSkills = true; + continue; + } + if (inSkills && line.match(/^[a-z]/i) && !line.startsWith(' ')) { + // New top-level key — exit skills section + if (currentSkill.name && currentSkill.path) skillPaths.push(currentSkill as { name: string; path: string }); + currentSkill = {}; + inSkills = false; + } + if (inSkills) { + const nameMatch = line.match(/^\s+- name:\s*(.+)$/); + const pathMatch = line.match(/^\s+path:\s*(.+)$/); + if (nameMatch) { + if (currentSkill.name && currentSkill.path) skillPaths.push(currentSkill as { name: string; path: string }); + currentSkill = { name: nameMatch[1]!.trim() }; + } + if (pathMatch) currentSkill.path = pathMatch[1]!.trim(); + } + } + if (currentSkill.name && currentSkill.path) skillPaths.push(currentSkill as { name: string; path: string }); + + // Filter by skill name if specified + const toInstall = skillFilter + ? skillPaths.filter(s => s.name === skillFilter || s.path.includes(skillFilter)) + : skillPaths; + + if (toInstall.length === 0) { + if (skillFilter) { + fatal(`Skill '${skillFilter}' not found in ${owner}/${repo}'s apm.yml`); + } else { + warn(`No skills declared in ${owner}/${repo}'s apm.yml`); + return; + } + } + + let installed = 0; + for (const skill of toInstall) { + try { + const { stdout: rawContent } = await execFileAsync('gh', [ + 'api', + `repos/${owner}/${repo}/contents/${skill.path}`, + '--jq', '.content', + ]); + const content = Buffer.from(rawContent.trim(), 'base64').toString('utf8'); + const skillDir = join(skillsDir, skill.name); + await mkdir(skillDir, { recursive: true }); + await writeFile(join(skillDir, 'skill.md'), content, 'utf8'); + + // Write a source metadata file so we can track origin + const meta = { + source: `${owner}/${repo}`, + path: skill.path, + installed_at: new Date().toISOString(), + }; + await writeFile(join(skillDir, '.apm-source.json'), JSON.stringify(meta, null, 2) + '\n', 'utf8'); + + success(`Installed skill '${skill.name}'`); + info(` ${DIM}Source: ${owner}/${repo}${skill.path}${RESET}`); + installed++; + } catch (err: any) { + warn(`Failed to install '${skill.name}': ${err.message}`); + } + } + + if (installed > 0) { + info(`\n${DIM}Run 'squad skill publish' to refresh apm.yml with newly installed skills${RESET}`); + } +} + +async function installSkillsFromSquadDir( + owner: string, + repo: string, + skillFilter: string | undefined, + skillsDir: string, +): Promise { + // Try .copilot/skills/ (standard) then .squad/skills/ (legacy) then bare skills/ + const candidates = ['.copilot/skills', '.squad/skills', 'skills']; + let entries: Array<{ name: string; path: string; type: string }> = []; + + for (const candidate of candidates) { + try { + const { stdout } = await execFileAsync('gh', [ + 'api', + `repos/${owner}/${repo}/contents/${candidate}`, + '--jq', '[.[] | {name: .name, path: .path, type: .type}]', + ]); + entries = JSON.parse(stdout); + break; + } catch { + continue; + } + } + + if (entries.length === 0) { + fatal(`No skills directory found in ${owner}/${repo}. The repo may not use APM or Squad conventions.`); + } + + const dirs = entries.filter(e => e.type === 'dir'); + const toInstall = skillFilter ? dirs.filter(d => d.name === skillFilter) : dirs; + + if (toInstall.length === 0) { + if (skillFilter) { + fatal(`Skill '${skillFilter}' not found in ${owner}/${repo}`); + } else { + warn(`No skill directories found in ${owner}/${repo}`); + return; + } + } + + let installed = 0; + for (const dir of toInstall) { + const skillFilePath = `${dir.path}/skill.md`; + try { + const { stdout: rawContent } = await execFileAsync('gh', [ + 'api', + `repos/${owner}/${repo}/contents/${skillFilePath}`, + '--jq', '.content', + ]); + const content = Buffer.from(rawContent.trim(), 'base64').toString('utf8'); + const skillDir = join(skillsDir, dir.name); + await mkdir(skillDir, { recursive: true }); + await writeFile(join(skillDir, 'skill.md'), content, 'utf8'); + + const meta = { + source: `${owner}/${repo}`, + path: skillFilePath, + installed_at: new Date().toISOString(), + }; + await writeFile(join(skillDir, '.apm-source.json'), JSON.stringify(meta, null, 2) + '\n', 'utf8'); + + success(`Installed skill '${dir.name}'`); + info(` ${DIM}Source: ${owner}/${repo}/${skillFilePath}${RESET}`); + installed++; + } catch { + warn(`Skipped '${dir.name}' — no skill.md found`); + } + } + + if (installed > 0) { + info(`\n${DIM}Run 'squad skill publish' to refresh apm.yml${RESET}`); + } +} + +/** + * squad skill list + * + * Lists installed skills from .copilot/skills/ (or legacy .squad/skills/). + */ +async function listSkills(dest: string): Promise { + const { dir: skillsDir, relPrefix } = resolveSkillsDir(dest); + + if (!existsSync(skillsDir)) { + info('No skills directory found. Run "squad init" first or create .copilot/skills/'); + return; + } + + const skills = await collectSkills(skillsDir, relPrefix); + + if (skills.length === 0) { + info(`${DIM}No skills installed yet.${RESET}`); + info(`Install a skill: squad skill install `); + return; + } + + console.log(`\n${BOLD}Installed Skills${RESET}\n`); + for (const skill of skills) { + const metaPath = join(skillsDir, skill.name, '.apm-source.json'); + let sourceNote = ''; + if (existsSync(metaPath)) { + try { + const meta = JSON.parse(await readFile(metaPath, 'utf8')); + sourceNote = ` ${DIM}(from ${meta.source})${RESET}`; + } catch { + // ignore + } + } + console.log(` ${BOLD}${skill.name}${RESET}${sourceNote}`); + if (skill.description) { + console.log(` ${DIM}${skill.description}${RESET}`); + } + } + console.log(); +} + +// ── Main export ─────────────────────────────────────────────────────────────── + +/** + * Entry point for `squad skill` command. + * + * @param dest - Working directory (usually process.cwd()) + * @param args - Remaining args after `squad skill` + */ +export async function runSkill(dest: string, args: string[]): Promise { + const subCmd = args[0]; + + if (!subCmd || subCmd === 'help' || subCmd === '--help') { + console.log(`\n${BOLD}squad skill${RESET} — APM (Agent Package Manager) integration\n`); + console.log(`Usage:`); + console.log(` squad skill publish [] Export skill(s) to APM format (apm.yml)`); + console.log(` squad skill install Install from APM registry`); + console.log(` squad skill list List installed skills`); + console.log(`\nInstall sources:`); + console.log(` owner/repo All skills from a GitHub repo`); + console.log(` owner/repo/skill-name A specific skill from a GitHub repo`); + console.log(` https://... A direct URL to a skill.md file`); + console.log(`\nAPM registry: https://github.com/microsoft/apm\n`); + return; + } + + switch (subCmd) { + case 'publish': { + const skillName = args[1]; + await publish(dest, skillName); + break; + } + case 'install': { + const source = args[1]; + if (!source) { + fatal('Usage: squad skill install [/] | '); + } + await install(dest, source); + break; + } + case 'list': + await listSkills(dest); + break; + default: + fatal(`Unknown skill subcommand: ${subCmd}\nRun 'squad skill help' for usage.`); + } +} diff --git a/packages/squad-cli/src/cli/core/init.ts b/packages/squad-cli/src/cli/core/init.ts index 8c73e0ae5..22ec44af3 100644 --- a/packages/squad-cli/src/cli/core/init.ts +++ b/packages/squad-cli/src/cli/core/init.ts @@ -3,6 +3,7 @@ * Scaffolds a new Squad project with templates, workflows, and directory structure */ +import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { FSStorageProvider } from '@bradygaster/squad-sdk'; @@ -17,6 +18,48 @@ const storage = new FSStorageProvider(); const CYAN = '\x1b[36m'; +// ── APM manifest generation ─────────────────────────────────────────────────── + +/** + * Generate a starter apm.yml at the project root. + * + * Only creates the file if it doesn't already exist, so repeat `squad init` + * invocations are safe (skipExisting semantics mirror the SDK's behaviour). + * + * APM (Agent Package Manager) is package.json for AI agent context. + * See: https://github.com/microsoft/apm + */ +export function generateApmYml(dest: string, projectName: string): void { + const apmPath = path.join(dest, 'apm.yml'); + if (fs.existsSync(apmPath)) return; // skip if already present + + const content = [ + `# apm.yml — Agent Package Manager manifest`, + `# See: https://github.com/microsoft/apm`, + `#`, + `# This file makes your Squad skills versioned, portable, and community-shareable.`, + `# Run 'squad skill publish' to populate the skills section after adding skills.`, + ``, + `name: ${projectName}`, + `version: 1.0.0`, + ``, + `# Skills — add entries here or run 'squad skill publish' to auto-populate`, + `skills: []`, + ``, + `# Instruction files deployed by 'apm install'`, + `instructions:`, + ` - path: .squad/copilot-instructions.md`, + ` target: .github/copilot-instructions.md`, + ``, + `# Prompts deployed by 'apm install'`, + `prompts:`, + ` - path: .squad/skills/*/skill.md`, + ` target: .github/prompts/`, + ].join('\n') + '\n'; + + fs.writeFileSync(apmPath, content, 'utf8'); +} + /** * Detect if the target directory is inside a parent git repo. * Returns the normalized git root path if a parent repo is detected, @@ -287,6 +330,19 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi console.log(`${DIM}${file} already exists — skipping${RESET}`); } + // ── APM manifest ───────────────────────────────────────────────────────────── + // Generate apm.yml so skills are ready for APM publishing/installation. + // Uses the project name derived from the directory basename. + const projectName = path.basename(dest) || 'my-project'; + const apmPath = path.join(dest, 'apm.yml'); + const apmAlreadyExisted = fs.existsSync(apmPath); + generateApmYml(dest, projectName); + if (!apmAlreadyExisted) { + success('apm.yml'); + } else { + console.log(`${DIM}apm.yml already exists — skipping${RESET}`); + } + // ── Celebration ceremony ────────────────────────────────────────── console.log(); await typewrite(`${CYAN}${BOLD}◆ SQUAD${RESET}`, 10); diff --git a/packages/squad-cli/src/cli/index.ts b/packages/squad-cli/src/cli/index.ts index b56cb21a0..191439e85 100644 --- a/packages/squad-cli/src/cli/index.ts +++ b/packages/squad-cli/src/cli/index.ts @@ -40,3 +40,5 @@ export { runExport } from './commands/export.js'; export { runImport } from './commands/import.js'; export { splitHistory } from './core/history-split.js'; export { discoverCommand, delegateCommand } from './commands/cross-squad.js'; +export { runSkill, type ApmSkill, type ApmManifest } from './commands/skill.js'; +export { generateApmYml } from './core/init.js';