From 544ae83714d87bbf14ab7d2d73e0385590501162 Mon Sep 17 00:00:00 2001 From: The Rabak Date: Mon, 1 Jun 2026 07:07:43 +0300 Subject: [PATCH] Update triage to workflows:triage Rename the portable triage workflow to workflows:triage, strengthen the research-first swarm orchestration contract, and remove stale renamed artifacts from generated and installed surfaces across Claude, Copilot, OpenViking, and OpenCode.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .claude-plugin/marketplace.json | 2 +- .github/skills/file-todos/SKILL.md | 4 +- .github/skills/triage/SKILL.md | 312 --------------- .github/skills/workflows-review/SKILL.md | 2 +- .github/skills/workflows-triage/SKILL.md | 361 +++++++++++++++++ .../architecture-improvement-prompt.md | 146 +++++++ .../references/execution-agent-prompt.md | 86 +++++ .../references/execution-shape.md | 98 +++++ .../references/orchestration-protocol.md | 39 ++ .../references/quality-review-prompt.md | 98 +++++ .../references/spec-review-prompt.md | 104 +++++ .../references/tdd-evidence-contract.md | 44 +++ .../references/ticket-execution-contract.md | 149 +++++++ .../references/ticketization-contract.md | 166 ++++++++ .../references/vertical-slice-architecture.md | 72 ++++ .../.claude-plugin/plugin.json | 2 +- plugins/compound-engineering/CHANGELOG.md | 7 + plugins/compound-engineering/README.md | 2 +- .../compound-engineering/commands/triage.md | 311 --------------- .../architecture-improvement-prompt.md | 146 +++++++ .../references/execution-agent-prompt.md | 233 +++-------- .../workflows/references/execution-shape.md | 98 +++++ .../references/orchestration-protocol.md | 39 ++ .../references/quality-review-prompt.md | 176 +++++---- .../references/spec-review-prompt.md | 186 +++++---- .../references/tdd-evidence-contract.md | 44 +++ .../references/ticket-execution-contract.md | 149 +++++++ .../references/ticketization-contract.md | 166 ++++++++ .../references/vertical-slice-architecture.md | 72 ++++ .../commands/workflows/review.md | 2 +- .../commands/workflows/triage.md | 360 +++++++++++++++++ .../skills/file-todos/SKILL.md | 4 +- .../compound-engineering/commands/triage.md | 321 ---------------- .../commands/workflows/review.md | 2 +- .../commands/workflows/triage.md | 362 ++++++++++++++++++ portable/compound-engineering/plugin.yaml | 2 +- .../skills/file-todos/SKILL.md | 4 +- src/commands/build.ts | 13 + src/commands/sync-ov.ts | 157 +++++++- src/converters/claude-to-codex.ts | 3 +- src/converters/claude-to-opencode.ts | 311 ++++++++------- src/converters/claude-to-pi.ts | 4 +- src/targets/claude.ts | 49 ++- src/targets/codex.ts | 35 +- src/targets/copilot.ts | 76 ++-- src/targets/droid.ts | 24 ++ src/targets/gemini.ts | 37 +- src/targets/kiro.ts | 224 ++++++----- src/targets/opencode.ts | 356 +++++++++++------ src/targets/pi.ts | 38 +- src/utils/managed-output.ts | 81 ++++ tests/claude-writer.test.ts | 28 ++ tests/cli.test.ts | 113 ++++++ tests/codex-converter.test.ts | 14 +- tests/codex-writer.test.ts | 36 +- tests/converter.test.ts | 6 +- tests/copilot-writer.test.ts | 25 ++ tests/droid-writer.test.ts | 25 ++ tests/gemini-writer.test.ts | 34 +- tests/opencode-writer.test.ts | 72 +++- tests/pi-converter.test.ts | 4 +- tests/pi-writer.test.ts | 32 +- 62 files changed, 4420 insertions(+), 1748 deletions(-) delete mode 100644 .github/skills/triage/SKILL.md create mode 100644 .github/skills/workflows-triage/SKILL.md create mode 100644 .github/skills/workflows-triage/references/architecture-improvement-prompt.md create mode 100644 .github/skills/workflows-triage/references/execution-agent-prompt.md create mode 100644 .github/skills/workflows-triage/references/execution-shape.md create mode 100644 .github/skills/workflows-triage/references/orchestration-protocol.md create mode 100644 .github/skills/workflows-triage/references/quality-review-prompt.md create mode 100644 .github/skills/workflows-triage/references/spec-review-prompt.md create mode 100644 .github/skills/workflows-triage/references/tdd-evidence-contract.md create mode 100644 .github/skills/workflows-triage/references/ticket-execution-contract.md create mode 100644 .github/skills/workflows-triage/references/ticketization-contract.md create mode 100644 .github/skills/workflows-triage/references/vertical-slice-architecture.md delete mode 100644 plugins/compound-engineering/commands/triage.md create mode 100644 plugins/compound-engineering/commands/workflows/references/architecture-improvement-prompt.md create mode 100644 plugins/compound-engineering/commands/workflows/references/execution-shape.md create mode 100644 plugins/compound-engineering/commands/workflows/references/orchestration-protocol.md create mode 100644 plugins/compound-engineering/commands/workflows/references/tdd-evidence-contract.md create mode 100644 plugins/compound-engineering/commands/workflows/references/ticket-execution-contract.md create mode 100644 plugins/compound-engineering/commands/workflows/references/ticketization-contract.md create mode 100644 plugins/compound-engineering/commands/workflows/references/vertical-slice-architecture.md create mode 100644 plugins/compound-engineering/commands/workflows/triage.md delete mode 100644 portable/compound-engineering/commands/triage.md create mode 100644 portable/compound-engineering/commands/workflows/triage.md create mode 100644 src/utils/managed-output.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2649314..c0f965c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ { "name": "compound-engineering", "description": "OpenCode-first AI-powered development tools. Includes 34 specialized agents, 28 commands, and 26 skills spanning code review, research, design, and workflow automation, with generated Copilot support and Claude Code compatibility outputs.", - "version": "4.13.0", + "version": "4.14.0", "author": { "name": "The Rabak", "email": "arielvaron@gmail.com", diff --git a/.github/skills/file-todos/SKILL.md b/.github/skills/file-todos/SKILL.md index 194163c..ec82a59 100644 --- a/.github/skills/file-todos/SKILL.md +++ b/.github/skills/file-todos/SKILL.md @@ -115,7 +115,7 @@ dependencies: ["001"] # Issue IDs this is blocked by - Adjust priority if different from initial assessment 4. Deferred todos stay in `pending` status -**Use slash command:** `/triage` for interactive approval workflow +**Use slash command:** `/workflows-triage` for research-backed interactive approval workflow ### Managing Dependencies @@ -185,7 +185,7 @@ Work logs serve as: | Trigger | Flow | Tool | |---------|------|------| -| Code review | `/workflows-review` → Findings → `/triage` → Todos | Review agent + skill | +| Code review | `/workflows-review` → Findings → `/workflows-triage` → Todos | Review agent + skill | | PR comments | `/resolve_pr_parallel` → Individual fixes → Todos | gh CLI + skill | | Code TODOs | `/resolve_todo_parallel` → Fixes + Complex todos | Agent + skill | | Planning | Brainstorm → Create todo → Work → Complete | Skill | diff --git a/.github/skills/triage/SKILL.md b/.github/skills/triage/SKILL.md deleted file mode 100644 index 7c0f655..0000000 --- a/.github/skills/triage/SKILL.md +++ /dev/null @@ -1,312 +0,0 @@ ---- -name: triage -description: Triage and categorize findings for the CLI todo system ---- - -## Arguments -[findings list or source type] - -- First set the /model to Haiku -- Then read all pending todos in the todos/ directory - -Present all findings, decisions, or issues here one by one for triage. The goal is to go through each item and decide whether to add it to the CLI todo system. - -**IMPORTANT: DO NOT CODE ANYTHING DURING TRIAGE!** - -This command is for: - -- Triaging code review findings -- Processing security audit results -- Reviewing performance analysis -- Handling any other categorized findings that need tracking - -## Workflow - -### Step 1: Present Each Finding - -For each finding, present in this format: - -``` ---- -Issue #X: [Brief Title] - -Severity: 🔴 P1 (CRITICAL) / 🟡 P2 (IMPORTANT) / 🔵 P3 (NICE-TO-HAVE) - -Category: [Security/Performance/Architecture/Bug/Feature/etc.] - -Description: -[Detailed explanation of the issue or improvement] - -Location: [file_path:line_number] - -Problem Scenario: -[Step by step what's wrong or could happen] - -Proposed Solution: -[How to fix it] - -Estimated Effort: [Small (< 2 hours) / Medium (2-8 hours) / Large (> 8 hours)] - ---- -Do you want to add this to the todo list? -1. yes - create todo file -2. next - skip this item -3. custom - modify before creating -``` - -### Step 2: Handle User Decision - -**When user says "yes":** - -1. **Update existing todo file** (if it exists) or **Create new filename:** - - If todo already exists (from code review): - - - Rename file from `{id}-pending-{priority}-{desc}.md` → `{id}-ready-{priority}-{desc}.md` - - Update YAML frontmatter: `status: pending` → `status: ready` - - Keep issue_id, priority, and description unchanged - - If creating new todo: - - ``` - {next_id}-ready-{priority}-{brief-description}.md - ``` - - Priority mapping: - - - 🔴 P1 (CRITICAL) → `p1` - - 🟡 P2 (IMPORTANT) → `p2` - - 🔵 P3 (NICE-TO-HAVE) → `p3` - - Example: `042-ready-p1-transaction-boundaries.md` - -2. **Update YAML frontmatter:** - - ```yaml - --- - status: ready # IMPORTANT: Change from "pending" to "ready" - priority: p1 # or p2, p3 based on severity - issue_id: "042" - tags: [category, relevant-tags] - dependencies: [] - --- - ``` - -3. **Populate or update the file:** - - ```yaml - # [Issue Title] - - ## Problem Statement - [Description from finding] - - ## Findings - - [Key discoveries] - - Location: [file_path:line_number] - - [Scenario details] - - ## Proposed Solutions - - ### Option 1: [Primary solution] - - **Pros**: [Benefits] - - **Cons**: [Drawbacks if any] - - **Effort**: [Small/Medium/Large] - - **Risk**: [Low/Medium/High] - - ## Recommended Action - [Filled during triage - specific action plan] - - ## Technical Details - - **Affected Files**: [List files] - - **Related Components**: [Components affected] - - **Database Changes**: [Yes/No - describe if yes] - - ## Resources - - Original finding: [Source of this issue] - - Related issues: [If any] - - ## Acceptance Criteria - - [ ] [Specific success criteria] - - [ ] Tests pass - - [ ] Code reviewed - - ## Work Log - - ### {date} - Approved for Work - **By:** Claude Triage System - **Actions:** - - Issue approved during triage session - - Status changed from pending → ready - - Ready to be picked up and worked on - - **Learnings:** - - [Context and insights] - - ## Notes - Source: Triage session on {date} - ``` - -4. **Confirm approval:** "✅ Approved: `{new_filename}` (Issue #{issue_id}) - Status: **ready** → Ready to work on" - -**When user says "next":** - -- **Delete the todo file** - Remove it from todos/ directory since it's not relevant -- Skip to the next item -- Track skipped items for summary - -**When user says "custom":** - -- Ask what to modify (priority, description, details) -- Update the information -- Present revised version -- Ask again: yes/next/custom - -### Step 3: Continue Until All Processed - -- Process all items one by one -- Track using TodoWrite for visibility -- Don't wait for approval between items - keep moving - -### Step 4: Final Summary - -After all items processed: - -````markdown -## Triage Complete - -**Total Items:** [X] **Todos Approved (ready):** [Y] **Skipped:** [Z] - -### Approved Todos (Ready for Work): - -- `042-ready-p1-transaction-boundaries.md` - Transaction boundary issue -- `043-ready-p2-cache-optimization.md` - Cache performance improvement ... - -### Skipped Items (Deleted): - -- Item #5: [reason] - Removed from todos/ -- Item #12: [reason] - Removed from todos/ - -### Summary of Changes Made: - -During triage, the following status updates occurred: - -- **Pending → Ready:** Filenames and frontmatter updated to reflect approved status -- **Deleted:** Todo files for skipped findings removed from todos/ directory -- Each approved file now has `status: ready` in YAML frontmatter - -### Next Steps: - -1. View approved todos ready for work: - ```bash - ls todos/*-ready-*.md - ``` -```` - -2. Start work on approved items: - - ```bash - /resolve_todo_parallel # Work on multiple approved items efficiently - ``` - -3. Or pick individual items to work on - -4. As you work, update todo status: - - Ready → In Progress (in your local context as you work) - - In Progress → Complete (rename file: ready → complete, update frontmatter) - -``` - -## Example Response Format - -``` - ---- - -Issue #5: Missing Transaction Boundaries for Multi-Step Operations - -Severity: 🔴 P1 (CRITICAL) - -Category: Data Integrity / Security - -Description: The google_oauth2_connected callback in GoogleOauthCallbacks concern performs multiple database operations without transaction protection. If any step fails midway, the database is left in an inconsistent state. - -Location: app/Services/OAuthService.php:13-50 - -Problem Scenario: - -1. User.update succeeds (email changed) -2. Account.save! fails (validation error) -3. Result: User has changed email but no associated Account -4. Next login attempt fails completely - -Operations Without Transaction: - -- User confirmation (line 13) -- Waitlist removal (line 14) -- User profile update (line 21-23) -- Account creation (line 28-37) -- Avatar attachment (line 39-45) -- Journey creation (line 47) - -Proposed Solution: Wrap all operations in ApplicationRecord.transaction do ... end block - -Estimated Effort: Small (30 minutes) - ---- - -Do you want to add this to the todo list? - -1. yes - create todo file -2. next - skip this item -3. custom - modify before creating - -``` - -## Important Implementation Details - -### Status Transitions During Triage - -**When "yes" is selected:** -1. Rename file: `{id}-pending-{priority}-{desc}.md` → `{id}-ready-{priority}-{desc}.md` -2. Update YAML frontmatter: `status: pending` → `status: ready` -3. Update Work Log with triage approval entry -4. Confirm: "✅ Approved: `{filename}` (Issue #{issue_id}) - Status: **ready**" - -**When "next" is selected:** -1. Delete the todo file from todos/ directory -2. Skip to next item -3. No file remains in the system - -### Progress Tracking - -Every time you present a todo as a header, include: -- **Progress:** X/Y completed (e.g., "3/10 completed") -- **Estimated time remaining:** Based on how quickly you're progressing -- **Pacing:** Monitor time per finding and adjust estimate accordingly - -Example: -``` - -Progress: 3/10 completed | Estimated time: ~2 minutes remaining - -``` - -### Do Not Code During Triage - -- ✅ Present findings -- ✅ Make yes/next/custom decisions -- ✅ Update todo files (rename, frontmatter, work log) -- ❌ Do NOT implement fixes or write code -- ❌ Do NOT add detailed implementation details -- ❌ That's for /resolve_todo_parallel phase -``` - -When done give these options - -```markdown -What would you like to do next? - -1. run /resolve_todo_parallel to resolve the todos -2. commit the todos -3. nothing, go chill -``` diff --git a/.github/skills/workflows-review/SKILL.md b/.github/skills/workflows-review/SKILL.md index 41484a8..4d202ef 100644 --- a/.github/skills/workflows-review/SKILL.md +++ b/.github/skills/workflows-review/SKILL.md @@ -614,7 +614,7 @@ After creating all todo files, present comprehensive summary: 3. **Triage All Todos**: ```bash ls todos/*-pending-*.md # View all pending todos - /triage # Use slash command for interactive triage + /workflows-triage # Use slash command for interactive triage ``` ```` diff --git a/.github/skills/workflows-triage/SKILL.md b/.github/skills/workflows-triage/SKILL.md new file mode 100644 index 0000000..d5389c3 --- /dev/null +++ b/.github/skills/workflows-triage/SKILL.md @@ -0,0 +1,361 @@ +--- +name: workflows-triage +description: Research each todo, resolve decisions one-by-one, write chosen actions into todo files, then execute safe batches in swarm mode with execution-agent +--- + +## Arguments +[todo range or scope] + +- Read all target todo files before asking decisions. +- Keep main context as orchestration, research, decision, and validation space; execution agents do implementation. +- Do not start execution until all targeted todos are fully triaged and updated in place. + +Use this command when you need to process review todos end-to-end: + +1. research each todo against the current codebase, +2. present grounded action options to the user, +3. resolve open questions one-by-one, +4. write the selected action and context back into every todo file, +5. build safe swarm batches with dedicated execution scopes, +6. orchestrate execution-agent runs with full packets, +7. validate each result independently, +8. close statuses cleanly. + +## Core Rule Set + +**IMPORTANT: During research and decision phases, DO NOT implement code fixes.** + +This command is for: + +- Review-todo triage and decision closure +- Converting vague todos into researched, executable units +- Writing action-ready todo files before execution starts +- Swarm orchestration with strict scope fences and independent validation + +## Workflow + +### Step 1: Bootstrap and Scope + +1. Load project context and memory first. +2. Identify target todos from user scope (range, priority, status, or "all open"). +3. Build a deterministic queue sorted by todo id. +4. Confirm dependency order before any execution planning. + +Recommended checks: + +```bash +rg '^status:\s*(pending|in_progress|ready|blocked)' todos/*.md +``` + +### Step 2: Read and Research Every Target Todo Before Asking Decisions + +Read each todo fully before asking any question. Then do focused repository research for that todo so your proposed actions are grounded in reality rather than guesswork. + +Research for each todo should cover: + +- Problem statement quality and missing acceptance criteria +- Relevant code paths, modules, tests, docs, and prior patterns +- Dependency and ordering concerns +- Likely implementation surface and blast radius +- Validation expectations and evidence commands +- Risks, blockers, or ambiguity that should shape the decision + +The research pass must produce concrete action options. Do not present shallow "maybe do X" suggestions; each option must reflect what you found in the codebase. + +Present triage output per todo in this format: + +```markdown +--- +Todo #NNN: [Title] + +Status: [pending/in_progress/ready/blocked] +Priority: [p1/p2/p3] +Dependencies: [none/list] + +Research Summary: +- Relevant files: [file], [file] +- Existing pattern: [summary] +- Validation surface: [tests/checks] + +Possible Actions: +1. [Action name] - [what changes] - [main tradeoff] +2. [Action name] - [what changes] - [main tradeoff] +3. [Action name] - [what changes] - [main tradeoff] + +Recommended Action: +- [Suggested action and why] + +Open Decisions: +1. [Decision question] +2. [Decision question] + +Execution Risks: +- [Risk] +--- +``` + +### Step 3: Resolve Open Decisions One Question at a Time + +Ask only one question at a time. Do not batch decisions. + +Decision prompt format: + +```markdown +Decision for Todo #NNN: +[Clear question] + +Research-backed options: +1. [option A] +2. [option B] +3. [option C] + +Recommended: [option] +Why: [brief evidence-based reason] +``` + +Rules: + +- Wait for explicit answer before asking the next question. +- If user gives freeform direction, normalize it and confirm in one sentence. +- No implementation starts until the full target set has been triaged and all required decisions are resolved. + +### Step 4: Write the Selected Action Back into Every Todo File + +Update todo files immediately after each resolved decision so the todo becomes the authoritative execution packet seed. + +Expected updates: + +1. Add or refresh `## Recommended Action` +2. Record the chosen action, scope fence, likely files, and validation commands +3. Append `## Work Log` entry with decision outcome and research summary +4. Keep status accurate: + - stays `pending` after triage-only updates + - moves to `in_progress` only when the todo is dispatched to execution + - moves to `done` only after independent validation passes + +Work log template: + +```markdown +### YYYY-MM-DD - Triage decisions recorded + +**By:** @user + +**Actions:** +- [Selected action recorded] +- [Scope fence or dependency decision recorded] + +**Learnings:** +- [Relevant codebase pattern discovered] +- [Why this direction was chosen] +``` + +### Step 5: Build a Swarm Plan After All Todos Are Triaged + +Do not execute immediately after the first todo is approved. First finish triage for the full target set, then build a safe execution plan. + +Create: + +1. A dependency graph across all targeted todos +2. A file-overlap and blast-radius check +3. Parallel-safe batches where each todo has a dedicated scope +4. A fallback serialization rule for any items that overlap too much + +Only put todos in the same swarm batch when: + +- their dependencies are already done or outside the target set, +- their likely file surfaces do not materially overlap, +- they do not require the same migration, schema, or shared contract change, +- their validation can run independently. + +If parallel safety is unclear, split the work into smaller or serial batches. + +### Step 6: Build a Full Execution Packet per Todo + +Before launching execution-agent, prepare a full context packet. Do not send minimal prompts. + +Every packet must include: + +- Repository path and branch +- Exact todo file path and title +- Goal and acceptance criteria +- Research summary and selected action +- Explicitly resolved decisions +- Scope fence (what not to change) +- Likely files and tests +- Validation expectations +- Reporting contract (what execution-agent must return) + +Execution packet skeleton: + +```markdown +AGENT_TEMPLATE loaded via local agent repository. Follow exactly. + +Repository: [path] +Branch: [branch] + +## Your Unit +Todo file: [path] +Title: [title] +Goal: [goal] + +## Selected Action +- [final action] + +## Research Summary +- [existing pattern] +- [relevant files] +- [validation surface] + +## Decisions (Final) +- [decision] +- [decision] + +## Architecture Handoff +Acceptance criteria: +1. [...] +2. [...] + +Scope fence: +- [...] +- [...] + +Likely files: +- [file] +- [file] + +Validation contract: +- run/update tests relevant to this todo +- report red/green/post-refactor evidence +- provide changed files and rationale +``` + +### Step 7: Execute in Swarm Mode with Dedicated Scopes + +Execute by safe batch, not by one giant parallel blast and not by immediate one-off serial runs. + +For each safe batch: + +1. Set each batch todo status to `in_progress` +2. Dispatch one `execution-agent` per todo with its full packet and dedicated scope +3. Keep the orchestrator focused on batch coordination, validation, and status integrity +4. Wait for every agent in the batch to complete +5. Review each execution report separately +6. Validate each todo independently from the orchestration context +7. Mark validated todos `done`; keep failures `in_progress` or `blocked` with exact reasons +8. Only advance dependent batches after prerequisites are truly complete + +Hard rules: + +- Every execution-agent owns exactly one todo scope at a time. +- Do not merge multiple todos into one agent prompt. +- Do not allow two agents to edit the same unstable surface unless the batch plan explicitly proves safety. +- If one todo in a batch fails, do not discard successful siblings; validate and close each todo independently. + +### Step 8: Orchestration-Side Validation (Mandatory) + +Never rely only on subagent self-report. Orchestrator validates. + +Validation checklist per todo: + +- targeted tests pass for the changed area +- expected files actually changed +- scope fence was respected +- todo acceptance criteria are now true +- todo status, selected action, and work log are updated + +Completion log template: + +```markdown +### YYYY-MM-DD - Execution completed + +**Actions:** +- [Implemented change summary] + +**Validation:** +- `command 1` +- `command 2` +``` + +### Step 9: Final Sweep and Completion Report + +After all todos are processed: + +1. Check no target todo remains stale without explanation +2. Report done, blocked, and in-progress counts +3. List any follow-up work created by the execution batches + +Final report format: + +```markdown +## Triage + Swarm Execution Complete + +**Total Targeted:** [X] +**Done:** [Y] +**Still Open:** [Z] + +### Done +- [todo-id] [title] + +### Still Open / Blocked +- [todo-id] [reason] + +### Validation Run +- [key command] +- [key command] +``` + +## Important Implementation Details + +### Status Discipline + +- `pending`: triaged and documented, but not executing yet +- `in_progress`: actively executing or retrying +- `done`: validated and closed +- `blocked`: cannot continue; include concrete blocker + +### Context Discipline + +- Main context owns research, decisions, swarm planning, validation, and status integrity. +- execution-agent owns code edits for one scoped todo at a time. +- Do not duplicate the same implementation work in both contexts. + +### Decision Discipline + +- Research before presenting options. +- Ask one decision question at a time. +- Write answers into the todo immediately. +- Never "assume defaults" if user decision is explicitly required. + +### Swarm Planning Quality Bar + +Bad swarm plan: +- "Run all open todos in parallel" + +Good swarm plan: +- groups only dependency-safe, low-overlap todos into the same batch +- gives each todo its own packet, scope fence, and validation path +- serializes risky overlaps instead of pretending they are safe + +### Do / Don't + +- ✅ Do research each todo before presenting options. +- ✅ Do capture possible actions grounded in the current codebase. +- ✅ Do update all targeted todo files before starting execution. +- ✅ Do batch execution in swarm mode only when scopes are truly parallel-safe. +- ✅ Do validate each completed todo independently. +- ❌ Don't code during the research and decision phase. +- ❌ Don't ask multiple decisions in one message. +- ❌ Don't start execution before the target set is fully triaged. +- ❌ Don't mark done before orchestration-side validation. +- ❌ Don't drop selected actions or research findings from todo logs. + +## Done Options + +When all targeted todos are processed, end with: + +```markdown +What would you like to do next? + +1. commit and push current completed todo batch +2. stop here +``` diff --git a/.github/skills/workflows-triage/references/architecture-improvement-prompt.md b/.github/skills/workflows-triage/references/architecture-improvement-prompt.md new file mode 100644 index 0000000..6dede43 --- /dev/null +++ b/.github/skills/workflows-triage/references/architecture-improvement-prompt.md @@ -0,0 +1,146 @@ +--- +{} +--- + +# Architecture Improvement Artifact Contract + +This template is consumed by the `workflows:architecture` phase. It is a **reference document**, not an invocable command. + +The purpose of the artifact is to make architectural choices explicit before `/deepen-plan` and `/workflows-work` continue. Use the vocabulary below consistently so downstream agents inherit a shared contract instead of guessing from oral tradition. + +## Mandatory inputs + +A valid architecture improvement pass requires: + +- `plan_ref` -- the plan being hardened +- Problem Narrative +- User Story +- Success Criteria +- Architectural Context +- Current plan phases/tasks +- The vertical-slice module contract from `vertical-slice-architecture.md` +- `brainstorm_ref`, constitution context, and source docs when available + +If any of the first five are missing, stop and report the missing input instead of improvising. + +## Mandatory output location + +Write the artifact to: + +```text +docs/architecture/YYYY-MM-DD--architecture.md +``` + +Then record that path back into the plan as `architecture_ref: ` or under a `## Related Artifacts` section. + +## Required frontmatter + +```yaml +--- +date: YYYY-MM-DD +topic: +status: complete +plan_ref: docs/plans/... +brainstorm_ref: docs/brainstorms/... # optional +handoff: + deepen_plan: true + work: true + review: true +--- +``` + +## Required sections + +```markdown +# Architecture Improvement + +## Purpose Linkage +- Problem Narrative: +- User Story: +- Success Criteria: +- Architectural Context: + +## Feature Homes and Ownership +- **Feature home:** `` + - Owns: + - Crosses into: + - Notes: + +## Shared / Global Decisions +| Candidate | Keep in feature home / Move to shared | Why | +|-----------|----------------------------------------|-----| +| | | | + +## Deepening Candidates +- : Why this area needs deeper architectural treatment before execution hardening + +## Deletion Test +| Candidate | Keep/Delete/Delay | Why | +|-----------|-------------------|-----| +| | | | + +## Interfaces as Test Surfaces +- **Interface:** + - Callers/tests rely on: + - Must not leak: + - Evidence needed later: + +## Seams, Adapters, and Contracts +- **Seam:** + - **Adapter:** + - **Contract:** + +## Design-It-Twice Options +- **Option A:** +- **Option B:** +- **Chosen for now:** + +## Context Tiers +- **Global context:** +- **On-demand context:** +- **Ticket-local context:** + +## Recommendations for `/deepen-plan` +- + +## Recommendations for `/workflows-work` +- + +## Recommendations for `/workflows-review` +- + +## Drift Checks +- + +## Open Questions +- +``` + +## Language rules + +Use these terms exactly and consistently: + +- **Deepening candidates** -- structural areas that need more treatment before execution hardening +- **Feature home** -- the primary namespace or directory where one feature's business behavior lives +- **Shared / global** -- code whose reason to change is truly cross-feature or infrastructural +- **Deletion test** -- the test that asks what can be removed, avoided, or delayed before adding abstraction +- **Interface as test surface** -- the stable behavior callers and tests should target +- **Seam** -- a boundary where implementation can vary +- **Adapter** -- the translation layer at a seam, usually for external systems or incompatible models +- **Contract** -- the explicit promise a seam or interface must honor +- **Design-it-twice** -- a lightweight comparison of two structural options when a boundary is high leverage +- **Context tiers** -- the split between global, on-demand, and ticket-local context after planning + +Avoid fuzzy substitutes like "clean it up later," "probably abstract here," or "future-proofing" unless you immediately restate them in deletion-test, interface, seam, or adapter terms. + +## Completion standard + +The artifact is complete only when: + +- Every proposed abstraction survives the deletion test or is explicitly deferred +- Feature homes and shared/global decisions are named explicitly +- Interfaces are described as test surfaces, not just nouns +- Seams and adapters are mapped to real boundaries in the plan +- Context tiers are explicit enough that ticket creation and execution can stay smaller than the full plan +- `/deepen-plan`, `/workflows-work`, and `/workflows-review` each have explicit handoff guidance +- The artifact path is recorded back into the plan diff --git a/.github/skills/workflows-triage/references/execution-agent-prompt.md b/.github/skills/workflows-triage/references/execution-agent-prompt.md new file mode 100644 index 0000000..14d9778 --- /dev/null +++ b/.github/skills/workflows-triage/references/execution-agent-prompt.md @@ -0,0 +1,86 @@ +--- +model: claude-sonnet-4.6 +platforms: + copilot: + model: gpt-5.3-codex + opencode: + model: openrouter/moonshotai/kimi-k2.6 +--- + +# Execution Agent Prompt Template + +This template is the **injected context packet** that `/workflows-work` passes into the named `execution-agent`. + +**Canonical execution rules live in `agents/workflow/execution-agent.md`.** `/workflows-work` must load that bundled agent template, then inject the fully populated context packet below when dispatching `Task(execution-agent, prompt=scoped_prompt)`. + +**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator so the exact context scaffold ships with generated workflow bundles. + +**Scaffold authority:** This file is the only valid source for the injected execution context packet. If you receive a shortened paraphrase, a prompt missing the sections below, or a prompt that still contains unresolved `{{PLACEHOLDER}}` tokens, stop and report that the execution template is incomplete. Do not proceed on a reconstructed or partial prompt. + +--- + +The bundled `execution-agent` enforces clean-code, DRY, SOLID, feature-home boundary discipline, doc blocks above non-trivial functions/classes, imports at the top of files unless a real exception exists, explicit failure handling, and the structured execution report contract. Populate the scaffold below completely before dispatch. + +## Your Unit + +**Unit:** {{UNIT_TITLE}} + +{{UNIT_DESCRIPTION}} + +**Unit kind:** {{UNIT_KIND}} + +**Outcome scenario:** {{OUTCOME_SCENARIO}} + +**Feature home:** {{FEATURE_HOME}} + +**Scope:** {{UNIT_SCOPE}} + +**Scope fence:** {{UNIT_SCOPE_FENCE}} + +**Files to create/modify:** {{FILE_LIST}} + +**Success criteria:** +{{SUCCESS_CRITERIA}} + +**Validation command:** `{{VALIDATION_COMMAND}}` + +**Dependencies completed:** {{COMPLETED_DEPENDENCIES}} + +**Parent refs:** {{PARENT_REFS}} + +## Ticket-local context + +{{TICKET_LOCAL_CONTEXT}} + +## Why This Unit Exists + +{{WHY_CONTEXT}} + +## Architectural Context + +{{ARCHITECTURAL_CONTEXT}} + +## Architecture Handoff + +{{ARCHITECTURE_HANDOFF}} + +## Learnings from Previous Units + +{{LEARNINGS_BRIEF}} + +## Project Conventions + +{{PROJECT_CONVENTIONS}} + +## TDD Execution Contract + +Use `references/tdd-evidence-contract.md` as the shared source of truth for contract resolution, Ralph evidence semantics, and report structure. Do not invent a lighter evidence format for convenience. + +{{TDD_CONTRACT}} + +### TDD Evidence + +- Ralph is the default TDD execution path whenever the resolved contract selects Ralph-driven work. +- `Red` and `Green` prove behavior coverage. +- `Post-Refactor Green` proves cleanup safety. +- If no cleanup was needed, still rerun and say so. diff --git a/.github/skills/workflows-triage/references/execution-shape.md b/.github/skills/workflows-triage/references/execution-shape.md new file mode 100644 index 0000000..5e00d66 --- /dev/null +++ b/.github/skills/workflows-triage/references/execution-shape.md @@ -0,0 +1,98 @@ +# Execution Shape Contract + +Use this reference when planning, deepening, or executing work so the workflow can preserve judgment without re-explaining the same rules in every prompt. + +For `vertical-slices`, also apply `references/vertical-slice-architecture.md` so the plan names the feature home and preserves the shared/global boundary instead of treating slices as pure task buckets. + +## Default + +Choose `vertical-slices` unless that would create fake end-to-end work just to satisfy the template. + +## Allowed modes + +### `vertical-slices` (default) + +Use when a thin, testable, end-to-end behavior exists. + +Required packet: +- `Slice type` +- `Serves` +- `Demo scenario` +- `Feature home` +- `Scope` +- `Scope fence` +- `Files` +- `Depends on` +- `Dependency type` +- `Success criteria` +- `Test command` + +### `infra-track` + +Use for enabling or foundational work where no honest user-visible tracer bullet exists yet. + +Required packet: +- `Capability enabled` +- `Consumers / downstream work unlocked` +- `Scope` +- `Files` +- `Depends on` +- `Risk / rollback` +- `Validation command` +- `Success criteria` + +### `fix-batch` + +Use for a series of small mostly independent fixes where forcing one vertical slice would blur the real work. + +Required packet: +- `Problem` +- `Repro / expected outcome` +- `Files` +- `Depends on` +- `Validation command` +- `Success criteria` + +## Selection rules + +1. Default to `vertical-slices`. +2. Switch to `infra-track` only when the honest near-term value is enabling capability, not a user-visible behavior. +3. Switch to `fix-batch` only when the work is truly a batch of small fixes, not a feature being split too late. +4. Never force `vertical-slices` if that would create fake verticality. +5. If the mode is not the default, record why in `execution_shape.rationale` and in `## Execution Shape`. + +## Plan shape + +Add this frontmatter block to every plan: + +```yaml +execution_shape: + mode: vertical-slices # vertical-slices | infra-track | fix-batch + rationale: "" # required when mode is not vertical-slices +``` + +Add this body section: + +```markdown +## Execution Shape +- **Mode:** [vertical-slices | infra-track | fix-batch] +- **Why:** [1-2 sentences] +``` + +Then use the packet section that matches the chosen mode: +- `## Execution Slices` +- `## Infrastructure Work Packets` +- `## Fix Batch Items` + +When the mode is `vertical-slices`, each packet must also name the feature home defined by `references/vertical-slice-architecture.md`. + +## Deepening rules + +- Validate the plan against the chosen mode, not against `vertical-slices` unconditionally. +- You may recommend switching modes if the selected one is clearly wrong, but record that as a `WHY Reassessment` note instead of silently rewriting intent. + +## Execution rules + +- `/workflows-work` must execute the units defined by the chosen mode. +- Do not coerce `infra-track` or `fix-batch` plans into slices unless the user explicitly approves a mode change. +- Session tracking may stay generic (`unit`, `work status`) even when the selected mode is `vertical-slices`. diff --git a/.github/skills/workflows-triage/references/orchestration-protocol.md b/.github/skills/workflows-triage/references/orchestration-protocol.md new file mode 100644 index 0000000..a5a80f7 --- /dev/null +++ b/.github/skills/workflows-triage/references/orchestration-protocol.md @@ -0,0 +1,39 @@ +--- +{} +--- + +# Shared Orchestration Protocol + +This reference is consumed by the core workflow prompts so template loading, named-agent dispatch, and downstream handoff rules stay aligned instead of drifting across commands. + +## Reference Template Loading + +Use this before loading any workflow prompt template from `commands/workflows/references/*.md`. + +1. Use the platform's file-search tool against the workflow reference directory to look for `.md`. Search the directory, not a full path embedded in the pattern argument. +2. Use the file-read tool to load the full template. +3. Before continuing, quote the first non-empty line of the loaded template and record which file you used. +4. If you cannot quote the template because it was not found or could not be read, stop execution, raise the missing-template issue, and do not continue. +5. Fill placeholders from the loaded template. Do not reconstruct the prompt from memory. + +## Named Agent Dispatch + +Use this before every `Task(...)` call that names an agent. + +1. Use the platform's file-search tool against the bundled agent directory to look for `.md`. Search the directory, not a full path embedded in the pattern argument. +2. If the bundled template exists, use the file-read tool to load the full template. +3. Only if no bundled template can be loaded, fall back to OpenViking/global context with `ov_load_global_agent ""`. +4. Before dispatching, quote the first non-empty line of the loaded template and record which source you used. +5. Include the loaded template's rules in the delegated prompt. Do not summarize or abbreviate the template. +6. If you cannot quote the template because it was not found or could not be read, stop execution, raise the missing-template issue, and do not dispatch. + +Never dispatch a named agent by name alone. + +## Delegated Handoff Requirements + +When a workflow has already resolved context, pass the resolved blocks along with the loaded template instead of rebuilding them from memory. + +- **WHY context** — problem narrative, user story, success criteria, and task/phase purpose. +- **Architecture handoff** — artifact path or explicit fallback contract, plus deletion-test, interface, seam, adapter, and contract guidance when relevant. +- **TDD/evidence expectations** — the resolved execution mode, required evidence, and any approved exceptions. +- **Workflow-specific payload** — diff content, task requirements, file lists, research scope, review mode, or other concrete inputs the receiving agent needs. diff --git a/.github/skills/workflows-triage/references/quality-review-prompt.md b/.github/skills/workflows-triage/references/quality-review-prompt.md new file mode 100644 index 0000000..3cae4e5 --- /dev/null +++ b/.github/skills/workflows-triage/references/quality-review-prompt.md @@ -0,0 +1,98 @@ +--- +{} +--- + +# Code Quality Review Prompt Template + +This template is used by the `workflows:work` orchestrator to dispatch a code quality reviewer subagent when `--review-mode inline` or `--review-mode both` is active. This reviewer is dispatched ONLY AFTER spec compliance has passed. + +**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator. + +--- + +You are a code quality reviewer. The implementation has already passed spec compliance review (it builds what was requested). Your job is to verify the implementation is well-built. + +## What Was Implemented + +{{IMPLEMENTER_REPORT}} + +## Files Changed + +{{FILES_CHANGED}} + +## TDD Evidence Gate + +Read `### TDD Evidence` in the implementer report first. + +Apply `references/tdd-evidence-contract.md` as the source of truth for Ralph evidence semantics and review-gate classifications. + +`Red` and `Green` prove behavior coverage. Do not reopen behavior-coverage gaps here unless the evidence is missing or obviously weak; send those back as a spec/TDD gate failure. + +Your TDD job in quality review is cleanup safety: +- **Missing cleanup after refactor** = no trustworthy `Post-Refactor Green` rerun after cleanup/refactor, or the rerun does not prove behavior stayed green. +- If no cleanup/refactor was needed, the report should still include a post-refactor rerun that says so. +- Weak post-refactor evidence is still a quality failure even when the feature appears to work. + +## Review Criteria + +### Code Quality +- Is the code clean, readable, and maintainable? +- Do names accurately describe what things do? +- Is error handling appropriate (not over-handled, not missing)? +- Are there any code smells (long functions, deep nesting, god objects)? +- Does it follow the existing codebase patterns and conventions? + +### Testing +- Do tests verify actual behavior (not just mock behavior)? +- Is post-refactor evidence strong enough to trust cleanup work? +- Are edge cases covered? +- Are tests maintainable (not brittle, not testing implementation details)? + +### Architecture +- Does the implementation fit cleanly into the existing codebase? +- Is there unnecessary coupling or inappropriate dependencies? +- Are abstractions appropriate (not too many, not too few)? +- Would this be easy for another developer to understand and modify? + +### Security & Performance (Quick Check) +- Any obvious security issues (injection, XSS, auth bypass, exposed secrets)? +- Any obvious performance issues (N+1 queries, unbounded loops, missing indexes)? +- Any resource leaks (unclosed connections, missing cleanup)? + +## Report Format + +Respond with exactly one of: + +**If approved:** +``` +## Quality Review: PASS + +Implementation is well-built. +Strengths: [brief summary of what was done well] +``` + +**If issues found:** +``` +## Quality Review: FAIL + +### Issues Found +1. **[P1 (Blocker)/p2 (Important)/p3 (Nice-to-have)] [Missing Cleanup After Refactor/Implementation Quality]:** [Description] + - File: `path/to/file:line` + - Evidence: [TDD block or code signal] + - Problem: [what is wrong] + - Suggestion: [how to fix] + +2. ... + +### Summary +- P1 (Blocker): [count] (must fix before proceeding) +- P2 (Important): [count] (should fix) +- P3 (Nice-to-have): [count] (consider fixing) +``` + +**Severity definitions:** +- **P1 (Blocker):** Bug, security vulnerability, data loss risk, broken functionality, or cleanup/refactor without trustworthy post-refactor rerun evidence. Must fix before proceeding. +- **P2 (Important):** Code smell, missing error handling, poor testability, missing cleanup follow-through, or maintainability concern. Should fix. +- **P3 (Nice-to-have):** Style issue, naming improvement, or minor optimization. Consider fixing. + +Keep the report terse. Lead with the evidence, then the smallest fix that resolves it. diff --git a/.github/skills/workflows-triage/references/spec-review-prompt.md b/.github/skills/workflows-triage/references/spec-review-prompt.md new file mode 100644 index 0000000..83c079d --- /dev/null +++ b/.github/skills/workflows-triage/references/spec-review-prompt.md @@ -0,0 +1,104 @@ +--- +{} +--- + +# Spec Compliance Review Prompt Template + +This template is used by the `workflows:work` orchestrator to dispatch a spec compliance reviewer subagent when `--review-mode inline` or `--review-mode both` is active. The orchestrator fills in context blocks before passing the result to a subagent. + +**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator. + +--- + +You are a spec compliance reviewer. Your job is to verify whether an implementation matches its specification -- nothing more, nothing less. + +## What Was Requested + +{{UNIT_REQUIREMENTS}} + +## Success Criteria + +{{SUCCESS_CRITERIA}} + +## Unit Purpose + +{{UNIT_PURPOSE}} + +## What Implementer Claims They Built + +{{IMPLEMENTER_REPORT}} + +## CRITICAL: Do Not Trust the Report + +The implementer's report may be incomplete, inaccurate, or optimistic. You MUST verify everything independently by reading the actual code. + +**DO NOT:** +- Take their word for what they implemented +- Trust their claims about completeness +- Accept their interpretation of requirements without verification + +**DO:** +- Read the actual code they wrote +- Compare the implementation to requirements line by line +- Check for missing pieces they claimed to implement +- Look for extra features they did not mention + +## TDD Evidence Gate + +Read `### TDD Evidence` in the implementer report before reviewing code. + +Apply `references/tdd-evidence-contract.md` as the source of truth for Ralph evidence semantics and review-gate classifications. + +Use this gate for **behavior coverage** only: +- **Missing behavior coverage** = no trustworthy red/green evidence that the requested behavior was specified first and then made to pass. +- Cleanup/refactor-quality issues belong in quality review unless they break the requested behavior or invalidate the evidence. + +## Your Review + +Read the implementation code and evaluate: + +### Missing Requirements +- Did they implement everything that was requested in the success criteria? +- Is the requested behavior covered by trustworthy red/green evidence? +- Are there requirements they skipped or missed? +- Did they claim something works but did not actually implement it? +- Are there edge cases implied by the criteria that are not handled? + +### Extra/Unneeded Work +- Did they build things that were not requested? +- Did they over-engineer or add unnecessary features? +- Did they add "nice to haves" that were not in the spec? +- Are there abstractions or layers that the spec did not call for? + +### Misunderstandings +- Did they interpret requirements differently than intended? +- Did they solve the wrong problem? +- Did they implement the right feature but in the wrong way? + +## Report Format + +Respond with exactly one of: + +**If compliant:** +``` +## Spec Review: PASS + +All requirements met. Implementation matches specification. +[Optional: brief note on anything worth highlighting] +``` + +**If issues found:** +``` +## Spec Review: FAIL + +### Issues Found +1. **[Missing/Extra/Misunderstood/Missing Behavior Coverage]:** [Description] + - File: `path/to/file:line` + - Expected: [what spec requires] + - Actual: [what was implemented or missing] + - Evidence: [code or TDD signal that proves it] + +2. ... +``` + +Keep the report terse. Cite the missing or contradictory requirement and the evidence that proves it. diff --git a/.github/skills/workflows-triage/references/tdd-evidence-contract.md b/.github/skills/workflows-triage/references/tdd-evidence-contract.md new file mode 100644 index 0000000..d6d543b --- /dev/null +++ b/.github/skills/workflows-triage/references/tdd-evidence-contract.md @@ -0,0 +1,44 @@ +--- +{} +--- + +# Shared TDD and Evidence Contract + +This reference is the single source of truth for TDD contract resolution, plan output shape, Ralph evidence semantics, and review-gate classifications across the workflow chain. + +## Contract Resolution + +- Plan-level `tdd` values override `compound-engineering.local.md`. +- Any field set to `inherit` falls back to the local config. +- If no local config exists, default to Ralph-driven `red-green-refactor` with unit + e2e evidence required. +- Any relaxation from Ralph or the unit + e2e default must be explicit, justified in `tdd.exceptions`, and paired with `replacement_evidence`. +- Do not silently weaken the TDD contract. + +## Plan Section Shape + +When a plan emits `## TDD & Evidence Contract`, keep this exact bullet shape and fill it with the resolved values: + +- **Precedence:** [how plan values override/fall back] +- **Effective mode:** [Ralph-driven TDD | Standard implementation] +- **Effective loop:** [Failing tests first -> minimal implementation -> refactor -> post-refactor rerun | Implementation-first] +- **Required evidence:** [Unit command/result], [E2E command/result] +- **Exceptions:** [None, or explicit justified deviation plus `replacement_evidence`] + +## Ralph Evidence Block + +When Ralph-driven execution is active, keep a stable `### TDD Evidence` / `## TDD Evidence` block with `Red`, `Green`, and `Post-Refactor Green` entries. + +- `Red` and `Green` prove behavior coverage. +- `Red` must fail for the missing behavior, not setup, import, syntax, or environment noise. +- `Green` must show the requested behavior now passes. +- `Post-Refactor Green` proves cleanup safety; even if no cleanup was needed, rerun and say so. +- Replacement evidence is valid only when an approved exception explicitly allows it. + +## Review Gate Classifications + +When auditing evidence, classify findings exactly this way: + +- **Missing behavior coverage** — weak or missing `Red`/`Green`, or evidence that does not prove the requested behavior. +- **Missing cleanup after refactor** — weak or missing `Post-Refactor Green`, or no rerun evidence after cleanup/refactor was claimed. + +Keep the gate output terse and evidence-based: cite the task/session file, the weak or missing block, and the concrete reason. diff --git a/.github/skills/workflows-triage/references/ticket-execution-contract.md b/.github/skills/workflows-triage/references/ticket-execution-contract.md new file mode 100644 index 0000000..39df591 --- /dev/null +++ b/.github/skills/workflows-triage/references/ticket-execution-contract.md @@ -0,0 +1,149 @@ +# Ticket Execution Contract + +Use this reference when writing or consuming local ticket artifacts. + +This contract is shared by `/workflows-to-issues`, the `focused-ticket-priming` skill, `/workflows-work`, and `/workflows-review` so ticket generation and ticket execution stay aligned. + +## Ticket set layout + +Write ticket sets to: + +```text +docs/tickets/YYYY-MM-DD-/ +``` + +Required files: + +- `index.md` +- `NN-.md` for each execution-ready ticket + +## Ticket set index + +`index.md` must include: + +- frontmatter with: + - `plan_ref` + - `architecture_ref` or an explicit handoff note + - `execution_shape` + - `ticket_set_status` -- `ready | in_progress | blocked | completed` + - `last_completed_batch` + - `total_batches` +- plan path +- architecture artifact path or explicit handoff note +- execution shape +- ticket order +- dependency graph +- execution batches +- file-overlap safety notes for every multi-ticket batch +- blocker summary +- review summary split into `Blocking gaps` and `Recommendations` + +Recommended body shape: + +1. `# Ticket Set: ` +2. `## Dependency Graph` +3. `## Execution Batches` +4. `## Ticket Table` +5. `## Blockers` +6. `## Review Summary` + +## Ticket file naming + +- Keep zero-padded numeric prefixes in execution order: `01-...`, `02-...`, `03-...` +- Use the slug to describe the outcome, not the layer or implementation detail + +## Required ticket frontmatter + +Every ticket file must include this frontmatter shape: + +```yaml +--- +ticket_id: T01 +title: Tracer bullet for account onboarding +kind: tracer-bullet # tracer-bullet | expansion | hardening | infra-track | fix-batch +status: ready # ready | in_progress | blocked | completed +plan_ref: docs/plans/2026-05-20-account-onboarding-plan.md +tickets_ref: docs/tickets/2026-05-20-account-onboarding/index.md +architecture_ref: docs/architecture/2026-05-20-account-onboarding.md +source_packet_ref: "## Execution Slices > Slice 1" +feature_home: src/features/account-onboarding +depends_on: [] +dependency_type: none # none | hard | soft | parallel-safe +serves: + - Success criterion 1 +files: + - src/features/account-onboarding/controller.ts +test_command: bun test tests/account-onboarding.test.ts +tdd_mode: inherit +--- +``` + +## Required ticket body + +Every ticket must include these sections in this order: + +1. `# ` +2. `## Serves` +3. `## Scope` +4. `## Scope Fence` +5. `## Acceptance Criteria` +6. `## Shared / Global Notes` +7. `## Local Context` +8. `## Parent Refs` +9. `## Deeper-Dive Refs` +10. `## Coupling Notes` + +## Local context rules + +The `## Local Context` section is the execution packet. + +It must include: + +- the smallest WHY summary needed for this ticket +- the concrete files and interfaces that matter now +- architectural boundary notes that keep the feature home honest +- explicit unknowns or open questions that an execution agent must surface instead of guessing + +It must NOT: + +- copy the whole plan +- restate the whole architecture artifact +- duplicate repo-global guardrails that already belong in global context + +## Status lifecycle + +Use these ticket statuses: + +- `ready` -- ticket is execution-ready +- `in_progress` -- active execution session owns it +- `blocked` -- a dependency or blocker prevents safe execution +- `completed` -- execution finished and required evidence exists + +## Execution consumption rules + +When `/workflows-work` receives `index.md`: + +- treat the index as the authoritative ticket queue for this ticket set +- read `last_completed_batch` to choose the next unfinished batch +- load only the tickets named in that next batch +- execute multiple tickets together only when the batch is explicitly declared as parallel-safe and the index records why the file sets do not conflict +- if the index or ticket files leave overlap safety ambiguous, collapse that batch to sequential execution instead of guessing +- update batch progress in `index.md`, and increment `last_completed_batch` only after every ticket in the batch reaches `completed` + +When `/workflows-work` receives a ticket file: + +- treat that ticket as one pre-scoped execution unit +- load `plan_ref` and `architecture_ref` for WHY and architecture context +- honor the ticket's `feature_home`, `files`, and `## Scope Fence` +- stop and send the user back to `/workflows-to-issues` if required frontmatter or body sections are missing + +## Review consumption rules + +When `/workflows-review` or `ticket-flow-auditor` consumes ticket artifacts, verify: + +- the ticket still matches the parent plan and architecture +- the implementation stayed inside the ticket scope fence unless the change was explicitly documented +- dependency order and status changes stayed honest +- the dependency graph and batch partition still match the ticket files +- parallel-safe batches really stay file-disjoint and race-safe +- evidence matches the ticket's stated acceptance criteria and test command diff --git a/.github/skills/workflows-triage/references/ticketization-contract.md b/.github/skills/workflows-triage/references/ticketization-contract.md new file mode 100644 index 0000000..6545e3f --- /dev/null +++ b/.github/skills/workflows-triage/references/ticketization-contract.md @@ -0,0 +1,166 @@ +# Ticketization Workflow Contract + +Use this reference when turning a plan into smaller local ticket artifacts. + +This contract exists to keep `/workflows-plan` and `/deepen-plan` strategic while moving execution-sized context into independently usable tickets. + +## Mandatory ticket-packaging assets + +Ticketization must use these shared assets instead of inventing ad hoc ticket shapes: + +- `focused-ticket-priming` -- the packaging skill that turns one execution packet into one compact ticket packet +- `ticket-execution-contract.md` -- the exact frontmatter/body schema that `/workflows-work` and `/workflows-review` will consume later + +## Canonical command + +- **Command name:** `/workflows-to-issues` +- **Placement:** after `/workflows-plan` or `/deepen-plan` +- **Recommended timing:** after `/deepen-plan` when the plan still needs architectural or research hardening + +## Inputs + +A ticketization run requires: + +- `plan_ref` -- the source plan +- `execution_shape` -- must already be explicit in the plan +- plan WHY artifacts: Problem Narrative, User Story, Success Criteria, Architectural Context +- the architecture artifact at `architecture_ref`, or an explicit architecture handoff contract + +If the plan is missing the WHY artifacts, execution shape, or architecture guidance, stop and ask the user to repair that first instead of guessing. + +## Output location + +Write the local ticket set to: + +```text +docs/tickets/YYYY-MM-DD-/ +``` + +Required outputs: + +- `index.md` -- ticket-set summary, dependency graph, execution batches, run guidance, and progress pointer +- `NN-.md` -- one file per ticket in execution order + +Record the ticket set back into the plan as: + +- `tickets_ref: docs/tickets/YYYY-MM-DD-/index.md` + +If frontmatter cannot be updated safely, add the path under `## Related Artifacts`. + +## v1 rollout + +The first version is **local-artifact first**. + +- Generate local ticket files only. +- Do not publish tracker issues automatically. +- Keep issue-tracker publishing as a later optional extension. + +## Ticket sizing rules + +- Start from the plan's declared execution packets instead of inventing a new backlog from scratch. +- Size tickets by **coupling and boundary clarity**, not raw line count. +- A ticket may cross backend, frontend, and tests when that is the thinnest honest slice. +- Split a packet when one ticket would deliver more than one meaningful outcome, blur ownership, or bury the feature home. +- Keep the first ticket a tracer bullet when the selected execution shape is `vertical-slices`. + +## Dependency graph and execution batches + +Ticketization must build a conservative ticket dependency graph and then derive execution batches from it. + +- Start from each ticket's explicit `depends_on` edges. +- Then partition tickets into `Batch 1`, `Batch 2`, and so on, where every ticket in a batch depends only on earlier batches. +- A batch may contain multiple tickets only when their declared `files` sets do not overlap and there is no unresolved shared mutable state, migration risk, or boundary ambiguity between them. +- If two tickets might race, overwrite each other, or require the same shared adapter/config surface, keep them in separate sequential batches. +- **Default-to-sequential rule:** if batch safety is uncertain, emit singleton batches instead of guessing at parallelism. + +The index file becomes the authoritative execution queue for ticketized work. `/workflows-work` should be able to read `index.md`, find the next unfinished batch, and execute only that batch without recomputing the whole backlog. + +## Required index progress state + +`index.md` must record batch progress explicitly so ticketized execution can resume from the index alone. + +Required fields: + +- `last_completed_batch` -- integer counter; `0` means no batches completed yet +- `total_batches` -- total number of execution batches in the ticket set +- batch-level status view showing which batches are pending, in progress, blocked, or completed + +`last_completed_batch` advances only after every ticket in that batch is complete. If any ticket in the batch is blocked or still running, do not advance the counter. + +## Required ticket-local context + +Every generated ticket must carry a compact execution packet that can stand on its own. + +Required fields: + +- `Ticket` +- `Kind` -- tracer-bullet | expansion | hardening | infra-track | fix-batch +- `Serves` +- `Feature home` +- `Scope` +- `Scope fence` +- `Files` +- `Depends on` +- `Dependency type` +- `Acceptance criteria` +- `Evidence / test command` +- `Shared / global notes` +- `Parent refs` -- plan path, architecture path, and source slice/packet reference +- `Deeper-dive refs` -- optional docs to read only when the ticket-local packet is insufficient +- `Coupling notes` -- why this ticket is one unit instead of multiple tickets + +Use `ticket-execution-contract.md` as the exact source of truth for the concrete frontmatter and section order. + +## Context packaging rules + +Split context across the existing three tiers: + +- **Global context** -- constitution rules, local defaults, repo-wide guardrails +- **On-demand context** -- architecture artifact, vertical-slice architecture contract, supporting docs +- **Ticket-local context** -- only the minimum packet needed to execute one ticket safely + +Ticketization should shrink execution context, not recreate the full plan inside every ticket. + +## Behavior after `plan` vs after `deepen-plan` + +- After **`/workflows-plan`**: allow earlier backlog shaping, but preserve visible uncertainty instead of pretending the research is settled +- After **`/deepen-plan`**: prefer this mode when the plan already has hardened boundaries, tests, and architecture guidance + +In both cases, never strip WHY or architecture references out of the tickets. + +## Final ticket-set review + +Ticketization ends with a final review sweep over the full ticket set. + +That sweep must use the dedicated `ticket-flow-auditor` reviewer so the same contract can be enforced again later during `/workflows-review`. + +That review must check: + +- vertical-slice honesty +- feature-home clarity +- shared/global boundary honesty +- blocker and dependency correctness +- dependency graph correctness +- execution batch safety and parallelization honesty +- file-overlap conflicts between tickets claimed to be parallel-safe +- context completeness +- ticket size and coupling quality + +The review output must separate: + +- **Blocking gaps** -- ticket set is not safe to execute yet +- **Recommendations** -- the set is usable, but could be improved + +## Completion standard + +The ticketization contract is satisfied only when: + +- the command name and placement are explicit +- the output path and artifact shape are explicit +- the priming skill and ticket schema contract are explicit +- local-artifact-first behavior is explicit +- `tickets_ref` recording is explicit +- the dependency graph and conservative batch partition are explicit +- the index progress pointer is explicit +- each ticket's required local context is explicit +- the final ticket-set review step and reviewer are explicit diff --git a/.github/skills/workflows-triage/references/vertical-slice-architecture.md b/.github/skills/workflows-triage/references/vertical-slice-architecture.md new file mode 100644 index 0000000..bb66412 --- /dev/null +++ b/.github/skills/workflows-triage/references/vertical-slice-architecture.md @@ -0,0 +1,72 @@ +# Vertical Slice Architecture Contract + +Use this reference alongside `references/execution-shape.md` when the work is organized as `vertical-slices`. + +`execution-shape.md` answers **how to decompose work**. This contract answers **where the feature lives**, **what stays local**, **what stays shared**, and **how to split context after planning** so downstream agents can work with smaller packets. + +## Core rule + +Every real feature should have a single named **feature home**: one namespace, directory, or module boundary that acts as the default home for that feature's business behavior. + +Inside that feature home, keep together the code needed to reason about the behavior end to end: +- feature-local backend logic +- feature-local frontend/UI logic +- feature-local tests +- feature-local mappers, policies, presenters, or helper code that only serves that feature + +If the work spans multiple existing feature homes, say so explicitly instead of pretending one home owns everything. + +## Shared / global rule + +Vertical slices do **not** suspend DRY or SOLID. + +Keep code in shared or global namespaces when it is genuinely cross-feature infrastructure or a stable reusable building block, such as: +- primitives and shared utilities +- reusable base classes and exceptions +- design-system components +- framework or vendor integration adapters +- cross-feature contracts and translation layers + +Do **not** move core business behavior into shared directories just because more than one layer touches it. + +## Extraction rule + +Extract behavior out of a feature home only when at least one of these is true: +1. Multiple feature homes already rely on the same stable behavior. +2. The code is an adapter or contract boundary whose reason to change is cross-feature, not feature-specific. +3. The architecture artifact's deletion test shows the local version is now the wrong boundary. + +If none of those are true, keep the code in the feature home even if that means a little local duplication for now. + +## Drift checks + +Treat these as architectural drift unless explicitly justified: +- Core feature behavior scattered across horizontal directories with no clear feature home. +- Copy-pasted shared abstractions across multiple feature homes. +- Shared/global directories that mainly contain one feature's business rules. +- Tickets or execution slices that cannot name the feature home they are changing. + +## Context tiers + +After planning, split context into these tiers: + +| Tier | What belongs here | Why | +|---|---|---| +| Global context | constitution rules, local config defaults, repo-wide architectural guardrails, stable shared/global boundaries | every downstream phase needs these rules | +| On-demand context | architecture artifact, this vertical-slice contract, deep design references, related docs | useful for deeper dives without bloating every ticket | +| Ticket-local context | feature home, exact files, scope fence, success criteria, evidence command, immediate blockers, concise WHY linkage | the smallest packet an execution agent should need by default | + +## Workflow obligations + +- **`/workflows-brainstorm`** should identify the likely feature home, neighboring boundaries, and any obvious shared/global constraints. +- **`/workflows-plan`** should keep execution packets honest and, for `vertical-slices`, require every slice to name its feature home. +- **`/workflows-architecture`** should confirm feature homes, shared/global decisions, context tiers, and drift checks in a durable artifact. +- **`/deepen-plan`**, **`/workflows-work`**, and **`/workflows-review`** must preserve the same feature-home and shared/global boundary decisions instead of re-inventing them later. + +## Packet add-on for `vertical-slices` + +When the selected execution shape is `vertical-slices`, every execution packet must name: +- `Feature home` +- any shared/global dependency it relies on when that dependency matters to scope or sequencing + +The packet stays execution-sized. Deeper architectural reasoning belongs in the architecture artifact and other on-demand context, not inside every slice body. diff --git a/plugins/compound-engineering/.claude-plugin/plugin.json b/plugins/compound-engineering/.claude-plugin/plugin.json index fc470aa..376edd8 100644 --- a/plugins/compound-engineering/.claude-plugin/plugin.json +++ b/plugins/compound-engineering/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "compound-engineering", - "version": "4.13.0", + "version": "4.14.0", "description": "OpenCode-first AI-powered development tools. Includes 34 specialized agents, 28 commands, and 26 skills spanning code review, research, design, and workflow automation, with generated Copilot support and Claude Code compatibility outputs.", "author": { "name": "The Rabak", diff --git a/plugins/compound-engineering/CHANGELOG.md b/plugins/compound-engineering/CHANGELOG.md index 73b5c4e..39ad547 100644 --- a/plugins/compound-engineering/CHANGELOG.md +++ b/plugins/compound-engineering/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to the compound-engineering plugin will be documented in thi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.14.0] - 2026-06-01 + +### Changed + +- **`/workflows:triage` command** -- Renamed the old `/triage` workflow to a namespaced command, added a mandatory research pass that produces evidence-backed action options for each todo, and changed execution to safe swarm batches built from dedicated per-todo execution packets. +- **Generated export cleanup** -- Repo builds now clear stale generated Claude command output before regeneration, and OV/OpenCode sync/install now prune the legacy `triage` surface when `/workflows:triage` is present, so renamed workflows stop leaking old installable artifacts across exported platforms. + ## [4.13.0] - 2026-05-24 ### Added diff --git a/plugins/compound-engineering/README.md b/plugins/compound-engineering/README.md index 111597d..1137e35 100644 --- a/plugins/compound-engineering/README.md +++ b/plugins/compound-engineering/README.md @@ -140,6 +140,7 @@ Core workflow commands use `workflows:` prefix to avoid collisions with built-in | `/workflows:architecture` | Produce a dedicated architecture improvement artifact before deepening and execution | | `/workflows:to-issues` | Convert plans into local vertical-slice ticket artifacts with scoped execution context | | `/workflows:review` | Run comprehensive code reviews | +| `/workflows:triage` | Research todos, record chosen actions, then execute safe batches in swarm mode | | `/workflows:work` | Execute execution slices systematically | | `/workflows:compound` | Document solved problems to compound team knowledge | | `/workflows:compound-refresh` | Refresh stale learnings and pattern docs in `docs/solutions/` | @@ -160,7 +161,6 @@ Core workflow commands use `workflows:` prefix to avoid collisions with built-in | `/reproduce-bug` | Reproduce bugs using logs and console | | `/resolve_parallel` | Resolve TODO comments in parallel | | `/resolve_todo_parallel` | Resolve todos in parallel | -| `/triage` | Triage and prioritize issues | | `/test-browser` | Run browser tests on PR-affected pages | | `/feature-video` | Record video walkthroughs and add to PR description | | `/ralph-loop` | Start a self-referential loop until completion promise is met | diff --git a/plugins/compound-engineering/commands/triage.md b/plugins/compound-engineering/commands/triage.md deleted file mode 100644 index be8bf7a..0000000 --- a/plugins/compound-engineering/commands/triage.md +++ /dev/null @@ -1,311 +0,0 @@ ---- -name: triage -description: Triage and categorize findings for the CLI todo system -argument-hint: "[findings list or source type]" -disable-model-invocation: true ---- - -- First set the /model to Haiku -- Then read all pending todos in the todos/ directory - -Present all findings, decisions, or issues here one by one for triage. The goal is to go through each item and decide whether to add it to the CLI todo system. - -**IMPORTANT: DO NOT CODE ANYTHING DURING TRIAGE!** - -This command is for: - -- Triaging code review findings -- Processing security audit results -- Reviewing performance analysis -- Handling any other categorized findings that need tracking - -## Workflow - -### Step 1: Present Each Finding - -For each finding, present in this format: - -``` ---- -Issue #X: [Brief Title] - -Severity: 🔴 P1 (CRITICAL) / 🟡 P2 (IMPORTANT) / 🔵 P3 (NICE-TO-HAVE) - -Category: [Security/Performance/Architecture/Bug/Feature/etc.] - -Description: -[Detailed explanation of the issue or improvement] - -Location: [file_path:line_number] - -Problem Scenario: -[Step by step what's wrong or could happen] - -Proposed Solution: -[How to fix it] - -Estimated Effort: [Small (< 2 hours) / Medium (2-8 hours) / Large (> 8 hours)] - ---- -Do you want to add this to the todo list? -1. yes - create todo file -2. next - skip this item -3. custom - modify before creating -``` - -### Step 2: Handle User Decision - -**When user says "yes":** - -1. **Update existing todo file** (if it exists) or **Create new filename:** - - If todo already exists (from code review): - - - Rename file from `{id}-pending-{priority}-{desc}.md` → `{id}-ready-{priority}-{desc}.md` - - Update YAML frontmatter: `status: pending` → `status: ready` - - Keep issue_id, priority, and description unchanged - - If creating new todo: - - ``` - {next_id}-ready-{priority}-{brief-description}.md - ``` - - Priority mapping: - - - 🔴 P1 (CRITICAL) → `p1` - - 🟡 P2 (IMPORTANT) → `p2` - - 🔵 P3 (NICE-TO-HAVE) → `p3` - - Example: `042-ready-p1-transaction-boundaries.md` - -2. **Update YAML frontmatter:** - - ```yaml - --- - status: ready # IMPORTANT: Change from "pending" to "ready" - priority: p1 # or p2, p3 based on severity - issue_id: "042" - tags: [category, relevant-tags] - dependencies: [] - --- - ``` - -3. **Populate or update the file:** - - ```yaml - # [Issue Title] - - ## Problem Statement - [Description from finding] - - ## Findings - - [Key discoveries] - - Location: [file_path:line_number] - - [Scenario details] - - ## Proposed Solutions - - ### Option 1: [Primary solution] - - **Pros**: [Benefits] - - **Cons**: [Drawbacks if any] - - **Effort**: [Small/Medium/Large] - - **Risk**: [Low/Medium/High] - - ## Recommended Action - [Filled during triage - specific action plan] - - ## Technical Details - - **Affected Files**: [List files] - - **Related Components**: [Components affected] - - **Database Changes**: [Yes/No - describe if yes] - - ## Resources - - Original finding: [Source of this issue] - - Related issues: [If any] - - ## Acceptance Criteria - - [ ] [Specific success criteria] - - [ ] Tests pass - - [ ] Code reviewed - - ## Work Log - - ### {date} - Approved for Work - **By:** Claude Triage System - **Actions:** - - Issue approved during triage session - - Status changed from pending → ready - - Ready to be picked up and worked on - - **Learnings:** - - [Context and insights] - - ## Notes - Source: Triage session on {date} - ``` - -4. **Confirm approval:** "✅ Approved: `{new_filename}` (Issue #{issue_id}) - Status: **ready** → Ready to work on" - -**When user says "next":** - -- **Delete the todo file** - Remove it from todos/ directory since it's not relevant -- Skip to the next item -- Track skipped items for summary - -**When user says "custom":** - -- Ask what to modify (priority, description, details) -- Update the information -- Present revised version -- Ask again: yes/next/custom - -### Step 3: Continue Until All Processed - -- Process all items one by one -- Track using TodoWrite for visibility -- Don't wait for approval between items - keep moving - -### Step 4: Final Summary - -After all items processed: - -````markdown -## Triage Complete - -**Total Items:** [X] **Todos Approved (ready):** [Y] **Skipped:** [Z] - -### Approved Todos (Ready for Work): - -- `042-ready-p1-transaction-boundaries.md` - Transaction boundary issue -- `043-ready-p2-cache-optimization.md` - Cache performance improvement ... - -### Skipped Items (Deleted): - -- Item #5: [reason] - Removed from todos/ -- Item #12: [reason] - Removed from todos/ - -### Summary of Changes Made: - -During triage, the following status updates occurred: - -- **Pending → Ready:** Filenames and frontmatter updated to reflect approved status -- **Deleted:** Todo files for skipped findings removed from todos/ directory -- Each approved file now has `status: ready` in YAML frontmatter - -### Next Steps: - -1. View approved todos ready for work: - ```bash - ls todos/*-ready-*.md - ``` -```` - -2. Start work on approved items: - - ```bash - /resolve_todo_parallel # Work on multiple approved items efficiently - ``` - -3. Or pick individual items to work on - -4. As you work, update todo status: - - Ready → In Progress (in your local context as you work) - - In Progress → Complete (rename file: ready → complete, update frontmatter) - -``` - -## Example Response Format - -``` - ---- - -Issue #5: Missing Transaction Boundaries for Multi-Step Operations - -Severity: 🔴 P1 (CRITICAL) - -Category: Data Integrity / Security - -Description: The google_oauth2_connected callback in GoogleOauthCallbacks concern performs multiple database operations without transaction protection. If any step fails midway, the database is left in an inconsistent state. - -Location: app/Services/OAuthService.php:13-50 - -Problem Scenario: - -1. User.update succeeds (email changed) -2. Account.save! fails (validation error) -3. Result: User has changed email but no associated Account -4. Next login attempt fails completely - -Operations Without Transaction: - -- User confirmation (line 13) -- Waitlist removal (line 14) -- User profile update (line 21-23) -- Account creation (line 28-37) -- Avatar attachment (line 39-45) -- Journey creation (line 47) - -Proposed Solution: Wrap all operations in ApplicationRecord.transaction do ... end block - -Estimated Effort: Small (30 minutes) - ---- - -Do you want to add this to the todo list? - -1. yes - create todo file -2. next - skip this item -3. custom - modify before creating - -``` - -## Important Implementation Details - -### Status Transitions During Triage - -**When "yes" is selected:** -1. Rename file: `{id}-pending-{priority}-{desc}.md` → `{id}-ready-{priority}-{desc}.md` -2. Update YAML frontmatter: `status: pending` → `status: ready` -3. Update Work Log with triage approval entry -4. Confirm: "✅ Approved: `{filename}` (Issue #{issue_id}) - Status: **ready**" - -**When "next" is selected:** -1. Delete the todo file from todos/ directory -2. Skip to next item -3. No file remains in the system - -### Progress Tracking - -Every time you present a todo as a header, include: -- **Progress:** X/Y completed (e.g., "3/10 completed") -- **Estimated time remaining:** Based on how quickly you're progressing -- **Pacing:** Monitor time per finding and adjust estimate accordingly - -Example: -``` - -Progress: 3/10 completed | Estimated time: ~2 minutes remaining - -``` - -### Do Not Code During Triage - -- ✅ Present findings -- ✅ Make yes/next/custom decisions -- ✅ Update todo files (rename, frontmatter, work log) -- ❌ Do NOT implement fixes or write code -- ❌ Do NOT add detailed implementation details -- ❌ That's for /resolve_todo_parallel phase -``` - -When done give these options - -```markdown -What would you like to do next? - -1. run /resolve_todo_parallel to resolve the todos -2. commit the todos -3. nothing, go chill -``` diff --git a/plugins/compound-engineering/commands/workflows/references/architecture-improvement-prompt.md b/plugins/compound-engineering/commands/workflows/references/architecture-improvement-prompt.md new file mode 100644 index 0000000..7944a15 --- /dev/null +++ b/plugins/compound-engineering/commands/workflows/references/architecture-improvement-prompt.md @@ -0,0 +1,146 @@ +--- +{} +--- + +# Architecture Improvement Artifact Contract + +This template is consumed by the `workflows:architecture` phase. It is a **reference document**, not an invocable command. + +The purpose of the artifact is to make architectural choices explicit before `/deepen-plan` and `/workflows:work` continue. Use the vocabulary below consistently so downstream agents inherit a shared contract instead of guessing from oral tradition. + +## Mandatory inputs + +A valid architecture improvement pass requires: + +- `plan_ref` -- the plan being hardened +- Problem Narrative +- User Story +- Success Criteria +- Architectural Context +- Current plan phases/tasks +- The vertical-slice module contract from `vertical-slice-architecture.md` +- `brainstorm_ref`, constitution context, and source docs when available + +If any of the first five are missing, stop and report the missing input instead of improvising. + +## Mandatory output location + +Write the artifact to: + +```text +docs/architecture/YYYY-MM-DD--architecture.md +``` + +Then record that path back into the plan as `architecture_ref: ` or under a `## Related Artifacts` section. + +## Required frontmatter + +```yaml +--- +date: YYYY-MM-DD +topic: +status: complete +plan_ref: docs/plans/... +brainstorm_ref: docs/brainstorms/... # optional +handoff: + deepen_plan: true + work: true + review: true +--- +``` + +## Required sections + +```markdown +# Architecture Improvement + +## Purpose Linkage +- Problem Narrative: +- User Story: +- Success Criteria: +- Architectural Context: + +## Feature Homes and Ownership +- **Feature home:** `` + - Owns: + - Crosses into: + - Notes: + +## Shared / Global Decisions +| Candidate | Keep in feature home / Move to shared | Why | +|-----------|----------------------------------------|-----| +| | | | + +## Deepening Candidates +- : Why this area needs deeper architectural treatment before execution hardening + +## Deletion Test +| Candidate | Keep/Delete/Delay | Why | +|-----------|-------------------|-----| +| | | | + +## Interfaces as Test Surfaces +- **Interface:** + - Callers/tests rely on: + - Must not leak: + - Evidence needed later: + +## Seams, Adapters, and Contracts +- **Seam:** + - **Adapter:** + - **Contract:** + +## Design-It-Twice Options +- **Option A:** +- **Option B:** +- **Chosen for now:** + +## Context Tiers +- **Global context:** +- **On-demand context:** +- **Ticket-local context:** + +## Recommendations for `/deepen-plan` +- + +## Recommendations for `/workflows:work` +- + +## Recommendations for `/workflows:review` +- + +## Drift Checks +- + +## Open Questions +- +``` + +## Language rules + +Use these terms exactly and consistently: + +- **Deepening candidates** -- structural areas that need more treatment before execution hardening +- **Feature home** -- the primary namespace or directory where one feature's business behavior lives +- **Shared / global** -- code whose reason to change is truly cross-feature or infrastructural +- **Deletion test** -- the test that asks what can be removed, avoided, or delayed before adding abstraction +- **Interface as test surface** -- the stable behavior callers and tests should target +- **Seam** -- a boundary where implementation can vary +- **Adapter** -- the translation layer at a seam, usually for external systems or incompatible models +- **Contract** -- the explicit promise a seam or interface must honor +- **Design-it-twice** -- a lightweight comparison of two structural options when a boundary is high leverage +- **Context tiers** -- the split between global, on-demand, and ticket-local context after planning + +Avoid fuzzy substitutes like "clean it up later," "probably abstract here," or "future-proofing" unless you immediately restate them in deletion-test, interface, seam, or adapter terms. + +## Completion standard + +The artifact is complete only when: + +- Every proposed abstraction survives the deletion test or is explicitly deferred +- Feature homes and shared/global decisions are named explicitly +- Interfaces are described as test surfaces, not just nouns +- Seams and adapters are mapped to real boundaries in the plan +- Context tiers are explicit enough that ticket creation and execution can stay smaller than the full plan +- `/deepen-plan`, `/workflows:work`, and `/workflows:review` each have explicit handoff guidance +- The artifact path is recorded back into the plan diff --git a/plugins/compound-engineering/commands/workflows/references/execution-agent-prompt.md b/plugins/compound-engineering/commands/workflows/references/execution-agent-prompt.md index bed9da0..33333cc 100644 --- a/plugins/compound-engineering/commands/workflows/references/execution-agent-prompt.md +++ b/plugins/compound-engineering/commands/workflows/references/execution-agent-prompt.md @@ -1,33 +1,58 @@ --- -{} +model: claude-sonnet-4.6 +platforms: + copilot: + model: gpt-5.3-codex + opencode: + model: openrouter/moonshotai/kimi-k2.6 --- -# Execution Agent Prompt Template - -This template is used by the `workflows:work` orchestrator to construct prompts for execution subagents. The orchestrator fills in context blocks (marked with `{{PLACEHOLDER}}`) before passing the result to `Task(general-purpose, prompt=filled_template)`. - -**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator. - ---- - -You are an execution agent implementing a specific task from a work plan. Follow the 4-phase protocol below exactly. +# Execution Agent Prompt Template -## Your Task +This template is the **injected context packet** that `/workflows:work` passes into the named `execution-agent`. -**Task:** {{TASK_NAME}} +**Canonical execution rules live in `agents/workflow/execution-agent.md`.** `/workflows:work` must load that bundled agent template, then inject the fully populated context packet below when dispatching `Task(execution-agent, prompt=scoped_prompt)`. -{{TASK_DESCRIPTION}} +**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator so the exact context scaffold ships with generated workflow bundles. + +**Scaffold authority:** This file is the only valid source for the injected execution context packet. If you receive a shortened paraphrase, a prompt missing the sections below, or a prompt that still contains unresolved `{{PLACEHOLDER}}` tokens, stop and report that the execution template is incomplete. Do not proceed on a reconstructed or partial prompt. + +--- + +The bundled `execution-agent` enforces clean-code, DRY, SOLID, feature-home boundary discipline, doc blocks above non-trivial functions/classes, imports at the top of files unless a real exception exists, explicit failure handling, and the structured execution report contract. Populate the scaffold below completely before dispatch. + +## Your Unit + +**Unit:** {{UNIT_TITLE}} + +{{UNIT_DESCRIPTION}} + +**Unit kind:** {{UNIT_KIND}} + +**Outcome scenario:** {{OUTCOME_SCENARIO}} + +**Feature home:** {{FEATURE_HOME}} + +**Scope:** {{UNIT_SCOPE}} + +**Scope fence:** {{UNIT_SCOPE_FENCE}} **Files to create/modify:** {{FILE_LIST}} **Success criteria:** {{SUCCESS_CRITERIA}} -**Test command:** `{{TEST_COMMAND}}` +**Validation command:** `{{VALIDATION_COMMAND}}` **Dependencies completed:** {{COMPLETED_DEPENDENCIES}} -## Why This Task Exists +**Parent refs:** {{PARENT_REFS}} + +## Ticket-local context + +{{TICKET_LOCAL_CONTEXT}} + +## Why This Unit Exists {{WHY_CONTEXT}} @@ -35,167 +60,27 @@ You are an execution agent implementing a specific task from a work plan. Follow {{ARCHITECTURAL_CONTEXT}} -## Learnings from Previous Tasks +## Architecture Handoff + +{{ARCHITECTURE_HANDOFF}} + +## Learnings from Previous Units {{LEARNINGS_BRIEF}} ## Project Conventions -{{PROJECT_CONVENTIONS}} - ---- - -## Phase 1: Understand Before Building - -Before writing ANY code, review the task requirements AND the "Why This Task Exists" section carefully. - -**If anything is unclear, ambiguous, or could be interpreted multiple ways:** -- List your questions explicitly -- State the assumptions you would make if proceeding without answers -- Ask for clarification before starting work - -**If everything is clear:** -- State your interpretation of the requirements in 2-3 sentences -- State how this task serves the overall user story (from the WHY context) -- List any assumptions you are making (even obvious ones) -- Proceed to Phase 2 - -Do NOT skip this phase. A few minutes of clarification prevents hours of rework. It is always better to ask than to guess. - -## Phase 2: Implement - -{{TDD_SECTION}} - -### While Implementing - -- If you encounter something unexpected or unclear, **STOP and ask** rather than guessing -- Follow existing codebase patterns -- do not invent new conventions -- Keep changes minimal -- implement what is asked, nothing more (YAGNI) -- Do not add "nice to have" features not in the success criteria -- Commit after each logical unit of complete work using the project's commit convention - -### On Test Failure - -If tests fail after implementation: -1. Read the error message carefully -- understand what failed and why -2. Analyze whether the failure is in your implementation or in the test -3. Fix the issue -4. Re-run the test command -5. Repeat up to 3 total attempts -6. If still failing after 3 attempts, report the failure with full error output -- do not keep retrying blindly - -## Phase 3: Self-Review - -Before reporting back, review your own work with fresh eyes. Go through each checklist item honestly: - -**Completeness:** -- [ ] Did I implement EVERYTHING in the success criteria? -- [ ] Are there edge cases the criteria imply that I did not handle? -- [ ] Did I miss any requirements? - -**Purpose alignment:** -- [ ] Does my implementation actually deliver what the "Why This Task Exists" section describes? -- [ ] Would a user achieve the stated outcome with this code? -- [ ] Did I build anything that doesn't trace back to the success criteria or user story? - -**Quality:** -- [ ] Do names accurately describe what things do (not how they work)? -- [ ] Is the code clean and maintainable? -- [ ] Does it follow existing codebase patterns? -- [ ] Is error handling appropriate? - -**Discipline:** -- [ ] Did I avoid overbuilding (YAGNI)? -- [ ] Did I ONLY build what was requested? -- [ ] No "nice to have" additions? -- [ ] No unnecessary abstractions or premature optimization? - -**Testing:** -- [ ] Do tests verify actual behavior (not just mock behavior)? -- [ ] Are tests comprehensive against the success criteria? -- [ ] Did I run the test command and confirm it passes? - -**Evidence:** -- [ ] Can I show actual test output (not just "tests pass")? -- [ ] For UI changes, do I have a screenshot or visual evidence? -- [ ] For API changes, do I have actual request/response data? - -If you find issues during self-review, **fix them now** before reporting. Do not report known issues -- fix them first. - -## Phase 4: Report - -Return a structured execution report in exactly this format: - -```markdown -## Execution Report: [Task Name] - -### Interpretation -[Your 2-3 sentence interpretation of what was asked] - -### Purpose Served -[Which user story aspect / success criterion this task delivers, from the WHY context] - -### Assumptions Made -- [List each assumption, even if obvious] - -### What Was Implemented -[Describe what you built and how it works] - -### Files Changed -- `path/to/file` -- created/modified (brief description of change) - -### Test Results -- Command: `[test command]` -- Result: PASS/FAIL -- Attempts: [n] -- Output: -``` -[paste ACTUAL test output here] -``` - -### Problems Encountered -[For each problem encountered during implementation:] -- **Error:** [exact error message] -- **Root cause:** [your analysis of why it happened] -- **Fix:** [what you did to resolve it] - -[If no problems: "None"] - -### Patterns Discovered -- [Naming conventions, architectural patterns, gotchas, or other learnings that would help future tasks] - -[If none: "None"] - -### Self-Review Findings -- [Issues found and fixed during self-review] - -[If none: "Self-review passed -- no issues found"] -``` - ---- - -## Standard Implementation Section - -_This section is included when TDD is not enabled._ - -1. Read referenced files and understand existing patterns -2. Implement the task following project conventions -3. Write tests matching the success criteria -4. Run the test command: `{{TEST_COMMAND}}` -5. If tests fail: analyze failure, fix, and retry (up to 3 internal attempts) - ---- - -## TDD Implementation Section - -_This section is included when `tdd_enabled: true` is configured._ - -Follow the red-green-refactor cycle strictly: - -1. Read referenced files and understand existing patterns -2. **RED:** Write tests FIRST based on the success criteria. Run them. They MUST fail -- and they must fail for the RIGHT reason (the behavior is missing, not import errors or syntax problems) -3. **GREEN:** Write the MINIMAL production code needed to make the tests pass. No more than what is necessary. -4. Run tests. They MUST pass. -5. **REFACTOR:** Clean up if needed. Tests must still pass after refactoring. - -**Iron rule:** If at any point you find yourself writing production code before a failing test exists for that behavior, STOP. Write the test first. This is not a suggestion -- it is the process. +{{PROJECT_CONVENTIONS}} + +## TDD Execution Contract + +Use `commands/workflows/references/tdd-evidence-contract.md` as the shared source of truth for contract resolution, Ralph evidence semantics, and report structure. Do not invent a lighter evidence format for convenience. + +{{TDD_CONTRACT}} + +### TDD Evidence + +- Ralph is the default TDD execution path whenever the resolved contract selects Ralph-driven work. +- `Red` and `Green` prove behavior coverage. +- `Post-Refactor Green` proves cleanup safety. +- If no cleanup was needed, still rerun and say so. diff --git a/plugins/compound-engineering/commands/workflows/references/execution-shape.md b/plugins/compound-engineering/commands/workflows/references/execution-shape.md new file mode 100644 index 0000000..e3acdf8 --- /dev/null +++ b/plugins/compound-engineering/commands/workflows/references/execution-shape.md @@ -0,0 +1,98 @@ +# Execution Shape Contract + +Use this reference when planning, deepening, or executing work so the workflow can preserve judgment without re-explaining the same rules in every prompt. + +For `vertical-slices`, also apply `commands/workflows/references/vertical-slice-architecture.md` so the plan names the feature home and preserves the shared/global boundary instead of treating slices as pure task buckets. + +## Default + +Choose `vertical-slices` unless that would create fake end-to-end work just to satisfy the template. + +## Allowed modes + +### `vertical-slices` (default) + +Use when a thin, testable, end-to-end behavior exists. + +Required packet: +- `Slice type` +- `Serves` +- `Demo scenario` +- `Feature home` +- `Scope` +- `Scope fence` +- `Files` +- `Depends on` +- `Dependency type` +- `Success criteria` +- `Test command` + +### `infra-track` + +Use for enabling or foundational work where no honest user-visible tracer bullet exists yet. + +Required packet: +- `Capability enabled` +- `Consumers / downstream work unlocked` +- `Scope` +- `Files` +- `Depends on` +- `Risk / rollback` +- `Validation command` +- `Success criteria` + +### `fix-batch` + +Use for a series of small mostly independent fixes where forcing one vertical slice would blur the real work. + +Required packet: +- `Problem` +- `Repro / expected outcome` +- `Files` +- `Depends on` +- `Validation command` +- `Success criteria` + +## Selection rules + +1. Default to `vertical-slices`. +2. Switch to `infra-track` only when the honest near-term value is enabling capability, not a user-visible behavior. +3. Switch to `fix-batch` only when the work is truly a batch of small fixes, not a feature being split too late. +4. Never force `vertical-slices` if that would create fake verticality. +5. If the mode is not the default, record why in `execution_shape.rationale` and in `## Execution Shape`. + +## Plan shape + +Add this frontmatter block to every plan: + +```yaml +execution_shape: + mode: vertical-slices # vertical-slices | infra-track | fix-batch + rationale: "" # required when mode is not vertical-slices +``` + +Add this body section: + +```markdown +## Execution Shape +- **Mode:** [vertical-slices | infra-track | fix-batch] +- **Why:** [1-2 sentences] +``` + +Then use the packet section that matches the chosen mode: +- `## Execution Slices` +- `## Infrastructure Work Packets` +- `## Fix Batch Items` + +When the mode is `vertical-slices`, each packet must also name the feature home defined by `commands/workflows/references/vertical-slice-architecture.md`. + +## Deepening rules + +- Validate the plan against the chosen mode, not against `vertical-slices` unconditionally. +- You may recommend switching modes if the selected one is clearly wrong, but record that as a `WHY Reassessment` note instead of silently rewriting intent. + +## Execution rules + +- `/workflows:work` must execute the units defined by the chosen mode. +- Do not coerce `infra-track` or `fix-batch` plans into slices unless the user explicitly approves a mode change. +- Session tracking may stay generic (`unit`, `work status`) even when the selected mode is `vertical-slices`. diff --git a/plugins/compound-engineering/commands/workflows/references/orchestration-protocol.md b/plugins/compound-engineering/commands/workflows/references/orchestration-protocol.md new file mode 100644 index 0000000..a5a80f7 --- /dev/null +++ b/plugins/compound-engineering/commands/workflows/references/orchestration-protocol.md @@ -0,0 +1,39 @@ +--- +{} +--- + +# Shared Orchestration Protocol + +This reference is consumed by the core workflow prompts so template loading, named-agent dispatch, and downstream handoff rules stay aligned instead of drifting across commands. + +## Reference Template Loading + +Use this before loading any workflow prompt template from `commands/workflows/references/*.md`. + +1. Use the platform's file-search tool against the workflow reference directory to look for `.md`. Search the directory, not a full path embedded in the pattern argument. +2. Use the file-read tool to load the full template. +3. Before continuing, quote the first non-empty line of the loaded template and record which file you used. +4. If you cannot quote the template because it was not found or could not be read, stop execution, raise the missing-template issue, and do not continue. +5. Fill placeholders from the loaded template. Do not reconstruct the prompt from memory. + +## Named Agent Dispatch + +Use this before every `Task(...)` call that names an agent. + +1. Use the platform's file-search tool against the bundled agent directory to look for `.md`. Search the directory, not a full path embedded in the pattern argument. +2. If the bundled template exists, use the file-read tool to load the full template. +3. Only if no bundled template can be loaded, fall back to OpenViking/global context with `ov_load_global_agent ""`. +4. Before dispatching, quote the first non-empty line of the loaded template and record which source you used. +5. Include the loaded template's rules in the delegated prompt. Do not summarize or abbreviate the template. +6. If you cannot quote the template because it was not found or could not be read, stop execution, raise the missing-template issue, and do not dispatch. + +Never dispatch a named agent by name alone. + +## Delegated Handoff Requirements + +When a workflow has already resolved context, pass the resolved blocks along with the loaded template instead of rebuilding them from memory. + +- **WHY context** — problem narrative, user story, success criteria, and task/phase purpose. +- **Architecture handoff** — artifact path or explicit fallback contract, plus deletion-test, interface, seam, adapter, and contract guidance when relevant. +- **TDD/evidence expectations** — the resolved execution mode, required evidence, and any approved exceptions. +- **Workflow-specific payload** — diff content, task requirements, file lists, research scope, review mode, or other concrete inputs the receiving agent needs. diff --git a/plugins/compound-engineering/commands/workflows/references/quality-review-prompt.md b/plugins/compound-engineering/commands/workflows/references/quality-review-prompt.md index 0a0d1fb..d4208f8 100644 --- a/plugins/compound-engineering/commands/workflows/references/quality-review-prompt.md +++ b/plugins/compound-engineering/commands/workflows/references/quality-review-prompt.md @@ -1,78 +1,98 @@ -# Code Quality Review Prompt Template - -This template is used by the `workflows:work` orchestrator to dispatch a code quality reviewer subagent when `--review-mode inline` or `--review-mode both` is active. This reviewer is dispatched ONLY AFTER spec compliance has passed. - -**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator. - ---- - -You are a code quality reviewer. The implementation has already passed spec compliance review (it builds what was requested). Your job is to verify the implementation is well-built. - -## What Was Implemented - -{{IMPLEMENTER_REPORT}} - -## Files Changed - -{{FILES_CHANGED}} - -## Review Criteria - -### Code Quality -- Is the code clean, readable, and maintainable? -- Do names accurately describe what things do? -- Is error handling appropriate (not over-handled, not missing)? -- Are there any code smells (long functions, deep nesting, god objects)? -- Does it follow the existing codebase patterns and conventions? - -### Testing -- Do tests verify actual behavior (not just mock behavior)? -- Is test coverage adequate for the success criteria? -- Are edge cases covered? -- Are tests maintainable (not brittle, not testing implementation details)? - -### Architecture -- Does the implementation fit cleanly into the existing codebase? -- Is there unnecessary coupling or inappropriate dependencies? -- Are abstractions appropriate (not too many, not too few)? -- Would this be easy for another developer to understand and modify? - -### Security & Performance (Quick Check) -- Any obvious security issues (injection, XSS, auth bypass, exposed secrets)? -- Any obvious performance issues (N+1 queries, unbounded loops, missing indexes)? -- Any resource leaks (unclosed connections, missing cleanup)? - -## Report Format - -Respond with exactly one of: - -**If approved:** -``` -## Quality Review: PASS - -Implementation is well-built. -Strengths: [brief summary of what was done well] -``` - -**If issues found:** -``` -## Quality Review: FAIL - -### Issues Found -1. **[P1 (Blocker)/P2 (Important)/P3 (Nice-to-have)]:** [Description] - - File: `path/to/file:line` - - Problem: [what is wrong] - - Suggestion: [how to fix] - -2. ... - -### Summary -- P1 (Blocker): [count] (must fix before proceeding) -- P2 (Important): [count] (should fix) -- P3 (Nice-to-have): [count] (consider fixing) -``` - -**Severity definitions:** -- **P1 (Blocker):** Bug, security vulnerability, data loss risk, or broken functionality. Must fix before proceeding. -- **P2 (Important):** Code smell, missing error handling, poor testability, or maintainability concern. Should fix. -- **P3 (Nice-to-have):** Style issue, naming improvement, or minor optimization. Consider fixing. +--- +{} +--- + +# Code Quality Review Prompt Template + +This template is used by the `workflows:work` orchestrator to dispatch a code quality reviewer subagent when `--review-mode inline` or `--review-mode both` is active. This reviewer is dispatched ONLY AFTER spec compliance has passed. + +**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator. + +--- + +You are a code quality reviewer. The implementation has already passed spec compliance review (it builds what was requested). Your job is to verify the implementation is well-built. + +## What Was Implemented + +{{IMPLEMENTER_REPORT}} + +## Files Changed + +{{FILES_CHANGED}} + +## TDD Evidence Gate + +Read `### TDD Evidence` in the implementer report first. + +Apply `commands/workflows/references/tdd-evidence-contract.md` as the source of truth for Ralph evidence semantics and review-gate classifications. + +`Red` and `Green` prove behavior coverage. Do not reopen behavior-coverage gaps here unless the evidence is missing or obviously weak; send those back as a spec/TDD gate failure. + +Your TDD job in quality review is cleanup safety: +- **Missing cleanup after refactor** = no trustworthy `Post-Refactor Green` rerun after cleanup/refactor, or the rerun does not prove behavior stayed green. +- If no cleanup/refactor was needed, the report should still include a post-refactor rerun that says so. +- Weak post-refactor evidence is still a quality failure even when the feature appears to work. + +## Review Criteria + +### Code Quality +- Is the code clean, readable, and maintainable? +- Do names accurately describe what things do? +- Is error handling appropriate (not over-handled, not missing)? +- Are there any code smells (long functions, deep nesting, god objects)? +- Does it follow the existing codebase patterns and conventions? + +### Testing +- Do tests verify actual behavior (not just mock behavior)? +- Is post-refactor evidence strong enough to trust cleanup work? +- Are edge cases covered? +- Are tests maintainable (not brittle, not testing implementation details)? + +### Architecture +- Does the implementation fit cleanly into the existing codebase? +- Is there unnecessary coupling or inappropriate dependencies? +- Are abstractions appropriate (not too many, not too few)? +- Would this be easy for another developer to understand and modify? + +### Security & Performance (Quick Check) +- Any obvious security issues (injection, XSS, auth bypass, exposed secrets)? +- Any obvious performance issues (N+1 queries, unbounded loops, missing indexes)? +- Any resource leaks (unclosed connections, missing cleanup)? + +## Report Format + +Respond with exactly one of: + +**If approved:** +``` +## Quality Review: PASS + +Implementation is well-built. +Strengths: [brief summary of what was done well] +``` + +**If issues found:** +``` +## Quality Review: FAIL + +### Issues Found +1. **[P1 (Blocker)/P2 (Important)/P3 (Nice-to-have)] [Missing Cleanup After Refactor/Implementation Quality]:** [Description] + - File: `path/to/file:line` + - Evidence: [TDD block or code signal] + - Problem: [what is wrong] + - Suggestion: [how to fix] + +2. ... + +### Summary +- P1 (Blocker): [count] (must fix before proceeding) +- P2 (Important): [count] (should fix) +- P3 (Nice-to-have): [count] (consider fixing) +``` + +**Severity definitions:** +- **P1 (Blocker):** Bug, security vulnerability, data loss risk, broken functionality, or cleanup/refactor without trustworthy post-refactor rerun evidence. Must fix before proceeding. +- **P2 (Important):** Code smell, missing error handling, poor testability, missing cleanup follow-through, or maintainability concern. Should fix. +- **P3 (Nice-to-have):** Style issue, naming improvement, or minor optimization. Consider fixing. + +Keep the report terse. Lead with the evidence, then the smallest fix that resolves it. diff --git a/plugins/compound-engineering/commands/workflows/references/spec-review-prompt.md b/plugins/compound-engineering/commands/workflows/references/spec-review-prompt.md index aaa4a74..34bcbc1 100644 --- a/plugins/compound-engineering/commands/workflows/references/spec-review-prompt.md +++ b/plugins/compound-engineering/commands/workflows/references/spec-review-prompt.md @@ -1,82 +1,104 @@ -# Spec Compliance Review Prompt Template - -This template is used by the `workflows:work` orchestrator to dispatch a spec compliance reviewer subagent when `--review-mode inline` or `--review-mode both` is active. The orchestrator fills in context blocks before passing the result to a subagent. - -**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator. - ---- - -You are a spec compliance reviewer. Your job is to verify whether an implementation matches its specification -- nothing more, nothing less. - -## What Was Requested - -{{TASK_REQUIREMENTS}} - -## Success Criteria - -{{SUCCESS_CRITERIA}} - -## What Implementer Claims They Built - -{{IMPLEMENTER_REPORT}} - -## CRITICAL: Do Not Trust the Report - -The implementer's report may be incomplete, inaccurate, or optimistic. You MUST verify everything independently by reading the actual code. - -**DO NOT:** -- Take their word for what they implemented -- Trust their claims about completeness -- Accept their interpretation of requirements without verification - -**DO:** -- Read the actual code they wrote -- Compare the implementation to requirements line by line -- Check for missing pieces they claimed to implement -- Look for extra features they did not mention - -## Your Review - -Read the implementation code and evaluate: - -### Missing Requirements -- Did they implement everything that was requested in the success criteria? -- Are there requirements they skipped or missed? -- Did they claim something works but did not actually implement it? -- Are there edge cases implied by the criteria that are not handled? - -### Extra/Unneeded Work -- Did they build things that were not requested? -- Did they over-engineer or add unnecessary features? -- Did they add "nice to haves" that were not in the spec? -- Are there abstractions or layers that the spec did not call for? - -### Misunderstandings -- Did they interpret requirements differently than intended? -- Did they solve the wrong problem? -- Did they implement the right feature but in the wrong way? - -## Report Format - -Respond with exactly one of: - -**If compliant:** -``` -## Spec Review: PASS - -All requirements met. Implementation matches specification. -[Optional: brief note on anything worth highlighting] -``` - -**If issues found:** -``` -## Spec Review: FAIL - -### Issues Found -1. **[Missing/Extra/Misunderstood]:** [Description] - - File: `path/to/file:line` - - Expected: [what spec requires] - - Actual: [what was implemented or missing] - -2. ... -``` +--- +{} +--- + +# Spec Compliance Review Prompt Template + +This template is used by the `workflows:work` orchestrator to dispatch a spec compliance reviewer subagent when `--review-mode inline` or `--review-mode both` is active. The orchestrator fills in context blocks before passing the result to a subagent. + +**This is NOT an invocable agent.** It is a reference document consumed by the orchestrator. + +--- + +You are a spec compliance reviewer. Your job is to verify whether an implementation matches its specification -- nothing more, nothing less. + +## What Was Requested + +{{UNIT_REQUIREMENTS}} + +## Success Criteria + +{{SUCCESS_CRITERIA}} + +## Unit Purpose + +{{UNIT_PURPOSE}} + +## What Implementer Claims They Built + +{{IMPLEMENTER_REPORT}} + +## CRITICAL: Do Not Trust the Report + +The implementer's report may be incomplete, inaccurate, or optimistic. You MUST verify everything independently by reading the actual code. + +**DO NOT:** +- Take their word for what they implemented +- Trust their claims about completeness +- Accept their interpretation of requirements without verification + +**DO:** +- Read the actual code they wrote +- Compare the implementation to requirements line by line +- Check for missing pieces they claimed to implement +- Look for extra features they did not mention + +## TDD Evidence Gate + +Read `### TDD Evidence` in the implementer report before reviewing code. + +Apply `commands/workflows/references/tdd-evidence-contract.md` as the source of truth for Ralph evidence semantics and review-gate classifications. + +Use this gate for **behavior coverage** only: +- **Missing behavior coverage** = no trustworthy red/green evidence that the requested behavior was specified first and then made to pass. +- Cleanup/refactor-quality issues belong in quality review unless they break the requested behavior or invalidate the evidence. + +## Your Review + +Read the implementation code and evaluate: + +### Missing Requirements +- Did they implement everything that was requested in the success criteria? +- Is the requested behavior covered by trustworthy red/green evidence? +- Are there requirements they skipped or missed? +- Did they claim something works but did not actually implement it? +- Are there edge cases implied by the criteria that are not handled? + +### Extra/Unneeded Work +- Did they build things that were not requested? +- Did they over-engineer or add unnecessary features? +- Did they add "nice to haves" that were not in the spec? +- Are there abstractions or layers that the spec did not call for? + +### Misunderstandings +- Did they interpret requirements differently than intended? +- Did they solve the wrong problem? +- Did they implement the right feature but in the wrong way? + +## Report Format + +Respond with exactly one of: + +**If compliant:** +``` +## Spec Review: PASS + +All requirements met. Implementation matches specification. +[Optional: brief note on anything worth highlighting] +``` + +**If issues found:** +``` +## Spec Review: FAIL + +### Issues Found +1. **[Missing/Extra/Misunderstood/Missing Behavior Coverage]:** [Description] + - File: `path/to/file:line` + - Expected: [what spec requires] + - Actual: [what was implemented or missing] + - Evidence: [code or TDD signal that proves it] + +2. ... +``` + +Keep the report terse. Cite the missing or contradictory requirement and the evidence that proves it. diff --git a/plugins/compound-engineering/commands/workflows/references/tdd-evidence-contract.md b/plugins/compound-engineering/commands/workflows/references/tdd-evidence-contract.md new file mode 100644 index 0000000..d6d543b --- /dev/null +++ b/plugins/compound-engineering/commands/workflows/references/tdd-evidence-contract.md @@ -0,0 +1,44 @@ +--- +{} +--- + +# Shared TDD and Evidence Contract + +This reference is the single source of truth for TDD contract resolution, plan output shape, Ralph evidence semantics, and review-gate classifications across the workflow chain. + +## Contract Resolution + +- Plan-level `tdd` values override `compound-engineering.local.md`. +- Any field set to `inherit` falls back to the local config. +- If no local config exists, default to Ralph-driven `red-green-refactor` with unit + e2e evidence required. +- Any relaxation from Ralph or the unit + e2e default must be explicit, justified in `tdd.exceptions`, and paired with `replacement_evidence`. +- Do not silently weaken the TDD contract. + +## Plan Section Shape + +When a plan emits `## TDD & Evidence Contract`, keep this exact bullet shape and fill it with the resolved values: + +- **Precedence:** [how plan values override/fall back] +- **Effective mode:** [Ralph-driven TDD | Standard implementation] +- **Effective loop:** [Failing tests first -> minimal implementation -> refactor -> post-refactor rerun | Implementation-first] +- **Required evidence:** [Unit command/result], [E2E command/result] +- **Exceptions:** [None, or explicit justified deviation plus `replacement_evidence`] + +## Ralph Evidence Block + +When Ralph-driven execution is active, keep a stable `### TDD Evidence` / `## TDD Evidence` block with `Red`, `Green`, and `Post-Refactor Green` entries. + +- `Red` and `Green` prove behavior coverage. +- `Red` must fail for the missing behavior, not setup, import, syntax, or environment noise. +- `Green` must show the requested behavior now passes. +- `Post-Refactor Green` proves cleanup safety; even if no cleanup was needed, rerun and say so. +- Replacement evidence is valid only when an approved exception explicitly allows it. + +## Review Gate Classifications + +When auditing evidence, classify findings exactly this way: + +- **Missing behavior coverage** — weak or missing `Red`/`Green`, or evidence that does not prove the requested behavior. +- **Missing cleanup after refactor** — weak or missing `Post-Refactor Green`, or no rerun evidence after cleanup/refactor was claimed. + +Keep the gate output terse and evidence-based: cite the task/session file, the weak or missing block, and the concrete reason. diff --git a/plugins/compound-engineering/commands/workflows/references/ticket-execution-contract.md b/plugins/compound-engineering/commands/workflows/references/ticket-execution-contract.md new file mode 100644 index 0000000..187a50e --- /dev/null +++ b/plugins/compound-engineering/commands/workflows/references/ticket-execution-contract.md @@ -0,0 +1,149 @@ +# Ticket Execution Contract + +Use this reference when writing or consuming local ticket artifacts. + +This contract is shared by `/workflows:to-issues`, the `focused-ticket-priming` skill, `/workflows:work`, and `/workflows:review` so ticket generation and ticket execution stay aligned. + +## Ticket set layout + +Write ticket sets to: + +```text +docs/tickets/YYYY-MM-DD-/ +``` + +Required files: + +- `index.md` +- `NN-.md` for each execution-ready ticket + +## Ticket set index + +`index.md` must include: + +- frontmatter with: + - `plan_ref` + - `architecture_ref` or an explicit handoff note + - `execution_shape` + - `ticket_set_status` -- `ready | in_progress | blocked | completed` + - `last_completed_batch` + - `total_batches` +- plan path +- architecture artifact path or explicit handoff note +- execution shape +- ticket order +- dependency graph +- execution batches +- file-overlap safety notes for every multi-ticket batch +- blocker summary +- review summary split into `Blocking gaps` and `Recommendations` + +Recommended body shape: + +1. `# Ticket Set: ` +2. `## Dependency Graph` +3. `## Execution Batches` +4. `## Ticket Table` +5. `## Blockers` +6. `## Review Summary` + +## Ticket file naming + +- Keep zero-padded numeric prefixes in execution order: `01-...`, `02-...`, `03-...` +- Use the slug to describe the outcome, not the layer or implementation detail + +## Required ticket frontmatter + +Every ticket file must include this frontmatter shape: + +```yaml +--- +ticket_id: T01 +title: Tracer bullet for account onboarding +kind: tracer-bullet # tracer-bullet | expansion | hardening | infra-track | fix-batch +status: ready # ready | in_progress | blocked | completed +plan_ref: docs/plans/2026-05-20-account-onboarding-plan.md +tickets_ref: docs/tickets/2026-05-20-account-onboarding/index.md +architecture_ref: docs/architecture/2026-05-20-account-onboarding.md +source_packet_ref: "## Execution Slices > Slice 1" +feature_home: src/features/account-onboarding +depends_on: [] +dependency_type: none # none | hard | soft | parallel-safe +serves: + - Success criterion 1 +files: + - src/features/account-onboarding/controller.ts +test_command: bun test tests/account-onboarding.test.ts +tdd_mode: inherit +--- +``` + +## Required ticket body + +Every ticket must include these sections in this order: + +1. `# ` +2. `## Serves` +3. `## Scope` +4. `## Scope Fence` +5. `## Acceptance Criteria` +6. `## Shared / Global Notes` +7. `## Local Context` +8. `## Parent Refs` +9. `## Deeper-Dive Refs` +10. `## Coupling Notes` + +## Local context rules + +The `## Local Context` section is the execution packet. + +It must include: + +- the smallest WHY summary needed for this ticket +- the concrete files and interfaces that matter now +- architectural boundary notes that keep the feature home honest +- explicit unknowns or open questions that an execution agent must surface instead of guessing + +It must NOT: + +- copy the whole plan +- restate the whole architecture artifact +- duplicate repo-global guardrails that already belong in global context + +## Status lifecycle + +Use these ticket statuses: + +- `ready` -- ticket is execution-ready +- `in_progress` -- active execution session owns it +- `blocked` -- a dependency or blocker prevents safe execution +- `completed` -- execution finished and required evidence exists + +## Execution consumption rules + +When `/workflows:work` receives `index.md`: + +- treat the index as the authoritative ticket queue for this ticket set +- read `last_completed_batch` to choose the next unfinished batch +- load only the tickets named in that next batch +- execute multiple tickets together only when the batch is explicitly declared as parallel-safe and the index records why the file sets do not conflict +- if the index or ticket files leave overlap safety ambiguous, collapse that batch to sequential execution instead of guessing +- update batch progress in `index.md`, and increment `last_completed_batch` only after every ticket in the batch reaches `completed` + +When `/workflows:work` receives a ticket file: + +- treat that ticket as one pre-scoped execution unit +- load `plan_ref` and `architecture_ref` for WHY and architecture context +- honor the ticket's `feature_home`, `files`, and `## Scope Fence` +- stop and send the user back to `/workflows:to-issues` if required frontmatter or body sections are missing + +## Review consumption rules + +When `/workflows:review` or `ticket-flow-auditor` consumes ticket artifacts, verify: + +- the ticket still matches the parent plan and architecture +- the implementation stayed inside the ticket scope fence unless the change was explicitly documented +- dependency order and status changes stayed honest +- the dependency graph and batch partition still match the ticket files +- parallel-safe batches really stay file-disjoint and race-safe +- evidence matches the ticket's stated acceptance criteria and test command diff --git a/plugins/compound-engineering/commands/workflows/references/ticketization-contract.md b/plugins/compound-engineering/commands/workflows/references/ticketization-contract.md new file mode 100644 index 0000000..d5042e5 --- /dev/null +++ b/plugins/compound-engineering/commands/workflows/references/ticketization-contract.md @@ -0,0 +1,166 @@ +# Ticketization Workflow Contract + +Use this reference when turning a plan into smaller local ticket artifacts. + +This contract exists to keep `/workflows:plan` and `/deepen-plan` strategic while moving execution-sized context into independently usable tickets. + +## Mandatory ticket-packaging assets + +Ticketization must use these shared assets instead of inventing ad hoc ticket shapes: + +- `focused-ticket-priming` -- the packaging skill that turns one execution packet into one compact ticket packet +- `ticket-execution-contract.md` -- the exact frontmatter/body schema that `/workflows:work` and `/workflows:review` will consume later + +## Canonical command + +- **Command name:** `/workflows:to-issues` +- **Placement:** after `/workflows:plan` or `/deepen-plan` +- **Recommended timing:** after `/deepen-plan` when the plan still needs architectural or research hardening + +## Inputs + +A ticketization run requires: + +- `plan_ref` -- the source plan +- `execution_shape` -- must already be explicit in the plan +- plan WHY artifacts: Problem Narrative, User Story, Success Criteria, Architectural Context +- the architecture artifact at `architecture_ref`, or an explicit architecture handoff contract + +If the plan is missing the WHY artifacts, execution shape, or architecture guidance, stop and ask the user to repair that first instead of guessing. + +## Output location + +Write the local ticket set to: + +```text +docs/tickets/YYYY-MM-DD-/ +``` + +Required outputs: + +- `index.md` -- ticket-set summary, dependency graph, execution batches, run guidance, and progress pointer +- `NN-.md` -- one file per ticket in execution order + +Record the ticket set back into the plan as: + +- `tickets_ref: docs/tickets/YYYY-MM-DD-/index.md` + +If frontmatter cannot be updated safely, add the path under `## Related Artifacts`. + +## v1 rollout + +The first version is **local-artifact first**. + +- Generate local ticket files only. +- Do not publish tracker issues automatically. +- Keep issue-tracker publishing as a later optional extension. + +## Ticket sizing rules + +- Start from the plan's declared execution packets instead of inventing a new backlog from scratch. +- Size tickets by **coupling and boundary clarity**, not raw line count. +- A ticket may cross backend, frontend, and tests when that is the thinnest honest slice. +- Split a packet when one ticket would deliver more than one meaningful outcome, blur ownership, or bury the feature home. +- Keep the first ticket a tracer bullet when the selected execution shape is `vertical-slices`. + +## Dependency graph and execution batches + +Ticketization must build a conservative ticket dependency graph and then derive execution batches from it. + +- Start from each ticket's explicit `depends_on` edges. +- Then partition tickets into `Batch 1`, `Batch 2`, and so on, where every ticket in a batch depends only on earlier batches. +- A batch may contain multiple tickets only when their declared `files` sets do not overlap and there is no unresolved shared mutable state, migration risk, or boundary ambiguity between them. +- If two tickets might race, overwrite each other, or require the same shared adapter/config surface, keep them in separate sequential batches. +- **Default-to-sequential rule:** if batch safety is uncertain, emit singleton batches instead of guessing at parallelism. + +The index file becomes the authoritative execution queue for ticketized work. `/workflows:work` should be able to read `index.md`, find the next unfinished batch, and execute only that batch without recomputing the whole backlog. + +## Required index progress state + +`index.md` must record batch progress explicitly so ticketized execution can resume from the index alone. + +Required fields: + +- `last_completed_batch` -- integer counter; `0` means no batches completed yet +- `total_batches` -- total number of execution batches in the ticket set +- batch-level status view showing which batches are pending, in progress, blocked, or completed + +`last_completed_batch` advances only after every ticket in that batch is complete. If any ticket in the batch is blocked or still running, do not advance the counter. + +## Required ticket-local context + +Every generated ticket must carry a compact execution packet that can stand on its own. + +Required fields: + +- `Ticket` +- `Kind` -- tracer-bullet | expansion | hardening | infra-track | fix-batch +- `Serves` +- `Feature home` +- `Scope` +- `Scope fence` +- `Files` +- `Depends on` +- `Dependency type` +- `Acceptance criteria` +- `Evidence / test command` +- `Shared / global notes` +- `Parent refs` -- plan path, architecture path, and source slice/packet reference +- `Deeper-dive refs` -- optional docs to read only when the ticket-local packet is insufficient +- `Coupling notes` -- why this ticket is one unit instead of multiple tickets + +Use `ticket-execution-contract.md` as the exact source of truth for the concrete frontmatter and section order. + +## Context packaging rules + +Split context across the existing three tiers: + +- **Global context** -- constitution rules, local defaults, repo-wide guardrails +- **On-demand context** -- architecture artifact, vertical-slice architecture contract, supporting docs +- **Ticket-local context** -- only the minimum packet needed to execute one ticket safely + +Ticketization should shrink execution context, not recreate the full plan inside every ticket. + +## Behavior after `plan` vs after `deepen-plan` + +- After **`/workflows:plan`**: allow earlier backlog shaping, but preserve visible uncertainty instead of pretending the research is settled +- After **`/deepen-plan`**: prefer this mode when the plan already has hardened boundaries, tests, and architecture guidance + +In both cases, never strip WHY or architecture references out of the tickets. + +## Final ticket-set review + +Ticketization ends with a final review sweep over the full ticket set. + +That sweep must use the dedicated `ticket-flow-auditor` reviewer so the same contract can be enforced again later during `/workflows:review`. + +That review must check: + +- vertical-slice honesty +- feature-home clarity +- shared/global boundary honesty +- blocker and dependency correctness +- dependency graph correctness +- execution batch safety and parallelization honesty +- file-overlap conflicts between tickets claimed to be parallel-safe +- context completeness +- ticket size and coupling quality + +The review output must separate: + +- **Blocking gaps** -- ticket set is not safe to execute yet +- **Recommendations** -- the set is usable, but could be improved + +## Completion standard + +The ticketization contract is satisfied only when: + +- the command name and placement are explicit +- the output path and artifact shape are explicit +- the priming skill and ticket schema contract are explicit +- local-artifact-first behavior is explicit +- `tickets_ref` recording is explicit +- the dependency graph and conservative batch partition are explicit +- the index progress pointer is explicit +- each ticket's required local context is explicit +- the final ticket-set review step and reviewer are explicit diff --git a/plugins/compound-engineering/commands/workflows/references/vertical-slice-architecture.md b/plugins/compound-engineering/commands/workflows/references/vertical-slice-architecture.md new file mode 100644 index 0000000..20e0d91 --- /dev/null +++ b/plugins/compound-engineering/commands/workflows/references/vertical-slice-architecture.md @@ -0,0 +1,72 @@ +# Vertical Slice Architecture Contract + +Use this reference alongside `commands/workflows/references/execution-shape.md` when the work is organized as `vertical-slices`. + +`execution-shape.md` answers **how to decompose work**. This contract answers **where the feature lives**, **what stays local**, **what stays shared**, and **how to split context after planning** so downstream agents can work with smaller packets. + +## Core rule + +Every real feature should have a single named **feature home**: one namespace, directory, or module boundary that acts as the default home for that feature's business behavior. + +Inside that feature home, keep together the code needed to reason about the behavior end to end: +- feature-local backend logic +- feature-local frontend/UI logic +- feature-local tests +- feature-local mappers, policies, presenters, or helper code that only serves that feature + +If the work spans multiple existing feature homes, say so explicitly instead of pretending one home owns everything. + +## Shared / global rule + +Vertical slices do **not** suspend DRY or SOLID. + +Keep code in shared or global namespaces when it is genuinely cross-feature infrastructure or a stable reusable building block, such as: +- primitives and shared utilities +- reusable base classes and exceptions +- design-system components +- framework or vendor integration adapters +- cross-feature contracts and translation layers + +Do **not** move core business behavior into shared directories just because more than one layer touches it. + +## Extraction rule + +Extract behavior out of a feature home only when at least one of these is true: +1. Multiple feature homes already rely on the same stable behavior. +2. The code is an adapter or contract boundary whose reason to change is cross-feature, not feature-specific. +3. The architecture artifact's deletion test shows the local version is now the wrong boundary. + +If none of those are true, keep the code in the feature home even if that means a little local duplication for now. + +## Drift checks + +Treat these as architectural drift unless explicitly justified: +- Core feature behavior scattered across horizontal directories with no clear feature home. +- Copy-pasted shared abstractions across multiple feature homes. +- Shared/global directories that mainly contain one feature's business rules. +- Tickets or execution slices that cannot name the feature home they are changing. + +## Context tiers + +After planning, split context into these tiers: + +| Tier | What belongs here | Why | +|---|---|---| +| Global context | constitution rules, local config defaults, repo-wide architectural guardrails, stable shared/global boundaries | every downstream phase needs these rules | +| On-demand context | architecture artifact, this vertical-slice contract, deep design references, related docs | useful for deeper dives without bloating every ticket | +| Ticket-local context | feature home, exact files, scope fence, success criteria, evidence command, immediate blockers, concise WHY linkage | the smallest packet an execution agent should need by default | + +## Workflow obligations + +- **`/workflows:brainstorm`** should identify the likely feature home, neighboring boundaries, and any obvious shared/global constraints. +- **`/workflows:plan`** should keep execution packets honest and, for `vertical-slices`, require every slice to name its feature home. +- **`/workflows:architecture`** should confirm feature homes, shared/global decisions, context tiers, and drift checks in a durable artifact. +- **`/deepen-plan`**, **`/workflows:work`**, and **`/workflows:review`** must preserve the same feature-home and shared/global boundary decisions instead of re-inventing them later. + +## Packet add-on for `vertical-slices` + +When the selected execution shape is `vertical-slices`, every execution packet must name: +- `Feature home` +- any shared/global dependency it relies on when that dependency matters to scope or sequencing + +The packet stays execution-sized. Deeper architectural reasoning belongs in the architecture artifact and other on-demand context, not inside every slice body. diff --git a/plugins/compound-engineering/commands/workflows/review.md b/plugins/compound-engineering/commands/workflows/review.md index ae8cf93..9e2bab4 100644 --- a/plugins/compound-engineering/commands/workflows/review.md +++ b/plugins/compound-engineering/commands/workflows/review.md @@ -612,7 +612,7 @@ After creating all todo files, present comprehensive summary: 3. **Triage All Todos**: ```bash ls todos/*-pending-*.md # View all pending todos - /triage # Use slash command for interactive triage + /workflows:triage # Use slash command for interactive triage ``` ```` diff --git a/plugins/compound-engineering/commands/workflows/triage.md b/plugins/compound-engineering/commands/workflows/triage.md new file mode 100644 index 0000000..a58b74d --- /dev/null +++ b/plugins/compound-engineering/commands/workflows/triage.md @@ -0,0 +1,360 @@ +--- +name: "workflows:triage" +description: Research each todo, resolve decisions one-by-one, write chosen actions into todo files, then execute safe batches in swarm mode with execution-agent +argument-hint: "[todo range or scope]" +disable-model-invocation: true +--- + +- Read all target todo files before asking decisions. +- Keep main context as orchestration, research, decision, and validation space; execution agents do implementation. +- Do not start execution until all targeted todos are fully triaged and updated in place. + +Use this command when you need to process review todos end-to-end: + +1. research each todo against the current codebase, +2. present grounded action options to the user, +3. resolve open questions one-by-one, +4. write the selected action and context back into every todo file, +5. build safe swarm batches with dedicated execution scopes, +6. orchestrate execution-agent runs with full packets, +7. validate each result independently, +8. close statuses cleanly. + +## Core Rule Set + +**IMPORTANT: During research and decision phases, DO NOT implement code fixes.** + +This command is for: + +- Review-todo triage and decision closure +- Converting vague todos into researched, executable units +- Writing action-ready todo files before execution starts +- Swarm orchestration with strict scope fences and independent validation + +## Workflow + +### Step 1: Bootstrap and Scope + +1. Load project context and memory first. +2. Identify target todos from user scope (range, priority, status, or "all open"). +3. Build a deterministic queue sorted by todo id. +4. Confirm dependency order before any execution planning. + +Recommended checks: + +```bash +rg '^status:\s*(pending|in_progress|ready|blocked)' todos/*.md +``` + +### Step 2: Read and Research Every Target Todo Before Asking Decisions + +Read each todo fully before asking any question. Then do focused repository research for that todo so your proposed actions are grounded in reality rather than guesswork. + +Research for each todo should cover: + +- Problem statement quality and missing acceptance criteria +- Relevant code paths, modules, tests, docs, and prior patterns +- Dependency and ordering concerns +- Likely implementation surface and blast radius +- Validation expectations and evidence commands +- Risks, blockers, or ambiguity that should shape the decision + +The research pass must produce concrete action options. Do not present shallow "maybe do X" suggestions; each option must reflect what you found in the codebase. + +Present triage output per todo in this format: + +```markdown +--- +Todo #NNN: [Title] + +Status: [pending/in_progress/ready/blocked] +Priority: [p1/p2/p3] +Dependencies: [none/list] + +Research Summary: +- Relevant files: [file], [file] +- Existing pattern: [summary] +- Validation surface: [tests/checks] + +Possible Actions: +1. [Action name] - [what changes] - [main tradeoff] +2. [Action name] - [what changes] - [main tradeoff] +3. [Action name] - [what changes] - [main tradeoff] + +Recommended Action: +- [Suggested action and why] + +Open Decisions: +1. [Decision question] +2. [Decision question] + +Execution Risks: +- [Risk] +--- +``` + +### Step 3: Resolve Open Decisions One Question at a Time + +Ask only one question at a time. Do not batch decisions. + +Decision prompt format: + +```markdown +Decision for Todo #NNN: +[Clear question] + +Research-backed options: +1. [option A] +2. [option B] +3. [option C] + +Recommended: [option] +Why: [brief evidence-based reason] +``` + +Rules: + +- Wait for explicit answer before asking the next question. +- If user gives freeform direction, normalize it and confirm in one sentence. +- No implementation starts until the full target set has been triaged and all required decisions are resolved. + +### Step 4: Write the Selected Action Back into Every Todo File + +Update todo files immediately after each resolved decision so the todo becomes the authoritative execution packet seed. + +Expected updates: + +1. Add or refresh `## Recommended Action` +2. Record the chosen action, scope fence, likely files, and validation commands +3. Append `## Work Log` entry with decision outcome and research summary +4. Keep status accurate: + - stays `pending` after triage-only updates + - moves to `in_progress` only when the todo is dispatched to execution + - moves to `done` only after independent validation passes + +Work log template: + +```markdown +### YYYY-MM-DD - Triage decisions recorded + +**By:** @user + +**Actions:** +- [Selected action recorded] +- [Scope fence or dependency decision recorded] + +**Learnings:** +- [Relevant codebase pattern discovered] +- [Why this direction was chosen] +``` + +### Step 5: Build a Swarm Plan After All Todos Are Triaged + +Do not execute immediately after the first todo is approved. First finish triage for the full target set, then build a safe execution plan. + +Create: + +1. A dependency graph across all targeted todos +2. A file-overlap and blast-radius check +3. Parallel-safe batches where each todo has a dedicated scope +4. A fallback serialization rule for any items that overlap too much + +Only put todos in the same swarm batch when: + +- their dependencies are already done or outside the target set, +- their likely file surfaces do not materially overlap, +- they do not require the same migration, schema, or shared contract change, +- their validation can run independently. + +If parallel safety is unclear, split the work into smaller or serial batches. + +### Step 6: Build a Full Execution Packet per Todo + +Before launching execution-agent, prepare a full context packet. Do not send minimal prompts. + +Every packet must include: + +- Repository path and branch +- Exact todo file path and title +- Goal and acceptance criteria +- Research summary and selected action +- Explicitly resolved decisions +- Scope fence (what not to change) +- Likely files and tests +- Validation expectations +- Reporting contract (what execution-agent must return) + +Execution packet skeleton: + +```markdown +AGENT_TEMPLATE loaded via local agent repository. Follow exactly. + +Repository: [path] +Branch: [branch] + +## Your Unit +Todo file: [path] +Title: [title] +Goal: [goal] + +## Selected Action +- [final action] + +## Research Summary +- [existing pattern] +- [relevant files] +- [validation surface] + +## Decisions (Final) +- [decision] +- [decision] + +## Architecture Handoff +Acceptance criteria: +1. [...] +2. [...] + +Scope fence: +- [...] +- [...] + +Likely files: +- [file] +- [file] + +Validation contract: +- run/update tests relevant to this todo +- report red/green/post-refactor evidence +- provide changed files and rationale +``` + +### Step 7: Execute in Swarm Mode with Dedicated Scopes + +Execute by safe batch, not by one giant parallel blast and not by immediate one-off serial runs. + +For each safe batch: + +1. Set each batch todo status to `in_progress` +2. Dispatch one `execution-agent` per todo with its full packet and dedicated scope +3. Keep the orchestrator focused on batch coordination, validation, and status integrity +4. Wait for every agent in the batch to complete +5. Review each execution report separately +6. Validate each todo independently from the orchestration context +7. Mark validated todos `done`; keep failures `in_progress` or `blocked` with exact reasons +8. Only advance dependent batches after prerequisites are truly complete + +Hard rules: + +- Every execution-agent owns exactly one todo scope at a time. +- Do not merge multiple todos into one agent prompt. +- Do not allow two agents to edit the same unstable surface unless the batch plan explicitly proves safety. +- If one todo in a batch fails, do not discard successful siblings; validate and close each todo independently. + +### Step 8: Orchestration-Side Validation (Mandatory) + +Never rely only on subagent self-report. Orchestrator validates. + +Validation checklist per todo: + +- targeted tests pass for the changed area +- expected files actually changed +- scope fence was respected +- todo acceptance criteria are now true +- todo status, selected action, and work log are updated + +Completion log template: + +```markdown +### YYYY-MM-DD - Execution completed + +**Actions:** +- [Implemented change summary] + +**Validation:** +- `command 1` +- `command 2` +``` + +### Step 9: Final Sweep and Completion Report + +After all todos are processed: + +1. Check no target todo remains stale without explanation +2. Report done, blocked, and in-progress counts +3. List any follow-up work created by the execution batches + +Final report format: + +```markdown +## Triage + Swarm Execution Complete + +**Total Targeted:** [X] +**Done:** [Y] +**Still Open:** [Z] + +### Done +- [todo-id] [title] + +### Still Open / Blocked +- [todo-id] [reason] + +### Validation Run +- [key command] +- [key command] +``` + +## Important Implementation Details + +### Status Discipline + +- `pending`: triaged and documented, but not executing yet +- `in_progress`: actively executing or retrying +- `done`: validated and closed +- `blocked`: cannot continue; include concrete blocker + +### Context Discipline + +- Main context owns research, decisions, swarm planning, validation, and status integrity. +- execution-agent owns code edits for one scoped todo at a time. +- Do not duplicate the same implementation work in both contexts. + +### Decision Discipline + +- Research before presenting options. +- Ask one decision question at a time. +- Write answers into the todo immediately. +- Never "assume defaults" if user decision is explicitly required. + +### Swarm Planning Quality Bar + +Bad swarm plan: +- "Run all open todos in parallel" + +Good swarm plan: +- groups only dependency-safe, low-overlap todos into the same batch +- gives each todo its own packet, scope fence, and validation path +- serializes risky overlaps instead of pretending they are safe + +### Do / Don't + +- ✅ Do research each todo before presenting options. +- ✅ Do capture possible actions grounded in the current codebase. +- ✅ Do update all targeted todo files before starting execution. +- ✅ Do batch execution in swarm mode only when scopes are truly parallel-safe. +- ✅ Do validate each completed todo independently. +- ❌ Don't code during the research and decision phase. +- ❌ Don't ask multiple decisions in one message. +- ❌ Don't start execution before the target set is fully triaged. +- ❌ Don't mark done before orchestration-side validation. +- ❌ Don't drop selected actions or research findings from todo logs. + +## Done Options + +When all targeted todos are processed, end with: + +```markdown +What would you like to do next? + +1. commit and push current completed todo batch +2. stop here +``` diff --git a/plugins/compound-engineering/skills/file-todos/SKILL.md b/plugins/compound-engineering/skills/file-todos/SKILL.md index 17ea141..4a678e2 100644 --- a/plugins/compound-engineering/skills/file-todos/SKILL.md +++ b/plugins/compound-engineering/skills/file-todos/SKILL.md @@ -116,7 +116,7 @@ dependencies: ["001"] # Issue IDs this is blocked by - Adjust priority if different from initial assessment 4. Deferred todos stay in `pending` status -**Use slash command:** `/triage` for interactive approval workflow +**Use slash command:** `/workflows:triage` for research-backed interactive approval workflow ### Managing Dependencies @@ -186,7 +186,7 @@ Work logs serve as: | Trigger | Flow | Tool | |---------|------|------| -| Code review | `/workflows:review` → Findings → `/triage` → Todos | Review agent + skill | +| Code review | `/workflows:review` → Findings → `/workflows:triage` → Todos | Review agent + skill | | PR comments | `/resolve_pr_parallel` → Individual fixes → Todos | gh CLI + skill | | Code TODOs | `/resolve_todo_parallel` → Fixes + Complex todos | Agent + skill | | Planning | Brainstorm → Create todo → Work → Complete | Skill | diff --git a/portable/compound-engineering/commands/triage.md b/portable/compound-engineering/commands/triage.md deleted file mode 100644 index 8831b11..0000000 --- a/portable/compound-engineering/commands/triage.md +++ /dev/null @@ -1,321 +0,0 @@ ---- -name: triage -description: Triage review todos, resolve open decisions one-by-one, then orchestrate execution-agent runs with strict per-item validation -argument-hint: '[todo range or scope]' -platforms: - claude: - disable-model-invocation: true ---- - -- Read all target todo files before asking decisions. -- Keep main context as orchestration + validation space; execution agents do implementation. - -Use this command when you need to process review todos end-to-end: - -1. triage each todo, -2. resolve open questions with the user, -3. write decisions into todo files, -4. execute todos one at a time via execution-agent with full context packets, -5. validate each result independently, -6. close statuses cleanly. - -## Core Rule Set - -**IMPORTANT: During triage/decision phases, DO NOT implement code fixes.** - -This command is for: - -- Review-todo triage and decision closure -- Converting vague todos into executable units -- Sequential execution-agent dispatch with rich context -- Independent orchestration-side validation and status control - -## Workflow - -### Step 1: Bootstrap and Scope - -1. Load project context and memory first. -2. Identify target todos from user scope (range, priority, status, or "all open"). -3. Build a deterministic queue sorted by todo id. - -Recommended checks: - -```bash -rg '^status:\s*(pending|in_progress|ready)' todos/*.md -``` - -### Step 2: Read and Triage All Target Todos - -Read each todo fully before asking any question. Capture: - -- Problem statement quality -- Missing decisions or unresolved options -- Dependency and ordering concerns -- Validation expectations -- Coupled files/tests/docs likely needed for execution - -Present triage output per todo in this format: - -```markdown ---- -Todo #NNN: [Title] - -Status: [pending/in_progress/ready] -Priority: [p1/p2/p3] -Dependencies: [none/list] - -Open Decisions: -1. [Decision question] -2. [Decision question] - -Execution Risks: -- [Risk] - -Initial Recommendation: -- [Suggested choice and why] ---- -``` - -### Step 3: Resolve Open Decisions (One Question at a Time) - -Ask only one question at a time. Do not batch decisions. - -Decision prompt format: - -```markdown -Decision for Todo #NNN: -[Clear question] - -Recommended: [option] -1. [option A] -2. [option B] -3. [option C] -``` - -Rules: - -- Wait for explicit answer before asking next question. -- If user gives freeform decision, normalize it and confirm in one sentence. -- No implementation starts until decisions for that todo are resolved. - -### Step 4: Write Decisions Back into Todo Files - -Update todo files immediately after each resolved decision. - -Expected updates: - -1. Add/refresh `## Recommended Action` -2. Append `## Work Log` entry with decision outcome -3. Keep status accurate: - - stays `pending` after triage-only updates - - moves to `in_progress` when execution starts - - moves to `done` only after independent validation passes - -Work log template: - -```markdown -### YYYY-MM-DD - Triage decisions recorded - -**By:** @user - -**Actions:** -- [Decision 1 captured] -- [Decision 2 captured] - -**Learnings:** -- [Why this direction was chosen] -``` - -### Step 5: Build an Execution Packet per Todo - -Before launching execution-agent, prepare a full context packet. Do not send minimal prompts. - -Every packet must include: - -- Repository path and branch -- Exact todo file path and title -- Goal and acceptance criteria -- Resolved decisions (explicitly listed) -- Scope fence (what not to change) -- Likely files and tests -- Validation expectations -- Reporting contract (what execution-agent must return) - -Execution packet skeleton: - -```markdown -AGENT_TEMPLATE loaded via local agent repository. Follow exactly. - -Repository: [path] -Branch: [branch] - -## Your Unit -Todo file: [path] -Title: [title] -Goal: [goal] - -## Decisions (Final) -- [decision] -- [decision] - -## Architecture Handoff -Acceptance criteria: -1. [...] -2. [...] - -Scope fence: -- [...] -- [...] - -## Likely Files -- [file] -- [file] - -## Validation Contract -- run/update tests relevant to this todo -- report red/green/post-refactor evidence -- provide changed files and rationale -``` - -### Step 6: Execute One Todo at a Time via execution-agent - -For each todo in queue: - -1. Set todo status to `in_progress` -2. Dispatch execution-agent in sync mode with full packet -3. Review execution report -4. Validate independently from orchestration context -5. If validation fails, keep `in_progress`, refine packet, and re-run -6. On success, set status to `done` and log completion evidence in todo - -Do not run parallel execution agents for this workflow unless user asks for parallelism. - -### Step 7: Orchestration-Side Validation (Mandatory) - -Never rely only on subagent self-report. Orchestrator validates. - -Validation checklist per todo: - -- targeted tests pass for changed area -- expected files actually changed -- scope fence was respected -- todo acceptance criteria now true -- todo status/frontmatter/log updated - -Completion log template: - -```markdown -### YYYY-MM-DD - Execution completed - -**Actions:** -- [Implemented change summary] - -**Validation:** -- `command 1` -- `command 2` -``` - -### Step 8: Final Sweep and Completion Report - -After all todos processed: - -1. Check no target todo remains `pending`/`in_progress` -2. Report done/incomplete counts -3. List any blocked items with exact blocker - -Final report format: - -```markdown -## Triage + Execution Complete - -**Total Targeted:** [X] -**Done:** [Y] -**Still Open:** [Z] - -### Done -- [todo-id] [title] - -### Still Open / Blocked -- [todo-id] [reason] - -### Validation Run -- [key command] -- [key command] -``` - -## Example Interaction Flow - -```markdown -Todo #057 has one open decision: auth hardening now vs defer. -Recommendation: defer auth to next phase and document deployment constraint. - -Which direction do you want? -1. Defer auth now and document constraints (Recommended) -2. Implement auth hardening now -``` - -Then: - -```markdown -Recorded decision in `todos/057-...md`. -Now dispatching execution-agent for todo 057 with full packet. -``` - -Then: - -```markdown -Todo 057 implemented and validated. -Status updated to done. -Proceeding to todo 058. -``` - -## Important Implementation Details - -### Status Discipline - -- `pending`: triaged but not executing -- `in_progress`: actively executing/iterating -- `done`: validated and closed -- `blocked`: cannot continue; include concrete blocker - -### Context Discipline - -- Main context owns orchestration, decisions, validation, and status integrity. -- execution-agent owns code edits for one scoped todo at a time. -- Do not duplicate the same implementation work in both contexts. - -### Decision Discipline - -- Ask one decision question at a time. -- Write answers into todo immediately. -- Never "assume defaults" if user decision is explicitly required. - -### Execution Prompt Quality Bar - -Bad packet: -- "Implement todo 059" - -Good packet: -- includes decisions, acceptance criteria, scope fence, likely files, and tests. - -### Do / Don't - -- ✅ Do triage all targeted todos before heavy execution when user asks for prep. -- ✅ Do capture recommendations before asking decisions. -- ✅ Do validate each completed todo independently. -- ✅ Do keep todo files as source of truth. -- ❌ Don't code during triage-only phase. -- ❌ Don't ask multiple decisions in one message. -- ❌ Don't mark done before orchestration-side validation. -- ❌ Don't drop decision outcomes from todo logs. - -## Done Options - -When all targeted todos are processed, end with: - -```markdown -What would you like to do next? - -1. commit and push current completed todo batch -2. stop here -``` diff --git a/portable/compound-engineering/commands/workflows/review.md b/portable/compound-engineering/commands/workflows/review.md index 801aa81..0a34ec7 100644 --- a/portable/compound-engineering/commands/workflows/review.md +++ b/portable/compound-engineering/commands/workflows/review.md @@ -614,7 +614,7 @@ After creating all todo files, present comprehensive summary: 3. **Triage All Todos**: ```bash ls todos/*-pending-*.md # View all pending todos - /triage # Use slash command for interactive triage + /workflows:triage # Use slash command for interactive triage ``` ```` diff --git a/portable/compound-engineering/commands/workflows/triage.md b/portable/compound-engineering/commands/workflows/triage.md new file mode 100644 index 0000000..61434f0 --- /dev/null +++ b/portable/compound-engineering/commands/workflows/triage.md @@ -0,0 +1,362 @@ +--- +name: workflows:triage +description: Research each todo, resolve decisions one-by-one, write chosen actions into todo files, then execute safe batches in swarm mode with execution-agent +argument-hint: '[todo range or scope]' +platforms: + claude: + disable-model-invocation: true +--- + +- Read all target todo files before asking decisions. +- Keep main context as orchestration, research, decision, and validation space; execution agents do implementation. +- Do not start execution until all targeted todos are fully triaged and updated in place. + +Use this command when you need to process review todos end-to-end: + +1. research each todo against the current codebase, +2. present grounded action options to the user, +3. resolve open questions one-by-one, +4. write the selected action and context back into every todo file, +5. build safe swarm batches with dedicated execution scopes, +6. orchestrate execution-agent runs with full packets, +7. validate each result independently, +8. close statuses cleanly. + +## Core Rule Set + +**IMPORTANT: During research and decision phases, DO NOT implement code fixes.** + +This command is for: + +- Review-todo triage and decision closure +- Converting vague todos into researched, executable units +- Writing action-ready todo files before execution starts +- Swarm orchestration with strict scope fences and independent validation + +## Workflow + +### Step 1: Bootstrap and Scope + +1. Load project context and memory first. +2. Identify target todos from user scope (range, priority, status, or "all open"). +3. Build a deterministic queue sorted by todo id. +4. Confirm dependency order before any execution planning. + +Recommended checks: + +```bash +rg '^status:\s*(pending|in_progress|ready|blocked)' todos/*.md +``` + +### Step 2: Read and Research Every Target Todo Before Asking Decisions + +Read each todo fully before asking any question. Then do focused repository research for that todo so your proposed actions are grounded in reality rather than guesswork. + +Research for each todo should cover: + +- Problem statement quality and missing acceptance criteria +- Relevant code paths, modules, tests, docs, and prior patterns +- Dependency and ordering concerns +- Likely implementation surface and blast radius +- Validation expectations and evidence commands +- Risks, blockers, or ambiguity that should shape the decision + +The research pass must produce concrete action options. Do not present shallow "maybe do X" suggestions; each option must reflect what you found in the codebase. + +Present triage output per todo in this format: + +```markdown +--- +Todo #NNN: [Title] + +Status: [pending/in_progress/ready/blocked] +Priority: [p1/p2/p3] +Dependencies: [none/list] + +Research Summary: +- Relevant files: [file], [file] +- Existing pattern: [summary] +- Validation surface: [tests/checks] + +Possible Actions: +1. [Action name] - [what changes] - [main tradeoff] +2. [Action name] - [what changes] - [main tradeoff] +3. [Action name] - [what changes] - [main tradeoff] + +Recommended Action: +- [Suggested action and why] + +Open Decisions: +1. [Decision question] +2. [Decision question] + +Execution Risks: +- [Risk] +--- +``` + +### Step 3: Resolve Open Decisions One Question at a Time + +Ask only one question at a time. Do not batch decisions. + +Decision prompt format: + +```markdown +Decision for Todo #NNN: +[Clear question] + +Research-backed options: +1. [option A] +2. [option B] +3. [option C] + +Recommended: [option] +Why: [brief evidence-based reason] +``` + +Rules: + +- Wait for explicit answer before asking the next question. +- If user gives freeform direction, normalize it and confirm in one sentence. +- No implementation starts until the full target set has been triaged and all required decisions are resolved. + +### Step 4: Write the Selected Action Back into Every Todo File + +Update todo files immediately after each resolved decision so the todo becomes the authoritative execution packet seed. + +Expected updates: + +1. Add or refresh `## Recommended Action` +2. Record the chosen action, scope fence, likely files, and validation commands +3. Append `## Work Log` entry with decision outcome and research summary +4. Keep status accurate: + - stays `pending` after triage-only updates + - moves to `in_progress` only when the todo is dispatched to execution + - moves to `done` only after independent validation passes + +Work log template: + +```markdown +### YYYY-MM-DD - Triage decisions recorded + +**By:** @user + +**Actions:** +- [Selected action recorded] +- [Scope fence or dependency decision recorded] + +**Learnings:** +- [Relevant codebase pattern discovered] +- [Why this direction was chosen] +``` + +### Step 5: Build a Swarm Plan After All Todos Are Triaged + +Do not execute immediately after the first todo is approved. First finish triage for the full target set, then build a safe execution plan. + +Create: + +1. A dependency graph across all targeted todos +2. A file-overlap and blast-radius check +3. Parallel-safe batches where each todo has a dedicated scope +4. A fallback serialization rule for any items that overlap too much + +Only put todos in the same swarm batch when: + +- their dependencies are already done or outside the target set, +- their likely file surfaces do not materially overlap, +- they do not require the same migration, schema, or shared contract change, +- their validation can run independently. + +If parallel safety is unclear, split the work into smaller or serial batches. + +### Step 6: Build a Full Execution Packet per Todo + +Before launching execution-agent, prepare a full context packet. Do not send minimal prompts. + +Every packet must include: + +- Repository path and branch +- Exact todo file path and title +- Goal and acceptance criteria +- Research summary and selected action +- Explicitly resolved decisions +- Scope fence (what not to change) +- Likely files and tests +- Validation expectations +- Reporting contract (what execution-agent must return) + +Execution packet skeleton: + +```markdown +AGENT_TEMPLATE loaded via local agent repository. Follow exactly. + +Repository: [path] +Branch: [branch] + +## Your Unit +Todo file: [path] +Title: [title] +Goal: [goal] + +## Selected Action +- [final action] + +## Research Summary +- [existing pattern] +- [relevant files] +- [validation surface] + +## Decisions (Final) +- [decision] +- [decision] + +## Architecture Handoff +Acceptance criteria: +1. [...] +2. [...] + +Scope fence: +- [...] +- [...] + +Likely files: +- [file] +- [file] + +Validation contract: +- run/update tests relevant to this todo +- report red/green/post-refactor evidence +- provide changed files and rationale +``` + +### Step 7: Execute in Swarm Mode with Dedicated Scopes + +Execute by safe batch, not by one giant parallel blast and not by immediate one-off serial runs. + +For each safe batch: + +1. Set each batch todo status to `in_progress` +2. Dispatch one `execution-agent` per todo with its full packet and dedicated scope +3. Keep the orchestrator focused on batch coordination, validation, and status integrity +4. Wait for every agent in the batch to complete +5. Review each execution report separately +6. Validate each todo independently from the orchestration context +7. Mark validated todos `done`; keep failures `in_progress` or `blocked` with exact reasons +8. Only advance dependent batches after prerequisites are truly complete + +Hard rules: + +- Every execution-agent owns exactly one todo scope at a time. +- Do not merge multiple todos into one agent prompt. +- Do not allow two agents to edit the same unstable surface unless the batch plan explicitly proves safety. +- If one todo in a batch fails, do not discard successful siblings; validate and close each todo independently. + +### Step 8: Orchestration-Side Validation (Mandatory) + +Never rely only on subagent self-report. Orchestrator validates. + +Validation checklist per todo: + +- targeted tests pass for the changed area +- expected files actually changed +- scope fence was respected +- todo acceptance criteria are now true +- todo status, selected action, and work log are updated + +Completion log template: + +```markdown +### YYYY-MM-DD - Execution completed + +**Actions:** +- [Implemented change summary] + +**Validation:** +- `command 1` +- `command 2` +``` + +### Step 9: Final Sweep and Completion Report + +After all todos are processed: + +1. Check no target todo remains stale without explanation +2. Report done, blocked, and in-progress counts +3. List any follow-up work created by the execution batches + +Final report format: + +```markdown +## Triage + Swarm Execution Complete + +**Total Targeted:** [X] +**Done:** [Y] +**Still Open:** [Z] + +### Done +- [todo-id] [title] + +### Still Open / Blocked +- [todo-id] [reason] + +### Validation Run +- [key command] +- [key command] +``` + +## Important Implementation Details + +### Status Discipline + +- `pending`: triaged and documented, but not executing yet +- `in_progress`: actively executing or retrying +- `done`: validated and closed +- `blocked`: cannot continue; include concrete blocker + +### Context Discipline + +- Main context owns research, decisions, swarm planning, validation, and status integrity. +- execution-agent owns code edits for one scoped todo at a time. +- Do not duplicate the same implementation work in both contexts. + +### Decision Discipline + +- Research before presenting options. +- Ask one decision question at a time. +- Write answers into the todo immediately. +- Never "assume defaults" if user decision is explicitly required. + +### Swarm Planning Quality Bar + +Bad swarm plan: +- "Run all open todos in parallel" + +Good swarm plan: +- groups only dependency-safe, low-overlap todos into the same batch +- gives each todo its own packet, scope fence, and validation path +- serializes risky overlaps instead of pretending they are safe + +### Do / Don't + +- ✅ Do research each todo before presenting options. +- ✅ Do capture possible actions grounded in the current codebase. +- ✅ Do update all targeted todo files before starting execution. +- ✅ Do batch execution in swarm mode only when scopes are truly parallel-safe. +- ✅ Do validate each completed todo independently. +- ❌ Don't code during the research and decision phase. +- ❌ Don't ask multiple decisions in one message. +- ❌ Don't start execution before the target set is fully triaged. +- ❌ Don't mark done before orchestration-side validation. +- ❌ Don't drop selected actions or research findings from todo logs. + +## Done Options + +When all targeted todos are processed, end with: + +```markdown +What would you like to do next? + +1. commit and push current completed todo batch +2. stop here +``` diff --git a/portable/compound-engineering/plugin.yaml b/portable/compound-engineering/plugin.yaml index caaaf04..d27cfc0 100644 --- a/portable/compound-engineering/plugin.yaml +++ b/portable/compound-engineering/plugin.yaml @@ -1,5 +1,5 @@ name: compound-engineering -version: 4.13.0 +version: 4.14.0 description: lead: OpenCode-first AI-powered development tools. suffix: spanning code review, research, design, and workflow automation, with generated Copilot support and Claude Code compatibility outputs. diff --git a/portable/compound-engineering/skills/file-todos/SKILL.md b/portable/compound-engineering/skills/file-todos/SKILL.md index 5456e14..2d4e48f 100644 --- a/portable/compound-engineering/skills/file-todos/SKILL.md +++ b/portable/compound-engineering/skills/file-todos/SKILL.md @@ -125,7 +125,7 @@ dependencies: ["001"] # Issue IDs this is blocked by - Adjust priority if different from initial assessment 4. Deferred todos stay in `pending` status -**Use slash command:** `/triage` for interactive approval workflow +**Use slash command:** `/workflows:triage` for research-backed interactive approval workflow ### Managing Dependencies @@ -195,7 +195,7 @@ Work logs serve as: | Trigger | Flow | Tool | |---------|------|------| -| Code review | `/workflows:review` → Findings → `/triage` → Todos | Review agent + skill | +| Code review | `/workflows:review` → Findings → `/workflows:triage` → Todos | Review agent + skill | | PR comments | `/resolve_pr_parallel` → Individual fixes → Todos | gh CLI + skill | | Code TODOs | `/resolve_todo_parallel` → Fixes + Complex todos | Agent + skill | | Planning | Brainstorm → Create todo → Work → Complete | Skill | diff --git a/src/commands/build.ts b/src/commands/build.ts index dad4116..31c069d 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -39,7 +39,9 @@ export default defineCommand({ if (targets.includes("claude")) { const claudeRoot = path.join(outputRoot, "plugins", plugin.manifest.name) + await removeGeneratedClaudeOutput(claudeRoot) await writeClaudeBundle(claudeRoot, plugin) + await fs.rm(path.join(claudeRoot, ".compound-engineering-claude-state.json"), { force: true }) await writeMarketplace(outputRoot, plugin) console.log(`Built Claude output at ${claudeRoot}`) } @@ -53,6 +55,7 @@ export default defineCommand({ const copilotRoot = path.join(outputRoot, ".github") await removeGeneratedCopilotOutput(copilotRoot) await writeCopilotBundle(copilotRoot, copilotBundle) + await fs.rm(path.join(copilotRoot, ".compound-engineering-copilot-state.json"), { force: true }) console.log(`Built Copilot output at ${copilotRoot}`) } }, @@ -100,4 +103,14 @@ async function removeGeneratedCopilotOutput(copilotRoot: string): Promise await fs.rm(path.join(copilotRoot, "agents"), { recursive: true, force: true }) await fs.rm(path.join(copilotRoot, "skills"), { recursive: true, force: true }) await fs.rm(path.join(copilotRoot, "copilot-mcp-config.json"), { force: true }) + await fs.rm(path.join(copilotRoot, ".compound-engineering-copilot-state.json"), { force: true }) +} + +async function removeGeneratedClaudeOutput(claudeRoot: string): Promise { + await fs.rm(path.join(claudeRoot, "agents"), { recursive: true, force: true }) + await fs.rm(path.join(claudeRoot, "commands"), { recursive: true, force: true }) + await fs.rm(path.join(claudeRoot, "skills"), { recursive: true, force: true }) + await fs.rm(path.join(claudeRoot, "hooks"), { recursive: true, force: true }) + await fs.rm(path.join(claudeRoot, ".claude-plugin", "plugin.json"), { force: true }) + await fs.rm(path.join(claudeRoot, ".compound-engineering-claude-state.json"), { force: true }) } diff --git a/src/commands/sync-ov.ts b/src/commands/sync-ov.ts index ca37d46..1c152ac 100644 --- a/src/commands/sync-ov.ts +++ b/src/commands/sync-ov.ts @@ -10,6 +10,7 @@ import { resolveTargetHome } from "../utils/resolve-home" const DEFAULT_OV_CORE = path.join(os.homedir(), ".copilot-skills", "ov-core.sh") const GLOBAL_SKILLS_URI = "viking://resources/_global/skills" +const OV_STATE_FILE_NAME = ".compound-engineering-sync-ov-state.json" type SkillSupportResource = { sourcePath: string @@ -23,6 +24,17 @@ type GeneratedCommandSkill = { commandSourcePath?: string } +type SyncOvState = { + version: 1 + agents: string[] + skills: string[] + supportUris: string[] +} + +const LEGACY_COMMAND_SKILL_RENAMES: Record = { + "workflows-triage": ["triage"], +} + export default defineCommand({ meta: { name: "sync-ov", @@ -51,9 +63,15 @@ export default defineCommand({ const skillSupportFiles = await collectSkillSupportResources(plugin) const commandSupportFiles = await collectCommandSupportResources(generatedCommandSkills) const supportFiles = [...skillSupportFiles, ...commandSupportFiles] + const syncState = buildSyncState(plugin, generatedCommandSkills, supportFiles) + const legacySkillNames = collectLegacySkillNames(plugin, generatedCommandSkills) try { - await fs.writeFile(scriptPath, buildSyncScript(ovCorePath, plugin, supportFiles, generatedCommandSkills), "utf8") + await fs.writeFile( + scriptPath, + buildSyncScript(ovCorePath, plugin, supportFiles, generatedCommandSkills, syncState, legacySkillNames), + "utf8", + ) const proc = Bun.spawn(["bash", scriptPath], { cwd: plugin.root, @@ -158,6 +176,8 @@ function buildSyncScript( plugin: PortablePlugin, supportFiles: SkillSupportResource[], generatedCommandSkills: GeneratedCommandSkill[], + syncState: SyncOvState, + legacySkillNames: string[], ): string { const lines = [ "#!/usr/bin/env bash", @@ -206,6 +226,76 @@ function buildSyncScript( " _ov_add_resource \"$source_file\" --parent \"$parent_uri\"", "}", "", + `OV_STATE_DIR="${'${OV_GLOBAL_DIR:-${HOME}/openviking_workspace/global}'}"`, + "mkdir -p \"$OV_STATE_DIR\"", + `OV_STATE_FILE="${'${OV_STATE_DIR}'}/${OV_STATE_FILE_NAME}"`, + "", + "compound_syncov_prune_stale() {", + " local kind=\"$1\"", + " local name=\"$2\"", + " if [[ -z \"$name\" ]]; then return 0; fi", + " if [[ \"$kind\" == \"agent\" ]]; then", + " rm -f \"${OV_GLOBAL_AGENTS_DIR}/${name}.md\"", + " rm -rf \"${OV_GLOBAL_AGENTS_DIR}/${name}\"", + " _ov_reset_uri \"${OV_GLOBAL_AGENTS_URI}/${name}\"", + " return 0", + " fi", + " rm -f \"${OV_GLOBAL_SKILLS_DIR}/${name}.md\"", + " rm -rf \"${OV_GLOBAL_SKILLS_DIR}/${name}\"", + " _ov_reset_uri \"${OV_GLOBAL_SKILLS_URI}/${name}\"", + "}", + "", + "compound_syncov_prune_support_uri() {", + " local uri=\"$1\"", + " if [[ -z \"$uri\" ]]; then return 0; fi", + " local prefix=\"${OV_GLOBAL_SKILLS_URI}/\"", + " if [[ \"$uri\" == \"$prefix\"* ]]; then", + " local rel=\"${uri#${prefix}}\"", + " rm -f \"${OV_GLOBAL_SKILLS_DIR}/${rel}\"", + " fi", + " _ov_reset_uri \"$uri\"", + "}", + "", + "compound_syncov_prune_from_state() {", + " local current_agents_json=\"$1\"", + " local current_skills_json=\"$2\"", + " local current_support_json=\"$3\"", + " python3 - \"$OV_STATE_FILE\" \"$current_agents_json\" \"$current_skills_json\" \"$current_support_json\" <<'PY'", + "import json, sys", + "state_path, agents_raw, skills_raw, support_raw = sys.argv[1:5]", + "try:", + " with open(state_path, 'r', encoding='utf-8') as fh:", + " state = json.load(fh)", + "except Exception:", + " state = {}", + "old_agents = set(state.get('agents') or [])", + "old_skills = set(state.get('skills') or [])", + "old_support = set(state.get('supportUris') or [])", + "new_agents = set(json.loads(agents_raw))", + "new_skills = set(json.loads(skills_raw))", + "new_support = set(json.loads(support_raw))", + "for name in sorted(old_agents - new_agents):", + " print(f'agent\\t{name}')", + "for name in sorted(old_skills - new_skills):", + " print(f'skill\\t{name}')", + "for uri in sorted(old_support - new_support):", + " print(f'support\\t{uri}')", + "PY", + "}", + "", + `CURRENT_AGENTS_JSON=${shellEscape(JSON.stringify(syncState.agents))}`, + `CURRENT_SKILLS_JSON=${shellEscape(JSON.stringify(syncState.skills))}`, + `CURRENT_SUPPORT_JSON=${shellEscape(JSON.stringify(syncState.supportUris))}`, + "", + "while IFS=$'\\t' read -r kind value; do", + " [[ -z \"$kind\" ]] && continue", + " case \"$kind\" in", + " agent) compound_syncov_prune_stale agent \"$value\" ;;", + " skill) compound_syncov_prune_stale skill \"$value\" ;;", + " support) compound_syncov_prune_support_uri \"$value\" ;;", + " esac", + "done < <(compound_syncov_prune_from_state \"$CURRENT_AGENTS_JSON\" \"$CURRENT_SKILLS_JSON\" \"$CURRENT_SUPPORT_JSON\")", + "", ] for (const agent of [...plugin.agents].sort((left, right) => left.name.localeCompare(right.name))) { @@ -236,9 +326,31 @@ function buildSyncScript( } lines.push("") + for (const skillName of legacySkillNames) { + lines.push(`compound_syncov_prune_stale skill ${shellEscape(skillName)}`) + } + if (legacySkillNames.length > 0) { + lines.push("") + } + lines.push("if declare -F _ov_rebuild_global_manifest >/dev/null 2>&1; then") lines.push(" _ov_rebuild_global_manifest") lines.push("fi") + lines.push("") + lines.push("cat >\"$OV_STATE_FILE\" <<'JSON'") + lines.push( + JSON.stringify( + { + version: 1, + agents: syncState.agents, + skills: syncState.skills, + supportUris: syncState.supportUris, + } satisfies SyncOvState, + null, + 2, + ), + ) + lines.push("JSON") return lines.join("\n") + "\n" } @@ -270,6 +382,49 @@ async function generateCommandSkills( return generated.sort((left, right) => left.name.localeCompare(right.name)) } +function buildSyncState( + plugin: PortablePlugin, + generatedCommandSkills: GeneratedCommandSkill[], + supportFiles: SkillSupportResource[], +): SyncOvState { + const agents = [...new Set(plugin.agents.map((agent) => agent.name))].sort() + const skills = [ + ...new Set([ + ...plugin.skills.map((skill) => skill.name), + ...generatedCommandSkills.map((skill) => skill.name), + ]), + ].sort() + const supportUris = [...new Set(supportFiles.map((resource) => resource.targetUri))].sort() + + return { + version: 1, + agents, + skills, + supportUris, + } +} + +function collectLegacySkillNames( + plugin: PortablePlugin, + generatedCommandSkills: GeneratedCommandSkill[], +): string[] { + const currentSkillNames = new Set([ + ...plugin.skills.map((skill) => skill.name), + ...generatedCommandSkills.map((skill) => skill.name), + ]) + const legacySkillNames = new Set() + + for (const generatedSkill of generatedCommandSkills) { + for (const legacyName of LEGACY_COMMAND_SKILL_RENAMES[generatedSkill.name] ?? []) { + if (!currentSkillNames.has(legacyName)) { + legacySkillNames.add(legacyName) + } + } + } + + return [...legacySkillNames].sort() +} + function assertNamespaceSegment(value: string, label: string): void { const trimmed = value.trim() if ( diff --git a/src/converters/claude-to-codex.ts b/src/converters/claude-to-codex.ts index b00f03b..7e1e4c2 100644 --- a/src/converters/claude-to-codex.ts +++ b/src/converters/claude-to-codex.ts @@ -19,8 +19,7 @@ export function convertClaudeToCodex( const usedSkillNames = new Set(skillDirs.map((skill) => normalizeName(skill.name))) const commandSkills: CodexGeneratedSkill[] = [] - const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation) - const prompts = invocableCommands.map((command) => { + const prompts = plugin.commands.map((command) => { const promptName = uniqueName(normalizeName(command.name), promptNames) const commandSkill = convertCommandSkill(command, usedSkillNames) commandSkills.push(commandSkill) diff --git a/src/converters/claude-to-opencode.ts b/src/converters/claude-to-opencode.ts index cf56f7b..506efe9 100644 --- a/src/converters/claude-to-opencode.ts +++ b/src/converters/claude-to-opencode.ts @@ -110,7 +110,7 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) { } } - const content = formatFrontmatter(frontmatter, transformContentForOpenCode(agent.body)) + const content = formatFrontmatter(frontmatter, transformContentForOpenCode(agent.body)) return { name: agent.name, @@ -120,10 +120,9 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) { // Commands are written as individual .md files rather than entries in opencode.json. // Chosen over JSON map because opencode resolves commands by filename at runtime (ADR-001). -function convertCommands(commands: ClaudeCommand[]): OpenCodeCommandFile[] { - const files: OpenCodeCommandFile[] = [] - for (const command of commands) { - if (command.disableModelInvocation) continue +function convertCommands(commands: ClaudeCommand[]): OpenCodeCommandFile[] { + const files: OpenCodeCommandFile[] = [] + for (const command of commands) { const frontmatter: Record = { description: command.description, } @@ -131,11 +130,11 @@ function convertCommands(commands: ClaudeCommand[]): OpenCodeCommandFile[] { if (model && model !== "inherit") { frontmatter.model = normalizeModel(model) } - const content = formatFrontmatter(frontmatter, transformContentForOpenCode(command.body)) - files.push({ name: command.name, content, sourcePath: command.sourcePath }) - } - return files -} + const content = formatFrontmatter(frontmatter, transformContentForOpenCode(command.body)) + files.push({ name: command.name, content, sourcePath: command.sourcePath }) + } + return files +} function convertMcp(servers: Record): Record { const result: Record = {} @@ -254,152 +253,152 @@ function renderHookStatements( return statements } -export function transformContentForOpenCode(body: string): string { - return transformTaskCallsForOpenCode( - body - .replace( - /use the platform's file-search tool against the bundled agent directory to look for `([ - "use `glob` with `paths` set to `.opencode/agents` and `pattern` set to `" + - agentName + - ".md`. Do not pass a full path as the `pattern`. If nothing matches, run `glob` again with `paths` set to `~/.config/opencode/agents` and the same `pattern`, then use `read` to load the full template from the matched file.", - ) - .replace( - /Only if the bundled template cannot be loaded should you fall back to `ov_load_global_agent "([ - `Only if neither file can be loaded should you fall back to \`ov_load_global_agent "${agentName}"\`.`, - ) - .replace( - /If the bundled template exists, use the file-read tool to load the full template\./g, - "If either `glob` call returns a match, use `read` to load the full file.", - ) - .replace( - /Before dispatching, quote the first non-empty line of the loaded template and record the source used\./g, - "Before dispatching, quote the first non-empty line of the loaded template and record whether it came from `.opencode/agents`, `~/.config/opencode/agents`, or OpenViking/global context.", - ) - .replace( - /If you cannot quote the template because it was not found or could not be read, stop execution, raise the missing-template issue, and do not dispatch\./g, - "If you cannot quote the template because it was not found or could not be read, stop execution, raise the missing-template issue, and do not dispatch.", - ) - .replace( - /Use the platform's file-search tool against the bundled agent directory to look for `\.md`\. Search the directory, not a full path embedded in the pattern argument\./g, - "Use `glob` with `paths` set to `.opencode/agents` and `pattern` set to `.md`. Do not pass a full path as the `pattern`. If nothing matches, run `glob` again with `paths` set to `~/.config/opencode/agents` and the same `pattern`.", - ) - .replace( - /Only if no bundled template can be loaded, fall back to OpenViking\/global context with `ov_load_global_agent ""`\./g, - "Only if neither file can be loaded should you fall back to `ov_load_global_agent \"\"`.", - ) - .replace( - /Before dispatching, quote the first non-empty line of the loaded template and record which source you used\./g, - "Before dispatching, quote the first non-empty line of the loaded template and record whether it came from `.opencode/agents`, `~/.config/opencode/agents`, or OpenViking/global context.", - ) - .replace( - /Use the platform's file-search tool against the command reference directory to look for `([a-z-]+-prompt\.md)`\. Search the directory, not a full path embedded in the pattern argument\./g, - (_match, fileName: string) => - `Use \`glob\` with \`paths\` set to \`.opencode/commands/workflows/references\` and \`pattern\` set to \`${fileName}\`. Do not pass a full path as the \`pattern\`. If nothing matches, run \`glob\` again with \`paths\` set to \`~/.config/opencode/commands/workflows/references\` and the same \`pattern\`.`, - ) - .replace( - /Use the file-read tool to load the full template\./g, - "Use `read` to load the full template.", - ) - .replace( - /Before continuing, quote the first non-empty line of the loaded template and record which file you used\./g, - "Before continuing, quote the first non-empty line of the loaded template and record whether it came from `.opencode/commands/workflows/references` or `~/.config/opencode/commands/workflows/references`.", - ) - .replace( - /Read its bundled template from `portable\/compound-engineering\/agents\/\.md` when present\./g, - "Check for a project override at `.opencode/agents/.md` first, then read the installed global template at `~/.config/opencode/agents/.md`.", - ) - .replace( - /first read `portable\/compound-engineering\/agents\/([a-z0-9-]+)\.md` when present\./g, - (_match, agentName: string) => - `first check for a project override at \`.opencode/agents/${agentName}.md\`, then read the installed global template at \`~/.config/opencode/agents/${agentName}.md\`.`, - ) - .replace( - /from `portable\/compound-engineering\/agents\/([a-z0-9-]+)\.md`/g, - (_match, agentName: string) => - `from \`.opencode/agents/${agentName}.md\` (project override) or \`~/.config/opencode/agents/${agentName}.md\` (global install)`, - ) - .replace( - /portable\/compound-engineering\/agents\/\.md/g, - ".opencode/agents/.md or ~/.config/opencode/agents/.md", - ) - .replace( - /`commands\/workflows\/references\/([a-z-]+-prompt\.md)`/g, - (_match, fileName: string) => - "`.opencode/commands/workflows/references/" + - fileName + - "` (project override) or `~/.config/opencode/commands/workflows/references/" + - fileName + - "` (global install)", - ) - .replace(/~\/\.claude\/agents\//g, "~/.config/opencode/agents/") - .replace(/~\/\.claude\/commands\//g, "~/.config/opencode/commands/") - .replace(/~\/\.claude\/skills\//g, "~/.config/opencode/skills/") - .replace(/~\/\.claude\/plugins\//g, "~/.config/opencode/plugins/") - .replace(/~\/\.claude\//g, "~/.config/opencode/") - .replace(/\.claude\/agents\//g, ".opencode/agents/") - .replace(/\.claude\/commands\//g, ".opencode/commands/") - .replace(/\.claude\/skills\//g, ".opencode/skills/") - .replace(/\.claude\/plugins\//g, ".opencode/plugins/") - .replace(/\.claude\//g, ".opencode/"), - ) -} - -function transformTaskCallsForOpenCode(body: string): string { - return body - .split("\n") - .map((line) => { - const match = line.match(/Task\s+([a-z{][a-z0-9{}_-]*)\(/) - if (!match || match.index === undefined) return line - - const agentName = match[1] - const argsStart = match.index + match[0].length - const closingParen = findClosingParen(line, argsStart - 1) - if (closingParen === -1) return line - - const args = line.slice(argsStart, closingParen).trim() - const replacement = `Use the Task tool to invoke the ${agentName} subagent with this prompt: ${args}` - return line.slice(0, match.index) + replacement + line.slice(closingParen + 1) - }) - .join("\n") -} - -function findClosingParen(line: string, openingParenIndex: number): number { - let depth = 0 - let quote: '"' | "'" | null = null - - for (let index = openingParenIndex; index < line.length; index += 1) { - const char = line[index] - const previous = index > 0 ? line[index - 1] : "" - - if (quote) { - if (char === quote && previous !== "\\") { - quote = null - } - continue - } - - if (char === '"' || char === "'") { - quote = char - continue - } - - if (char === "(") { - depth += 1 - continue - } - - if (char === ")") { - depth -= 1 - if (depth === 0) { - return index - } - } - } - - return -1 -} +export function transformContentForOpenCode(body: string): string { + return transformTaskCallsForOpenCode( + body + .replace( + /use the platform's file-search tool against the bundled agent directory to look for `([ + "use `glob` with `paths` set to `.opencode/agents` and `pattern` set to `" + + agentName + + ".md`. Do not pass a full path as the `pattern`. If nothing matches, run `glob` again with `paths` set to `~/.config/opencode/agents` and the same `pattern`, then use `read` to load the full template from the matched file.", + ) + .replace( + /Only if the bundled template cannot be loaded should you fall back to `ov_load_global_agent "([ + `Only if neither file can be loaded should you fall back to \`ov_load_global_agent "${agentName}"\`.`, + ) + .replace( + /If the bundled template exists, use the file-read tool to load the full template\./g, + "If either `glob` call returns a match, use `read` to load the full file.", + ) + .replace( + /Before dispatching, quote the first non-empty line of the loaded template and record the source used\./g, + "Before dispatching, quote the first non-empty line of the loaded template and record whether it came from `.opencode/agents`, `~/.config/opencode/agents`, or OpenViking/global context.", + ) + .replace( + /If you cannot quote the template because it was not found or could not be read, stop execution, raise the missing-template issue, and do not dispatch\./g, + "If you cannot quote the template because it was not found or could not be read, stop execution, raise the missing-template issue, and do not dispatch.", + ) + .replace( + /Use the platform's file-search tool against the bundled agent directory to look for `\.md`\. Search the directory, not a full path embedded in the pattern argument\./g, + "Use `glob` with `paths` set to `.opencode/agents` and `pattern` set to `.md`. Do not pass a full path as the `pattern`. If nothing matches, run `glob` again with `paths` set to `~/.config/opencode/agents` and the same `pattern`.", + ) + .replace( + /Only if no bundled template can be loaded, fall back to OpenViking\/global context with `ov_load_global_agent ""`\./g, + "Only if neither file can be loaded should you fall back to `ov_load_global_agent \"\"`.", + ) + .replace( + /Before dispatching, quote the first non-empty line of the loaded template and record which source you used\./g, + "Before dispatching, quote the first non-empty line of the loaded template and record whether it came from `.opencode/agents`, `~/.config/opencode/agents`, or OpenViking/global context.", + ) + .replace( + /Use the platform's file-search tool against the command reference directory to look for `([a-z-]+-prompt\.md)`\. Search the directory, not a full path embedded in the pattern argument\./g, + (_match, fileName: string) => + `Use \`glob\` with \`paths\` set to \`.opencode/commands/workflows/references\` and \`pattern\` set to \`${fileName}\`. Do not pass a full path as the \`pattern\`. If nothing matches, run \`glob\` again with \`paths\` set to \`~/.config/opencode/commands/workflows/references\` and the same \`pattern\`.`, + ) + .replace( + /Use the file-read tool to load the full template\./g, + "Use `read` to load the full template.", + ) + .replace( + /Before continuing, quote the first non-empty line of the loaded template and record which file you used\./g, + "Before continuing, quote the first non-empty line of the loaded template and record whether it came from `.opencode/commands/workflows/references` or `~/.config/opencode/commands/workflows/references`.", + ) + .replace( + /Read its bundled template from `portable\/compound-engineering\/agents\/\.md` when present\./g, + "Check for a project override at `.opencode/agents/.md` first, then read the installed global template at `~/.config/opencode/agents/.md`.", + ) + .replace( + /first read `portable\/compound-engineering\/agents\/([a-z0-9-]+)\.md` when present\./g, + (_match, agentName: string) => + `first check for a project override at \`.opencode/agents/${agentName}.md\`, then read the installed global template at \`~/.config/opencode/agents/${agentName}.md\`.`, + ) + .replace( + /from `portable\/compound-engineering\/agents\/([a-z0-9-]+)\.md`/g, + (_match, agentName: string) => + `from \`.opencode/agents/${agentName}.md\` (project override) or \`~/.config/opencode/agents/${agentName}.md\` (global install)`, + ) + .replace( + /portable\/compound-engineering\/agents\/\.md/g, + ".opencode/agents/.md or ~/.config/opencode/agents/.md", + ) + .replace( + /`commands\/workflows\/references\/([a-z-]+-prompt\.md)`/g, + (_match, fileName: string) => + "`.opencode/commands/workflows/references/" + + fileName + + "` (project override) or `~/.config/opencode/commands/workflows/references/" + + fileName + + "` (global install)", + ) + .replace(/~\/\.claude\/agents\//g, "~/.config/opencode/agents/") + .replace(/~\/\.claude\/commands\//g, "~/.config/opencode/commands/") + .replace(/~\/\.claude\/skills\//g, "~/.config/opencode/skills/") + .replace(/~\/\.claude\/plugins\//g, "~/.config/opencode/plugins/") + .replace(/~\/\.claude\//g, "~/.config/opencode/") + .replace(/\.claude\/agents\//g, ".opencode/agents/") + .replace(/\.claude\/commands\//g, ".opencode/commands/") + .replace(/\.claude\/skills\//g, ".opencode/skills/") + .replace(/\.claude\/plugins\//g, ".opencode/plugins/") + .replace(/\.claude\//g, ".opencode/"), + ) +} + +function transformTaskCallsForOpenCode(body: string): string { + return body + .split("\n") + .map((line) => { + const match = line.match(/Task\s+([a-z{][a-z0-9{}_-]*)\(/) + if (!match || match.index === undefined) return line + + const agentName = match[1] + const argsStart = match.index + match[0].length + const closingParen = findClosingParen(line, argsStart - 1) + if (closingParen === -1) return line + + const args = line.slice(argsStart, closingParen).trim() + const replacement = `Use the Task tool to invoke the ${agentName} subagent with this prompt: ${args}` + return line.slice(0, match.index) + replacement + line.slice(closingParen + 1) + }) + .join("\n") +} + +function findClosingParen(line: string, openingParenIndex: number): number { + let depth = 0 + let quote: '"' | "'" | null = null + + for (let index = openingParenIndex; index < line.length; index += 1) { + const char = line[index] + const previous = index > 0 ? line[index - 1] : "" + + if (quote) { + if (char === quote && previous !== "\\") { + quote = null + } + continue + } + + if (char === '"' || char === "'") { + quote = char + continue + } + + if (char === "(") { + depth += 1 + continue + } + + if (char === ")") { + depth -= 1 + if (depth === 0) { + return index + } + } + } + + return -1 +} // Bare Claude family aliases used in Claude Code (e.g. `model: haiku`). // Update these when new model generations are released. diff --git a/src/converters/claude-to-pi.ts b/src/converters/claude-to-pi.ts index b6ea0f3..5696cca 100644 --- a/src/converters/claude-to-pi.ts +++ b/src/converters/claude-to-pi.ts @@ -20,9 +20,7 @@ export function convertClaudeToPi( const promptNames = new Set() const usedSkillNames = new Set(plugin.skills.map((skill) => normalizeName(skill.name))) - const prompts = plugin.commands - .filter((command) => !command.disableModelInvocation) - .map((command) => convertPrompt(command, promptNames)) + const prompts = plugin.commands.map((command) => convertPrompt(command, promptNames)) const generatedSkills = plugin.agents.map((agent) => convertAgent(agent, usedSkillNames)) diff --git a/src/targets/claude.ts b/src/targets/claude.ts index 498bf9a..231e7e8 100644 --- a/src/targets/claude.ts +++ b/src/targets/claude.ts @@ -1,12 +1,17 @@ import { promises as fs } from "fs" import path from "path" -import { copyDir, ensureDir, readText, writeJson, writeText } from "../utils/files" +import { copyDir, ensureDir, pathExists, readText, writeJson, writeText } from "../utils/files" import { formatFrontmatter } from "../utils/frontmatter" import { parseFrontmatter } from "../utils/frontmatter" import type { ClaudeAgent, ClaudeCommand, ClaudeManifest, ClaudePlugin, ClaudeSkill } from "../types/claude" +import { pruneManagedOutput, writeManagedOutputState } from "../utils/managed-output" + +const STATE_FILE_NAME = ".compound-engineering-claude-state.json" export async function writeClaudeBundle(outputRoot: string, plugin: ClaudePlugin): Promise { await ensureDir(outputRoot) + const managedPaths = await collectManagedPaths(outputRoot, plugin) + const normalizedManagedPaths = await pruneManagedOutput(outputRoot, STATE_FILE_NAME, managedPaths) await writeJson(path.join(outputRoot, ".claude-plugin", "plugin.json"), buildClaudeManifest(plugin.manifest)) await fs.rm(path.join(outputRoot, "hooks"), { recursive: true, force: true }) @@ -17,7 +22,9 @@ export async function writeClaudeBundle(outputRoot: string, plugin: ClaudePlugin for (const command of plugin.commands) { const relativePath = relativeComponentPath(plugin.root, "commands", command.sourcePath, `${command.name}.md`) - await writeText(path.join(outputRoot, "commands", relativePath), formatCommand(command) + "\n") + const targetPath = path.join(outputRoot, "commands", relativePath) + await writeText(targetPath, formatCommand(command) + "\n") + await copyCommandReferenceDocs(command.sourcePath, path.dirname(targetPath)) } for (const skill of plugin.skills) { @@ -31,6 +38,8 @@ export async function writeClaudeBundle(outputRoot: string, plugin: ClaudePlugin if (plugin.hooks) { await writeJson(path.join(outputRoot, "hooks", "hooks.json"), plugin.hooks) } + + await writeManagedOutputState(outputRoot, STATE_FILE_NAME, normalizedManagedPaths) } function buildClaudeManifest(manifest: ClaudeManifest): ClaudeManifest { @@ -108,3 +117,39 @@ function relativeComponentDir(root: string, componentDir: string, sourceDir: str } return fallback } + +async function copyCommandReferenceDocs(commandSourcePath: string, targetDir: string): Promise { + const sourceReferencesDir = path.join(path.dirname(commandSourcePath), "references") + if (!(await pathExists(sourceReferencesDir))) return + await copyDir(sourceReferencesDir, path.join(targetDir, "references")) +} + +async function collectManagedPaths(outputRoot: string, plugin: ClaudePlugin): Promise { + const managed = new Set() + managed.add(path.join(outputRoot, ".claude-plugin", "plugin.json")) + + for (const agent of plugin.agents) { + const relativePath = relativeComponentPath(plugin.root, "agents", agent.sourcePath, `${agent.name}.md`) + managed.add(path.join(outputRoot, "agents", relativePath)) + } + + for (const command of plugin.commands) { + const relativePath = relativeComponentPath(plugin.root, "commands", command.sourcePath, `${command.name}.md`) + managed.add(path.join(outputRoot, "commands", relativePath)) + const sourceReferencesDir = path.join(path.dirname(command.sourcePath), "references") + if (await pathExists(sourceReferencesDir)) { + managed.add(path.join(outputRoot, "commands", path.dirname(relativePath), "references")) + } + } + + for (const skill of plugin.skills) { + const relativeDir = relativeComponentDir(plugin.root, "skills", skill.sourceDir, skill.name) + managed.add(path.join(outputRoot, "skills", relativeDir)) + } + + if (plugin.hooks) { + managed.add(path.join(outputRoot, "hooks", "hooks.json")) + } + + return [...managed] +} diff --git a/src/targets/codex.ts b/src/targets/codex.ts index 4d0430c..3ac0cff 100644 --- a/src/targets/codex.ts +++ b/src/targets/codex.ts @@ -1,11 +1,21 @@ import path from "path" -import { backupFile, copyDir, ensureDir, writeText } from "../utils/files" +import { copyDir, ensureDir, writeText } from "../utils/files" import type { CodexBundle } from "../types/codex" import type { ClaudeMcpServer } from "../types/claude" +import { + pruneManagedOutput, + removeLegacyBackupArtifacts, + writeManagedOutputState, +} from "../utils/managed-output" + +const STATE_FILE_NAME = ".compound-engineering-codex-state.json" export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): Promise { const codexRoot = resolveCodexRoot(outputRoot) await ensureDir(codexRoot) + const managedPaths = collectManagedPaths(codexRoot, bundle) + const normalizedManagedPaths = await pruneManagedOutput(codexRoot, STATE_FILE_NAME, managedPaths) + await removeLegacyBackupArtifacts(codexRoot, [/^config\.toml\.bak\./]) if (bundle.prompts.length > 0) { const promptsDir = path.join(codexRoot, "prompts") @@ -31,18 +41,33 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): const config = renderCodexConfig(bundle.mcpServers) if (config) { const configPath = path.join(codexRoot, "config.toml") - const backupPath = await backupFile(configPath) - if (backupPath) { - console.log(`Backed up existing config to ${backupPath}`) - } await writeText(configPath, config) } + + await writeManagedOutputState(codexRoot, STATE_FILE_NAME, normalizedManagedPaths) } function resolveCodexRoot(outputRoot: string): string { return path.basename(outputRoot) === ".codex" ? outputRoot : path.join(outputRoot, ".codex") } +function collectManagedPaths(codexRoot: string, bundle: CodexBundle): string[] { + const managed = new Set() + for (const prompt of bundle.prompts) { + managed.add(path.join(codexRoot, "prompts", `${prompt.name}.md`)) + } + for (const skill of bundle.skillDirs) { + managed.add(path.join(codexRoot, "skills", skill.name)) + } + for (const skill of bundle.generatedSkills) { + managed.add(path.join(codexRoot, "skills", skill.name)) + } + if (bundle.mcpServers && Object.keys(bundle.mcpServers).length > 0) { + managed.add(path.join(codexRoot, "config.toml")) + } + return [...managed] +} + export function renderCodexConfig(mcpServers?: Record): string | null { if (!mcpServers || Object.keys(mcpServers).length === 0) return null diff --git a/src/targets/copilot.ts b/src/targets/copilot.ts index 55f40bd..f5d0da0 100644 --- a/src/targets/copilot.ts +++ b/src/targets/copilot.ts @@ -1,23 +1,24 @@ import path from "path" -import { - backupFile, - copyDir, - ensureDir, - pathExists, - readText, - walkFiles, - writeJson, - writeText, -} from "../utils/files" +import { copyDir, ensureDir, pathExists, readText, walkFiles, writeJson, writeText } from "../utils/files" import { transformContentForCopilot } from "../converters/claude-to-copilot" import type { CopilotBundle } from "../types/copilot" import { formatFrontmatter, parseFrontmatter } from "../utils/frontmatter" import { assertSafeOutputName } from "../utils/path-safety" +import { + pruneManagedOutput, + removeLegacyBackupArtifacts, + writeManagedOutputState, +} from "../utils/managed-output" + +const STATE_FILE_NAME = ".compound-engineering-copilot-state.json" export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBundle): Promise { const paths = resolveCopilotPaths(outputRoot) await ensureDir(paths.githubDir) - + const managedPaths = collectManagedPaths(paths.githubDir, bundle) + const normalizedManagedPaths = await pruneManagedOutput(paths.githubDir, STATE_FILE_NAME, managedPaths) + await removeLegacyBackupArtifacts(paths.githubDir, [/^copilot-mcp-config\.json\.bak\./]) + if (bundle.agents.length > 0) { const agentsDir = path.join(paths.githubDir, "agents") for (const agent of bundle.agents) { @@ -25,7 +26,7 @@ export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBund await writeText(path.join(agentsDir, `${agent.name}.agent.md`), agent.content + "\n") } } - + if (bundle.generatedSkills.length > 0) { const skillsDir = path.join(paths.githubDir, "skills") for (const skill of bundle.generatedSkills) { @@ -37,7 +38,7 @@ export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBund } } } - + if (bundle.skillDirs.length > 0) { const skillsDir = path.join(paths.githubDir, "skills") for (const skill of bundle.skillDirs) { @@ -59,15 +60,13 @@ export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBund await writeText(path.join(targetDir, "SKILL.md"), content + "\n") } } - - if (bundle.mcpConfig && Object.keys(bundle.mcpConfig).length > 0) { - const mcpPath = path.join(paths.githubDir, "copilot-mcp-config.json") - const backupPath = await backupFile(mcpPath) - if (backupPath) { - console.log(`Backed up existing copilot-mcp-config.json to ${backupPath}`) - } - await writeJson(mcpPath, { mcpServers: bundle.mcpConfig }) - } + + if (bundle.mcpConfig && Object.keys(bundle.mcpConfig).length > 0) { + const mcpPath = path.join(paths.githubDir, "copilot-mcp-config.json") + await writeJson(mcpPath, { mcpServers: bundle.mcpConfig }) + } + + await writeManagedOutputState(paths.githubDir, STATE_FILE_NAME, normalizedManagedPaths) } async function copyCommandReferenceDocs(commandSourcePath: string, targetDir: string): Promise { @@ -94,11 +93,28 @@ async function transformCopiedMarkdownForCopilot(targetDir: string): Promise() + for (const agent of bundle.agents) { + managed.add(path.join(githubDir, "agents", `${agent.name}.agent.md`)) + } + for (const skill of bundle.generatedSkills) { + managed.add(path.join(githubDir, "skills", skill.name)) + } + for (const skill of bundle.skillDirs) { + managed.add(path.join(githubDir, "skills", skill.name)) + } + if (bundle.mcpConfig && Object.keys(bundle.mcpConfig).length > 0) { + managed.add(path.join(githubDir, "copilot-mcp-config.json")) + } + return [...managed] +} diff --git a/src/targets/droid.ts b/src/targets/droid.ts index 68159cb..c84db3f 100644 --- a/src/targets/droid.ts +++ b/src/targets/droid.ts @@ -1,10 +1,15 @@ import path from "path" import { copyDir, ensureDir, writeText } from "../utils/files" import type { DroidBundle } from "../types/droid" +import { pruneManagedOutput, writeManagedOutputState } from "../utils/managed-output" + +const STATE_FILE_NAME = ".compound-engineering-droid-state.json" export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle): Promise { const paths = resolveDroidPaths(outputRoot) await ensureDir(paths.root) + const managedPaths = collectManagedPaths(paths, bundle) + const normalizedManagedPaths = await pruneManagedOutput(paths.root, STATE_FILE_NAME, managedPaths) if (bundle.commands.length > 0) { await ensureDir(paths.commandsDir) @@ -26,6 +31,8 @@ export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle): await copyDir(skill.sourceDir, path.join(paths.skillsDir, skill.name)) } } + + await writeManagedOutputState(paths.root, STATE_FILE_NAME, normalizedManagedPaths) } function resolveDroidPaths(outputRoot: string) { @@ -48,3 +55,20 @@ function resolveDroidPaths(outputRoot: string) { skillsDir: path.join(outputRoot, ".factory", "skills"), } } + +function collectManagedPaths( + paths: ReturnType, + bundle: DroidBundle, +): string[] { + const managed = new Set() + for (const command of bundle.commands) { + managed.add(path.join(paths.commandsDir, `${command.name}.md`)) + } + for (const droid of bundle.droids) { + managed.add(path.join(paths.droidsDir, `${droid.name}.md`)) + } + for (const skill of bundle.skillDirs) { + managed.add(path.join(paths.skillsDir, skill.name)) + } + return [...managed] +} diff --git a/src/targets/gemini.ts b/src/targets/gemini.ts index 20a898a..c90d885 100644 --- a/src/targets/gemini.ts +++ b/src/targets/gemini.ts @@ -1,10 +1,20 @@ import path from "path" -import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files" +import { copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files" import type { GeminiBundle } from "../types/gemini" +import { + pruneManagedOutput, + removeLegacyBackupArtifacts, + writeManagedOutputState, +} from "../utils/managed-output" + +const STATE_FILE_NAME = ".compound-engineering-gemini-state.json" export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle): Promise { const paths = resolveGeminiPaths(outputRoot) await ensureDir(paths.geminiDir) + const managedPaths = collectManagedPaths(paths, bundle) + const normalizedManagedPaths = await pruneManagedOutput(paths.geminiDir, STATE_FILE_NAME, managedPaths) + await removeLegacyBackupArtifacts(paths.geminiDir, [/^settings\.json\.bak\./]) if (bundle.generatedSkills.length > 0) { for (const skill of bundle.generatedSkills) { @@ -26,10 +36,6 @@ export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle if (bundle.mcpServers && Object.keys(bundle.mcpServers).length > 0) { const settingsPath = path.join(paths.geminiDir, "settings.json") - const backupPath = await backupFile(settingsPath) - if (backupPath) { - console.log(`Backed up existing settings.json to ${backupPath}`) - } // Merge mcpServers into existing settings if present let existingSettings: Record = {} @@ -47,6 +53,7 @@ export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle const merged = { ...existingSettings, mcpServers: { ...existingMcp, ...bundle.mcpServers } } await writeJson(settingsPath, merged) } + await writeManagedOutputState(paths.geminiDir, STATE_FILE_NAME, normalizedManagedPaths) } function resolveGeminiPaths(outputRoot: string) { @@ -66,3 +73,23 @@ function resolveGeminiPaths(outputRoot: string) { commandsDir: path.join(outputRoot, ".gemini", "commands"), } } + +function collectManagedPaths( + paths: ReturnType, + bundle: GeminiBundle, +): string[] { + const managed = new Set() + for (const skill of bundle.generatedSkills) { + managed.add(path.join(paths.skillsDir, skill.name)) + } + for (const skill of bundle.skillDirs) { + managed.add(path.join(paths.skillsDir, skill.name)) + } + for (const command of bundle.commands) { + managed.add(path.join(paths.commandsDir, `${command.name}.toml`)) + } + if (bundle.mcpServers && Object.keys(bundle.mcpServers).length > 0) { + managed.add(path.join(paths.geminiDir, "settings.json")) + } + return [...managed] +} diff --git a/src/targets/kiro.ts b/src/targets/kiro.ts index 2e9eea5..48d20c0 100644 --- a/src/targets/kiro.ts +++ b/src/targets/kiro.ts @@ -1,117 +1,149 @@ import path from "path" -import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files" +import { copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files" import type { KiroBundle } from "../types/kiro" import { assertSafeOutputName } from "../utils/path-safety" - -export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): Promise { - const paths = resolveKiroPaths(outputRoot) - await ensureDir(paths.kiroDir) - - // Write agents - if (bundle.agents.length > 0) { - for (const agent of bundle.agents) { +import { + pruneManagedOutput, + removeLegacyBackupArtifacts, + writeManagedOutputState, +} from "../utils/managed-output" + +const STATE_FILE_NAME = ".compound-engineering-kiro-state.json" + +export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): Promise { + const paths = resolveKiroPaths(outputRoot) + await ensureDir(paths.kiroDir) + const managedPaths = collectManagedPaths(paths, bundle) + const normalizedManagedPaths = await pruneManagedOutput(paths.kiroDir, STATE_FILE_NAME, managedPaths) + await removeLegacyBackupArtifacts(paths.kiroDir, [/^mcp\.json\.bak\./]) + + // Write agents + if (bundle.agents.length > 0) { + for (const agent of bundle.agents) { // Validate name doesn't escape agents directory assertSafeOutputName(agent.name, "agent") - - // Write agent JSON config - await writeJson( - path.join(paths.agentsDir, `${agent.name}.json`), - agent.config, - ) - - // Write agent prompt file - await writeText( - path.join(paths.agentsDir, "prompts", `${agent.name}.md`), - agent.promptContent + "\n", - ) - } - } - - // Write generated skills (from commands) - if (bundle.generatedSkills.length > 0) { + + // Write agent JSON config + await writeJson( + path.join(paths.agentsDir, `${agent.name}.json`), + agent.config, + ) + + // Write agent prompt file + await writeText( + path.join(paths.agentsDir, "prompts", `${agent.name}.md`), + agent.promptContent + "\n", + ) + } + } + + // Write generated skills (from commands) + if (bundle.generatedSkills.length > 0) { for (const skill of bundle.generatedSkills) { assertSafeOutputName(skill.name, "skill") await writeText( path.join(paths.skillsDir, skill.name, "SKILL.md"), skill.content + "\n", - ) - } - } - - // Copy skill directories (pass-through) + ) + } + } + + // Copy skill directories (pass-through) if (bundle.skillDirs.length > 0) { for (const skill of bundle.skillDirs) { assertSafeOutputName(skill.name, "skill directory") const destDir = path.join(paths.skillsDir, skill.name) - - // Validate destination doesn't escape skills directory - const resolvedDest = path.resolve(destDir) - if (!resolvedDest.startsWith(path.resolve(paths.skillsDir))) { - console.warn(`Warning: Skill name "${skill.name}" escapes .kiro/skills/. Skipping.`) - continue - } - - await copyDir(skill.sourceDir, destDir) - } - } - - // Write steering files + + // Validate destination doesn't escape skills directory + const resolvedDest = path.resolve(destDir) + if (!resolvedDest.startsWith(path.resolve(paths.skillsDir))) { + console.warn(`Warning: Skill name "${skill.name}" escapes .kiro/skills/. Skipping.`) + continue + } + + await copyDir(skill.sourceDir, destDir) + } + } + + // Write steering files if (bundle.steeringFiles.length > 0) { for (const file of bundle.steeringFiles) { assertSafeOutputName(file.name, "steering file") await writeText( path.join(paths.steeringDir, `${file.name}.md`), file.content + "\n", - ) - } - } - - // Write MCP servers to mcp.json - if (Object.keys(bundle.mcpServers).length > 0) { - const mcpPath = path.join(paths.settingsDir, "mcp.json") - const backupPath = await backupFile(mcpPath) - if (backupPath) { - console.log(`Backed up existing mcp.json to ${backupPath}`) - } - - // Merge with existing mcp.json if present - let existingConfig: Record = {} - if (await pathExists(mcpPath)) { - try { - existingConfig = await readJson>(mcpPath) - } catch { - console.warn("Warning: existing mcp.json could not be parsed and will be replaced.") - } - } - - const existingServers = - existingConfig.mcpServers && typeof existingConfig.mcpServers === "object" - ? (existingConfig.mcpServers as Record) - : {} - const merged = { ...existingConfig, mcpServers: { ...existingServers, ...bundle.mcpServers } } - await writeJson(mcpPath, merged) - } -} - -function resolveKiroPaths(outputRoot: string) { - const base = path.basename(outputRoot) - // If already pointing at .kiro, write directly into it - if (base === ".kiro") { - return { - kiroDir: outputRoot, - agentsDir: path.join(outputRoot, "agents"), - skillsDir: path.join(outputRoot, "skills"), - steeringDir: path.join(outputRoot, "steering"), - settingsDir: path.join(outputRoot, "settings"), - } - } - // Otherwise nest under .kiro - const kiroDir = path.join(outputRoot, ".kiro") - return { - kiroDir, - agentsDir: path.join(kiroDir, "agents"), - skillsDir: path.join(kiroDir, "skills"), - steeringDir: path.join(kiroDir, "steering"), - settingsDir: path.join(kiroDir, "settings"), - } + ) + } + } + + // Write MCP servers to mcp.json + if (Object.keys(bundle.mcpServers).length > 0) { + const mcpPath = path.join(paths.settingsDir, "mcp.json") + + // Merge with existing mcp.json if present + let existingConfig: Record = {} + if (await pathExists(mcpPath)) { + try { + existingConfig = await readJson>(mcpPath) + } catch { + console.warn("Warning: existing mcp.json could not be parsed and will be replaced.") + } + } + + const existingServers = + existingConfig.mcpServers && typeof existingConfig.mcpServers === "object" + ? (existingConfig.mcpServers as Record) + : {} + const merged = { ...existingConfig, mcpServers: { ...existingServers, ...bundle.mcpServers } } + await writeJson(mcpPath, merged) + } + + await writeManagedOutputState(paths.kiroDir, STATE_FILE_NAME, normalizedManagedPaths) +} + +function resolveKiroPaths(outputRoot: string) { + const base = path.basename(outputRoot) + // If already pointing at .kiro, write directly into it + if (base === ".kiro") { + return { + kiroDir: outputRoot, + agentsDir: path.join(outputRoot, "agents"), + skillsDir: path.join(outputRoot, "skills"), + steeringDir: path.join(outputRoot, "steering"), + settingsDir: path.join(outputRoot, "settings"), + } + + function collectManagedPaths( + paths: ReturnType, + bundle: KiroBundle, + ): string[] { + const managed = new Set() + for (const agent of bundle.agents) { + managed.add(path.join(paths.agentsDir, `${agent.name}.json`)) + managed.add(path.join(paths.agentsDir, "prompts", `${agent.name}.md`)) + } + for (const skill of bundle.generatedSkills) { + managed.add(path.join(paths.skillsDir, skill.name)) + } + for (const skill of bundle.skillDirs) { + managed.add(path.join(paths.skillsDir, skill.name)) + } + for (const file of bundle.steeringFiles) { + managed.add(path.join(paths.steeringDir, `${file.name}.md`)) + } + if (Object.keys(bundle.mcpServers).length > 0) { + managed.add(path.join(paths.settingsDir, "mcp.json")) + } + return [...managed] + } + } + // Otherwise nest under .kiro + const kiroDir = path.join(outputRoot, ".kiro") + return { + kiroDir, + agentsDir: path.join(kiroDir, "agents"), + skillsDir: path.join(kiroDir, "skills"), + steeringDir: path.join(kiroDir, "steering"), + settingsDir: path.join(kiroDir, "settings"), + } } diff --git a/src/targets/opencode.ts b/src/targets/opencode.ts index 1d88cda..08d63a8 100644 --- a/src/targets/opencode.ts +++ b/src/targets/opencode.ts @@ -1,117 +1,223 @@ +import { promises as fs } from "fs" import path from "path" -import { backupFile, copyDir, ensureDir, pathExists, readJson, readText, writeJson, writeText } from "../utils/files" +import { copyDir, ensureDir, pathExists, readJson, readText, writeJson, writeText } from "../utils/files" import { transformContentForOpenCode } from "../converters/claude-to-opencode" import { formatFrontmatter, parseFrontmatter } from "../utils/frontmatter" import type { OpenCodeBundle, OpenCodeConfig } from "../types/opencode" - -// Merges plugin config into existing opencode.json. User keys win on conflict. See ADR-002. -async function mergeOpenCodeConfig( - configPath: string, - incoming: OpenCodeConfig, -): Promise { - // If no existing config, write plugin config as-is - if (!(await pathExists(configPath))) return incoming - - let existing: OpenCodeConfig - try { - existing = await readJson(configPath) - } catch { - // Safety first per AGENTS.md -- do not destroy user data even if their config is malformed. - // Warn and fall back to plugin-only config rather than crashing. - console.warn( - `Warning: existing ${configPath} is not valid JSON. Writing plugin config without merging.` - ) - return incoming - } - - // User config wins on conflict -- see ADR-002 - // MCP servers: add plugin entry, skip keys already in user config. - const mergedMcp = { - ...(incoming.mcp ?? {}), - ...(existing.mcp ?? {}), // existing takes precedence (overwrites same-named plugin entry) - } - - // Permission: add plugin entry, skip keys already in user config. - const mergedPermission = incoming.permission - ? { - ...(incoming.permission), - ...(existing.permission ?? {}), // existing takes precedence - } - : existing.permission - - // Tools: same pattern - const mergedTools = incoming.tools - ? { - ...(incoming.tools), - ...(existing.tools ?? {}), - } - : existing.tools - - return { - ...existing, // all user keys preserved - $schema: incoming.$schema ?? existing.$schema, - mcp: Object.keys(mergedMcp).length > 0 ? mergedMcp : undefined, - permission: mergedPermission, - tools: mergedTools, - } -} - -export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise { - const openCodePaths = resolveOpenCodePaths(outputRoot) - await ensureDir(openCodePaths.root) - - const backupPath = await backupFile(openCodePaths.configPath) - if (backupPath) { - console.log(`Backed up existing config to ${backupPath}`) - } - const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config) - await writeJson(openCodePaths.configPath, merged) - - const agentsDir = openCodePaths.agentsDir - for (const agent of bundle.agents) { - await writeText(path.join(agentsDir, `${agent.name}.md`), agent.content + "\n") - } - + +type OpenCodeInstallState = { + version: 1 + generatedPaths: string[] +} + +const STATE_FILE_NAME = ".compound-engineering-opencode-state.json" + +// Merges plugin config into existing opencode.json. User keys win on conflict. See ADR-002. +async function mergeOpenCodeConfig( + configPath: string, + incoming: OpenCodeConfig, +): Promise { + // If no existing config, write plugin config as-is + if (!(await pathExists(configPath))) return incoming + + let existing: OpenCodeConfig + try { + existing = await readJson(configPath) + } catch { + // Safety first per AGENTS.md -- do not destroy user data even if their config is malformed. + // Warn and fall back to plugin-only config rather than crashing. + console.warn( + `Warning: existing ${configPath} is not valid JSON. Writing plugin config without merging.` + ) + return incoming + } + + // User config wins on conflict -- see ADR-002 + // MCP servers: add plugin entry, skip keys already in user config. + const mergedMcp = { + ...(incoming.mcp ?? {}), + ...(existing.mcp ?? {}), // existing takes precedence (overwrites same-named plugin entry) + } + + // Permission: add plugin entry, skip keys already in user config. + const mergedPermission = incoming.permission + ? { + ...(incoming.permission), + ...(existing.permission ?? {}), // existing takes precedence + } + : existing.permission + + // Tools: same pattern + const mergedTools = incoming.tools + ? { + ...(incoming.tools), + ...(existing.tools ?? {}), + } + : existing.tools + + return { + ...existing, // all user keys preserved + $schema: incoming.$schema ?? existing.$schema, + mcp: Object.keys(mergedMcp).length > 0 ? mergedMcp : undefined, + permission: mergedPermission, + tools: mergedTools, + } +} + +export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise { + const openCodePaths = resolveOpenCodePaths(outputRoot) + await ensureDir(openCodePaths.root) + const desiredPaths = await collectDesiredManagedPaths(openCodePaths, bundle) + await pruneStaleGeneratedArtifacts(openCodePaths.root, desiredPaths) + await pruneLegacyRenamedArtifacts(openCodePaths, bundle) + await removeLegacyBackupFiles(openCodePaths) + + const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config) + await writeJson(openCodePaths.configPath, merged) + + const agentsDir = openCodePaths.agentsDir + for (const agent of bundle.agents) { + await writeText(path.join(agentsDir, `${agent.name}.md`), agent.content + "\n") + } + for (const commandFile of bundle.commandFiles ?? []) { const dest = path.join(openCodePaths.commandDir, `${commandFile.name}.md`) - const cmdBackupPath = await backupFile(dest) - if (cmdBackupPath) { - console.log(`Backed up existing command file to ${cmdBackupPath}`) - } await writeText(dest, commandFile.content + "\n") if (commandFile.sourcePath) { await copyCommandReferenceDocs(commandFile.sourcePath, openCodePaths.commandDir) } } - - if (bundle.plugins.length > 0) { - const pluginsDir = openCodePaths.pluginsDir - for (const plugin of bundle.plugins) { - await writeText(path.join(pluginsDir, plugin.name), plugin.content + "\n") - } - } - - if (bundle.skillDirs.length > 0) { - const skillsRoot = openCodePaths.skillsDir - for (const skill of bundle.skillDirs) { - const targetDir = path.join(skillsRoot, skill.name) - await copyDir(skill.sourceDir, targetDir) - - // Rewrite SKILL.md with platform-specific model - const raw = await readText(skill.skillPath) - const parsed = parseFrontmatter(raw) - const fm: Record = { - name: skill.name, - description: skill.description, - } - if (skill.model) { - fm.model = skill.model - } + + if (bundle.plugins.length > 0) { + const pluginsDir = openCodePaths.pluginsDir + for (const plugin of bundle.plugins) { + await writeText(path.join(pluginsDir, plugin.name), plugin.content + "\n") + } + } + + if (bundle.skillDirs.length > 0) { + const skillsRoot = openCodePaths.skillsDir + for (const skill of bundle.skillDirs) { + const targetDir = path.join(skillsRoot, skill.name) + await copyDir(skill.sourceDir, targetDir) + + // Rewrite SKILL.md with platform-specific model + const raw = await readText(skill.skillPath) + const parsed = parseFrontmatter(raw) + const fm: Record = { + name: skill.name, + description: skill.description, + } + if (skill.model) { + fm.model = skill.model + } const content = formatFrontmatter(fm, transformContentForOpenCode(parsed.body.trim())) await writeText(path.join(targetDir, "SKILL.md"), content + "\n") } } + + await writeInstallState(openCodePaths.root, desiredPaths) +} + +async function collectDesiredManagedPaths( + openCodePaths: ReturnType, + bundle: OpenCodeBundle, +): Promise { + const paths = new Set() + + for (const agent of bundle.agents) { + paths.add(path.join(openCodePaths.agentsDir, `${agent.name}.md`)) + } + for (const commandFile of bundle.commandFiles ?? []) { + paths.add(path.join(openCodePaths.commandDir, `${commandFile.name}.md`)) + if (commandFile.sourcePath) { + const sourceReferencesDir = path.join(path.dirname(commandFile.sourcePath), "references") + if (await pathExists(sourceReferencesDir)) { + const relativeDir = resolveCommandRelativeDir(commandFile.sourcePath) + paths.add(path.join(openCodePaths.commandDir, relativeDir, "references")) + } + } + } + for (const plugin of bundle.plugins) { + paths.add(path.join(openCodePaths.pluginsDir, plugin.name)) + } + for (const skill of bundle.skillDirs) { + paths.add(path.join(openCodePaths.skillsDir, skill.name)) + } + + return [...paths] +} + +async function pruneStaleGeneratedArtifacts(rootDir: string, desiredPaths: string[]): Promise { + const state = await readInstallState(rootDir) + if (!state) return + + const desired = new Set(desiredPaths.map((filePath) => path.resolve(filePath))) + const stale = state.generatedPaths + .map((filePath) => path.resolve(filePath)) + .filter((filePath) => !desired.has(filePath)) + .sort((a, b) => b.length - a.length) + + for (const stalePath of stale) { + await fs.rm(stalePath, { recursive: true, force: true }) + } +} + +async function removeLegacyBackupFiles(openCodePaths: ReturnType): Promise { + await removeMatchingFiles(openCodePaths.root, (name) => /^opencode\.json\.bak\./.test(name)) + await removeMatchingFiles(openCodePaths.commandDir, (name) => /\.md\.bak\./.test(name)) +} + +async function pruneLegacyRenamedArtifacts( + openCodePaths: ReturnType, + bundle: OpenCodeBundle, +): Promise { + const commandNames = new Set((bundle.commandFiles ?? []).map((commandFile) => commandFile.name)) + + if (commandNames.has("workflows:triage")) { + await fs.rm(path.join(openCodePaths.commandDir, "triage.md"), { force: true }) + await fs.rm(path.join(openCodePaths.skillsDir, "triage"), { recursive: true, force: true }) + } +} + +async function removeMatchingFiles( + rootDir: string, + matcher: (name: string) => boolean, +): Promise { + if (!(await pathExists(rootDir))) return + const entries = await fs.readdir(rootDir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(rootDir, entry.name) + if (entry.isDirectory()) { + await removeMatchingFiles(fullPath, matcher) + continue + } + if (entry.isFile() && matcher(entry.name)) { + await fs.rm(fullPath, { force: true }) + } + } +} + +async function readInstallState(rootDir: string): Promise { + const statePath = path.join(rootDir, STATE_FILE_NAME) + if (!(await pathExists(statePath))) return null + try { + const state = await readJson(statePath) + if (state.version !== 1 || !Array.isArray(state.generatedPaths)) { + return null + } + return state + } catch { + return null + } } + +async function writeInstallState(rootDir: string, desiredPaths: string[]): Promise { + const statePath = path.join(rootDir, STATE_FILE_NAME) + const normalized = [...new Set(desiredPaths.map((filePath) => path.resolve(filePath)))].sort() + await writeJson(statePath, { version: 1, generatedPaths: normalized } satisfies OpenCodeInstallState) +} + async function copyCommandReferenceDocs(commandSourcePath: string, commandDir: string): Promise { const sourceReferencesDir = path.join(path.dirname(commandSourcePath), "references") if (!(await pathExists(sourceReferencesDir))) return @@ -130,31 +236,31 @@ function resolveCommandRelativeDir(commandSourcePath: string): string { const relativeDir = path.dirname(relativePath) return relativeDir === "." ? "" : relativeDir } - -function resolveOpenCodePaths(outputRoot: string) { - const base = path.basename(outputRoot) - // Global install: ~/.config/opencode (basename is "opencode") - // Project install: .opencode (basename is ".opencode") - if (base === "opencode" || base === ".opencode") { - return { - root: outputRoot, - configPath: path.join(outputRoot, "opencode.json"), - agentsDir: path.join(outputRoot, "agents"), - pluginsDir: path.join(outputRoot, "plugins"), - skillsDir: path.join(outputRoot, "skills"), - // .md command files; alternative to the command key in opencode.json - commandDir: path.join(outputRoot, "commands"), - } - } - - // Custom output directory - nest under .opencode subdirectory - return { - root: outputRoot, - configPath: path.join(outputRoot, "opencode.json"), - agentsDir: path.join(outputRoot, ".opencode", "agents"), - pluginsDir: path.join(outputRoot, ".opencode", "plugins"), - skillsDir: path.join(outputRoot, ".opencode", "skills"), - // .md command files; alternative to the command key in opencode.json - commandDir: path.join(outputRoot, ".opencode", "commands"), - } + +function resolveOpenCodePaths(outputRoot: string) { + const base = path.basename(outputRoot) + // Global install: ~/.config/opencode (basename is "opencode") + // Project install: .opencode (basename is ".opencode") + if (base === "opencode" || base === ".opencode") { + return { + root: outputRoot, + configPath: path.join(outputRoot, "opencode.json"), + agentsDir: path.join(outputRoot, "agents"), + pluginsDir: path.join(outputRoot, "plugins"), + skillsDir: path.join(outputRoot, "skills"), + // .md command files; alternative to the command key in opencode.json + commandDir: path.join(outputRoot, "commands"), + } + } + + // Custom output directory - nest under .opencode subdirectory + return { + root: outputRoot, + configPath: path.join(outputRoot, "opencode.json"), + agentsDir: path.join(outputRoot, ".opencode", "agents"), + pluginsDir: path.join(outputRoot, ".opencode", "plugins"), + skillsDir: path.join(outputRoot, ".opencode", "skills"), + // .md command files; alternative to the command key in opencode.json + commandDir: path.join(outputRoot, ".opencode", "commands"), + } } diff --git a/src/targets/pi.ts b/src/targets/pi.ts index 436b3d8..0eb1aac 100644 --- a/src/targets/pi.ts +++ b/src/targets/pi.ts @@ -1,6 +1,5 @@ import path from "path" import { - backupFile, copyDir, ensureDir, pathExists, @@ -9,6 +8,11 @@ import { writeText, } from "../utils/files" import type { PiBundle } from "../types/pi" +import { + pruneManagedOutput, + removeLegacyBackupArtifacts, + writeManagedOutputState, +} from "../utils/managed-output" const PI_AGENTS_BLOCK_START = "" const PI_AGENTS_BLOCK_END = "" @@ -24,9 +28,13 @@ Compatibility notes: - MCP access uses MCPorter via mcporter_list and mcporter_call extension tools - MCPorter config path: .pi/compound-engineering/mcporter.json (project) or ~/.pi/agent/compound-engineering/mcporter.json (global) ` +const STATE_FILE_NAME = ".compound-engineering-pi-state.json" export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promise { const paths = resolvePiPaths(outputRoot) + const managedPaths = collectManagedPaths(paths, bundle) + const normalizedManagedPaths = await pruneManagedOutput(paths.root, STATE_FILE_NAME, managedPaths) + await removeLegacyBackupArtifacts(paths.root, [/^mcporter\.json\.bak\./]) await ensureDir(paths.skillsDir) await ensureDir(paths.promptsDir) @@ -49,14 +57,11 @@ export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promi } if (bundle.mcporterConfig) { - const backupPath = await backupFile(paths.mcporterConfigPath) - if (backupPath) { - console.log(`Backed up existing MCPorter config to ${backupPath}`) - } await writeJson(paths.mcporterConfigPath, bundle.mcporterConfig) } await ensurePiAgentsBlock(paths.agentsPath) + await writeManagedOutputState(paths.root, STATE_FILE_NAME, normalizedManagedPaths) } function resolvePiPaths(outputRoot: string) { @@ -65,6 +70,7 @@ function resolvePiPaths(outputRoot: string) { // Global install root: ~/.pi/agent if (base === "agent") { return { + root: outputRoot, skillsDir: path.join(outputRoot, "skills"), promptsDir: path.join(outputRoot, "prompts"), extensionsDir: path.join(outputRoot, "extensions"), @@ -76,6 +82,7 @@ function resolvePiPaths(outputRoot: string) { // Project local .pi directory if (base === ".pi") { return { + root: outputRoot, skillsDir: path.join(outputRoot, "skills"), promptsDir: path.join(outputRoot, "prompts"), extensionsDir: path.join(outputRoot, "extensions"), @@ -86,6 +93,7 @@ function resolvePiPaths(outputRoot: string) { // Custom output root -> nest under .pi return { + root: path.join(outputRoot, ".pi"), skillsDir: path.join(outputRoot, ".pi", "skills"), promptsDir: path.join(outputRoot, ".pi", "prompts"), extensionsDir: path.join(outputRoot, ".pi", "extensions"), @@ -94,6 +102,26 @@ function resolvePiPaths(outputRoot: string) { } } +function collectManagedPaths(paths: ReturnType, bundle: PiBundle): string[] { + const managed = new Set() + for (const prompt of bundle.prompts) { + managed.add(path.join(paths.promptsDir, `${prompt.name}.md`)) + } + for (const skill of bundle.skillDirs) { + managed.add(path.join(paths.skillsDir, skill.name)) + } + for (const skill of bundle.generatedSkills) { + managed.add(path.join(paths.skillsDir, skill.name)) + } + for (const extension of bundle.extensions) { + managed.add(path.join(paths.extensionsDir, extension.name)) + } + if (bundle.mcporterConfig) { + managed.add(paths.mcporterConfigPath) + } + return [...managed] +} + async function ensurePiAgentsBlock(filePath: string): Promise { const block = buildPiAgentsBlock() diff --git a/src/utils/managed-output.ts b/src/utils/managed-output.ts new file mode 100644 index 0000000..5183030 --- /dev/null +++ b/src/utils/managed-output.ts @@ -0,0 +1,81 @@ +import { promises as fs } from "fs" +import path from "path" +import { pathExists, readJson, writeJson } from "./files" + +type ManagedOutputState = { + version: 1 + generatedPaths: string[] +} + +export async function pruneManagedOutput( + rootDir: string, + stateFileName: string, + desiredPaths: string[], +): Promise { + const normalized = normalizePaths(desiredPaths) + const state = await readManagedOutputState(rootDir, stateFileName) + if (!state) return normalized + + const desiredSet = new Set(normalized) + const stalePaths = state.generatedPaths + .filter((filePath) => !desiredSet.has(filePath)) + .sort((left, right) => right.length - left.length) + + for (const stalePath of stalePaths) { + await fs.rm(stalePath, { recursive: true, force: true }) + } + + return normalized +} + +export async function writeManagedOutputState( + rootDir: string, + stateFileName: string, + generatedPaths: string[], +): Promise { + const statePath = path.join(rootDir, stateFileName) + await writeJson(statePath, { + version: 1, + generatedPaths: normalizePaths(generatedPaths), + } satisfies ManagedOutputState) +} + +export async function removeLegacyBackupArtifacts( + rootDir: string, + patterns: RegExp[], +): Promise { + if (!(await pathExists(rootDir))) return + const entries = await fs.readdir(rootDir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(rootDir, entry.name) + if (entry.isDirectory()) { + await removeLegacyBackupArtifacts(fullPath, patterns) + continue + } + if (entry.isFile() && patterns.some((pattern) => pattern.test(entry.name))) { + await fs.rm(fullPath, { force: true }) + } + } +} + +function normalizePaths(paths: string[]): string[] { + return [...new Set(paths.map((filePath) => path.resolve(filePath)))].sort() +} + +async function readManagedOutputState( + rootDir: string, + stateFileName: string, +): Promise { + const statePath = path.join(rootDir, stateFileName) + if (!(await pathExists(statePath))) return null + + try { + const state = await readJson(statePath) + if (state.version !== 1 || !Array.isArray(state.generatedPaths)) { + return null + } + return state + } catch { + return null + } +} diff --git a/tests/claude-writer.test.ts b/tests/claude-writer.test.ts index 5f06ac0..4d51804 100644 --- a/tests/claude-writer.test.ts +++ b/tests/claude-writer.test.ts @@ -83,4 +83,32 @@ describe("writeClaudeBundle", () => { expect(await exists(hooksPath)).toBe(false) expect(await exists(staleScriptPath)).toBe(false) }) + + test("prunes stale generated agents, commands, and skills", async () => { + const plugin = await loadPortablePlugin(fixtureRoot) + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "claude-writer-prune-")) + const outputRoot = path.join(tempRoot, "plugins", plugin.manifest.name) + + await writeClaudeBundle(outputRoot, plugin) + + const oldAgentPath = path.join(outputRoot, "agents", "research", "repo-research-analyst.md") + const oldCommandPath = path.join(outputRoot, "commands", "workflows", "plan.md") + const oldSkillPath = path.join(outputRoot, "skills", "skill-one") + expect(await exists(oldAgentPath)).toBe(true) + expect(await exists(oldCommandPath)).toBe(true) + expect(await exists(oldSkillPath)).toBe(true) + + const trimmedPlugin = { + ...plugin, + agents: [], + commands: [], + skills: [], + hooks: undefined, + } + await writeClaudeBundle(outputRoot, trimmedPlugin) + + expect(await exists(oldAgentPath)).toBe(false) + expect(await exists(oldCommandPath)).toBe(false) + expect(await exists(oldSkillPath)).toBe(false) + }) }) diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 9a80b6d..9682ebc 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -425,6 +425,57 @@ describe("CLI", () => { expect(copiedSkillContent).toContain("model: gpt-5.4-mini") expect(copiedSkillContent).toContain("/workflows-plan") expect(copiedSkillContent).toContain("~/.copilot/skills/skill-one/notes.md") + expect( + await exists(path.join(claudePluginRoot, "commands", "workflows", "references", "ignored.md")), + ).toBe(true) + }) + + test("build removes stale generated Claude commands before regeneration", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-build-stale-")) + const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-portable-plugin") + const staleCommand = path.join( + tempRoot, + "plugins", + "compound-engineering", + "commands", + "triage.md", + ) + + await fs.mkdir(path.dirname(staleCommand), { recursive: true }) + await fs.writeFile(staleCommand, "# stale triage command\n", "utf8") + + const proc = Bun.spawn([ + "bun", + "run", + "src/index.ts", + "build", + fixtureRoot, + "--output", + tempRoot, + "--targets", + "claude", + ], { + cwd: path.join(import.meta.dir, ".."), + stdout: "pipe", + stderr: "pipe", + }) + + const exitCode = await proc.exited + const stdout = await new Response(proc.stdout).text() + const stderr = await new Response(proc.stderr).text() + + if (exitCode !== 0) { + throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`) + } + + expect(stdout).toContain("Built Claude output") + expect(await exists(staleCommand)).toBe(false) + expect( + await exists(path.join(tempRoot, "plugins", "compound-engineering", "commands", "workflows", "plan.md")), + ).toBe(true) + expect( + await exists(path.join(tempRoot, "plugins", "compound-engineering", "commands", "workflows", "references")), + ).toBe(true) }) test("sync-ov registers portable agents, skills, commands, and skill support files", async () => { @@ -497,6 +548,68 @@ describe("CLI", () => { expect(log).toContain("rebuild\tglobal-manifest") }) + test("sync-ov prunes legacy triage skill artifacts when workflows:triage is present", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-sync-ov-triage-legacy-")) + const sourceFixture = path.join(import.meta.dir, "fixtures", "sample-portable-plugin") + const fixtureRoot = path.join(tempRoot, "portable-plugin") + const fakeOvCore = path.join(import.meta.dir, "fixtures", "fake-ov-core.sh") + const fakeOvRoot = path.join(tempRoot, "ov-global") + const fakeOvLog = path.join(tempRoot, "ov.log") + const legacySkillPath = path.join(fakeOvRoot, "skills", "triage.md") + const legacySupportPath = path.join(fakeOvRoot, "skills", "triage", "SKILL.md") + + await fs.cp(sourceFixture, fixtureRoot, { recursive: true }) + await fs.writeFile( + path.join(fixtureRoot, "commands", "workflows", "triage.md"), + [ + "---", + "name: workflows:triage", + "description: Triage todo items.", + "---", + "", + "Task execution-agent(resolve todos)", + "", + ].join("\n"), + "utf8", + ) + await fs.mkdir(path.dirname(legacySkillPath), { recursive: true }) + await fs.mkdir(path.dirname(legacySupportPath), { recursive: true }) + await fs.writeFile(legacySkillPath, "# stale triage\n", "utf8") + await fs.writeFile(legacySupportPath, "# stale triage skill\n", "utf8") + + const proc = Bun.spawn([ + "bun", + "run", + "src/index.ts", + "sync-ov", + fixtureRoot, + "--ov-core", + fakeOvCore, + ], { + cwd: path.join(import.meta.dir, ".."), + stdout: "pipe", + stderr: "pipe", + env: { + ...process.env, + FAKE_OV_ROOT: fakeOvRoot, + FAKE_OV_LOG: fakeOvLog, + }, + }) + + const exitCode = await proc.exited + const stdout = await new Response(proc.stdout).text() + const stderr = await new Response(proc.stderr).text() + + if (exitCode !== 0) { + throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`) + } + + expect(stdout).toContain("Synced 1 agents, 1 skills, 2 commands") + expect(await exists(legacySkillPath)).toBe(false) + expect(await exists(path.join(fakeOvRoot, "skills", "triage"))).toBe(false) + expect(await exists(path.join(fakeOvRoot, "skills", "workflows-triage.md"))).toBe(true) + }) + test("sync-ov rejects unsafe skill names before mirroring", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-sync-ov-invalid-skill-")) const sourceFixture = path.join(import.meta.dir, "fixtures", "sample-portable-plugin") diff --git a/tests/codex-converter.test.ts b/tests/codex-converter.test.ts index 755d6e9..141fc90 100644 --- a/tests/codex-converter.test.ts +++ b/tests/codex-converter.test.ts @@ -172,7 +172,7 @@ Don't confuse with file paths like /tmp/output.md or /dev/null.`, expect(parsed.body).toContain("/dev/null") }) - test("excludes commands with disable-model-invocation from prompts and skills", () => { + test("keeps commands with disable-model-invocation in prompts and skills", () => { const plugin: ClaudePlugin = { ...fixturePlugin, commands: [ @@ -200,14 +200,14 @@ Don't confuse with file paths like /tmp/output.md or /dev/null.`, permissions: "none", }) - // Only normal command should produce a prompt - expect(bundle.prompts).toHaveLength(1) - expect(bundle.prompts[0].name).toBe("normal-command") + // Both commands should produce prompts + expect(bundle.prompts).toHaveLength(2) + expect(bundle.prompts.some((prompt) => prompt.name === "normal-command")).toBe(true) + expect(bundle.prompts.some((prompt) => prompt.name === "disabled-command")).toBe(true) - // Only normal command should produce a generated skill + // Both commands should produce generated skills const commandSkills = bundle.generatedSkills.filter((s) => s.name === "normal-command" || s.name === "disabled-command") - expect(commandSkills).toHaveLength(1) - expect(commandSkills[0].name).toBe("normal-command") + expect(commandSkills).toHaveLength(2) }) test("rewrites .claude/ paths to .codex/ in command skill bodies", () => { diff --git a/tests/codex-writer.test.ts b/tests/codex-writer.test.ts index 3aeb42e..dae3dd4 100644 --- a/tests/codex-writer.test.ts +++ b/tests/codex-writer.test.ts @@ -74,7 +74,7 @@ describe("writeCodexBundle", () => { expect(await exists(path.join(codexRoot, "skills", "skill-one", "SKILL.md"))).toBe(true) }) - test("backs up existing config.toml before overwriting", async () => { + test("overwrites existing config.toml without backup artifacts", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-backup-")) const codexRoot = path.join(tempRoot, ".codex") const configPath = path.join(codexRoot, "config.toml") @@ -97,12 +97,38 @@ describe("writeCodexBundle", () => { const newConfig = await fs.readFile(configPath, "utf8") expect(newConfig).toContain("[mcp_servers.test]") - // Backup should exist with original content + // Backup should not be created const files = await fs.readdir(codexRoot) const backupFileName = files.find((f) => f.startsWith("config.toml.bak.")) - expect(backupFileName).toBeDefined() + expect(backupFileName).toBeUndefined() + }) + + test("prunes stale generated prompts and skills", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-prune-")) + const codexRoot = path.join(tempRoot, ".codex") + + const firstBundle: CodexBundle = { + prompts: [{ name: "old-command", content: "Old" }], + skillDirs: [ + { + name: "skill-one", + sourceDir: path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one"), + }, + ], + generatedSkills: [{ name: "old-generated", content: "Old generated" }], + } + await writeCodexBundle(codexRoot, firstBundle) + + const secondBundle: CodexBundle = { + prompts: [{ name: "new-command", content: "New" }], + skillDirs: [], + generatedSkills: [], + } + await writeCodexBundle(codexRoot, secondBundle) - const backupContent = await fs.readFile(path.join(codexRoot, backupFileName!), "utf8") - expect(backupContent).toBe(originalContent) + expect(await exists(path.join(codexRoot, "prompts", "old-command.md"))).toBe(false) + expect(await exists(path.join(codexRoot, "skills", "old-generated", "SKILL.md"))).toBe(false) + expect(await exists(path.join(codexRoot, "skills", "skill-one"))).toBe(false) + expect(await exists(path.join(codexRoot, "prompts", "new-command.md"))).toBe(true) }) }) diff --git a/tests/converter.test.ts b/tests/converter.test.ts index 1c2ec48..43f007f 100644 --- a/tests/converter.test.ts +++ b/tests/converter.test.ts @@ -204,7 +204,7 @@ describe("convertClaudeToOpenCode", () => { expect(parsed.data.mode).toBe("primary") }) - test("excludes commands with disable-model-invocation from command map", async () => { + test("keeps commands with disable-model-invocation in command map", async () => { const plugin = await loadClaudePlugin(fixtureRoot) const bundle = convertClaudeToOpenCode(plugin, { agentMode: "subagent", @@ -212,8 +212,8 @@ describe("convertClaudeToOpenCode", () => { permissions: "none", }) - // deploy-docs has disable-model-invocation: true, should be excluded - expect(bundle.commandFiles.find((f) => f.name === "deploy-docs")).toBeUndefined() + // deploy-docs has disable-model-invocation: true and should still be exported for OpenCode. + expect(bundle.commandFiles.find((f) => f.name === "deploy-docs")).toBeDefined() // Normal commands should still be present expect(bundle.commandFiles.find((f) => f.name === "workflows:review")).toBeDefined() diff --git a/tests/copilot-writer.test.ts b/tests/copilot-writer.test.ts index 411c162..8ae5e78 100644 --- a/tests/copilot-writer.test.ts +++ b/tests/copilot-writer.test.ts @@ -78,4 +78,29 @@ describe("writeCopilotBundle", () => { "skill name contains unsafe path characters: ../escape", ) }) + + test("prunes stale generated agents and skills", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-prune-")) + const outputRoot = path.join(tempRoot, ".github") + const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one") + + const firstBundle: CopilotBundle = { + agents: [{ name: "old-agent", content: "old" }], + generatedSkills: [{ name: "old-generated", content: "old skill" }], + skillDirs: [{ name: "skill-one", sourceDir: fixtureSkillDir, skillPath: path.join(fixtureSkillDir, "SKILL.md") }], + } + await writeCopilotBundle(outputRoot, firstBundle) + + const secondBundle: CopilotBundle = { + agents: [{ name: "new-agent", content: "new" }], + generatedSkills: [], + skillDirs: [], + } + await writeCopilotBundle(outputRoot, secondBundle) + + expect(await exists(path.join(outputRoot, "agents", "old-agent.agent.md"))).toBe(false) + expect(await exists(path.join(outputRoot, "skills", "old-generated"))).toBe(false) + expect(await exists(path.join(outputRoot, "skills", "skill-one"))).toBe(false) + expect(await exists(path.join(outputRoot, "agents", "new-agent.agent.md"))).toBe(true) + }) }) diff --git a/tests/droid-writer.test.ts b/tests/droid-writer.test.ts index f8ecf6c..ebe98d5 100644 --- a/tests/droid-writer.test.ts +++ b/tests/droid-writer.test.ts @@ -97,4 +97,29 @@ describe("writeDroidBundle", () => { expect(await exists(path.join(factoryRoot, "commands", "work.md"))).toBe(true) expect(await exists(path.join(factoryRoot, "commands", "brainstorm.md"))).toBe(true) }) + + test("prunes stale generated commands, droids, and skills", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "droid-prune-")) + const outputRoot = path.join(tempRoot, ".factory") + const skillFixture = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one") + + const firstBundle: DroidBundle = { + commands: [{ name: "old-command", content: "old" }], + droids: [{ name: "old-droid", content: "old" }], + skillDirs: [{ name: "skill-one", sourceDir: skillFixture }], + } + await writeDroidBundle(outputRoot, firstBundle) + + const secondBundle: DroidBundle = { + commands: [{ name: "new-command", content: "new" }], + droids: [], + skillDirs: [], + } + await writeDroidBundle(outputRoot, secondBundle) + + expect(await exists(path.join(outputRoot, "commands", "old-command.md"))).toBe(false) + expect(await exists(path.join(outputRoot, "droids", "old-droid.md"))).toBe(false) + expect(await exists(path.join(outputRoot, "skills", "skill-one"))).toBe(false) + expect(await exists(path.join(outputRoot, "commands", "new-command.md"))).toBe(true) + }) }) diff --git a/tests/gemini-writer.test.ts b/tests/gemini-writer.test.ts index a6a9df3..0a18cc5 100644 --- a/tests/gemini-writer.test.ts +++ b/tests/gemini-writer.test.ts @@ -117,7 +117,7 @@ describe("writeGeminiBundle", () => { expect(await exists(tempRoot)).toBe(true) }) - test("backs up existing settings.json before overwrite", async () => { + test("overwrites settings.json without backup artifacts", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-backup-")) const geminiRoot = path.join(tempRoot, ".gemini") await fs.mkdir(geminiRoot, { recursive: true }) @@ -141,10 +141,10 @@ describe("writeGeminiBundle", () => { const newContent = JSON.parse(await fs.readFile(settingsPath, "utf8")) expect(newContent.mcpServers.newServer.command).toBe("new-cmd") - // A backup file should exist + // Backup files should not exist const files = await fs.readdir(geminiRoot) const backupFiles = files.filter((f) => f.startsWith("settings.json.bak.")) - expect(backupFiles.length).toBeGreaterThanOrEqual(1) + expect(backupFiles.length).toBe(0) }) test("merges mcpServers into existing settings.json without clobbering other keys", async () => { @@ -178,4 +178,32 @@ describe("writeGeminiBundle", () => { // Should add new MCP server expect(content.mcpServers.newServer.command).toBe("new-cmd") }) + + test("prunes stale generated skills and commands", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-prune-")) + const bundleOne: GeminiBundle = { + generatedSkills: [{ name: "old-skill", content: "old" }], + skillDirs: [ + { + name: "skill-one", + sourceDir: path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one"), + }, + ], + commands: [{ name: "old-command", content: "old" }], + } + await writeGeminiBundle(tempRoot, bundleOne) + + const bundleTwo: GeminiBundle = { + generatedSkills: [{ name: "new-skill", content: "new" }], + skillDirs: [], + commands: [{ name: "new-command", content: "new" }], + } + await writeGeminiBundle(tempRoot, bundleTwo) + + expect(await exists(path.join(tempRoot, ".gemini", "skills", "old-skill"))).toBe(false) + expect(await exists(path.join(tempRoot, ".gemini", "skills", "skill-one"))).toBe(false) + expect(await exists(path.join(tempRoot, ".gemini", "commands", "old-command.toml"))).toBe(false) + expect(await exists(path.join(tempRoot, ".gemini", "skills", "new-skill", "SKILL.md"))).toBe(true) + expect(await exists(path.join(tempRoot, ".gemini", "commands", "new-command.toml"))).toBe(true) + }) }) diff --git a/tests/opencode-writer.test.ts b/tests/opencode-writer.test.ts index 7b06219..6a1a5d7 100644 --- a/tests/opencode-writer.test.ts +++ b/tests/opencode-writer.test.ts @@ -140,7 +140,7 @@ describe("writeOpenCodeBundle", () => { ).toBe(true) }) - test("backs up existing opencode.json before overwriting", async () => { + test("merges existing opencode.json without creating backup files", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-backup-")) const outputRoot = path.join(tempRoot, ".opencode") const configPath = path.join(outputRoot, "opencode.json") @@ -164,12 +164,74 @@ describe("writeOpenCodeBundle", () => { expect(newConfig.custom).toBe("value") expect(newConfig.$schema).toBe("https://opencode.ai/config.json") - // Backup should exist with original content + // No backup files should be left behind const files = await fs.readdir(outputRoot) const backupFileName = files.find((f) => f.startsWith("opencode.json.bak.")) - expect(backupFileName).toBeDefined() + expect(backupFileName).toBeUndefined() + }) + + test("prunes stale generated artifacts and legacy backup files", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-prune-")) + const outputRoot = path.join(tempRoot, ".opencode") + const commandDir = path.join(outputRoot, "commands") + const skillFixture = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one") + const skillPath = path.join(skillFixture, "SKILL.md") + + const firstBundle: OpenCodeBundle = { + config: { $schema: "https://opencode.ai/config.json" }, + agents: [{ name: "old-agent", content: "Old agent" }], + commandFiles: [{ name: "old-command", content: "Old command" }], + plugins: [{ name: "old-plugin.ts", content: "export {}" }], + skillDirs: [{ name: "old-skill", sourceDir: skillFixture, skillPath }], + } + + await writeOpenCodeBundle(outputRoot, firstBundle) + await fs.writeFile(path.join(outputRoot, "opencode.json.bak.legacy"), "{}\n") + await fs.mkdir(commandDir, { recursive: true }) + await fs.writeFile(path.join(commandDir, "old-command.md.bak.legacy"), "# stale backup\n") + + const secondBundle: OpenCodeBundle = { + config: { $schema: "https://opencode.ai/config.json" }, + agents: [{ name: "new-agent", content: "New agent" }], + commandFiles: [{ name: "new-command", content: "New command" }], + plugins: [], + skillDirs: [], + } + + await writeOpenCodeBundle(outputRoot, secondBundle) + + expect(await exists(path.join(outputRoot, "agents", "old-agent.md"))).toBe(false) + expect(await exists(path.join(outputRoot, "commands", "old-command.md"))).toBe(false) + expect(await exists(path.join(outputRoot, "plugins", "old-plugin.ts"))).toBe(false) + expect(await exists(path.join(outputRoot, "skills", "old-skill"))).toBe(false) + expect(await exists(path.join(outputRoot, "agents", "new-agent.md"))).toBe(true) + expect(await exists(path.join(outputRoot, "commands", "new-command.md"))).toBe(true) + expect(await exists(path.join(outputRoot, "opencode.json.bak.legacy"))).toBe(false) + expect(await exists(path.join(commandDir, "old-command.md.bak.legacy"))).toBe(false) + }) + + test("prunes legacy triage artifacts when workflows:triage is installed", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-triage-legacy-")) + const outputRoot = path.join(tempRoot, ".opencode") + const legacySkillRoot = path.join(outputRoot, "skills", "triage") + + await fs.mkdir(path.join(outputRoot, "commands"), { recursive: true }) + await fs.mkdir(legacySkillRoot, { recursive: true }) + await fs.writeFile(path.join(outputRoot, "commands", "triage.md"), "# stale triage\n") + await fs.writeFile(path.join(legacySkillRoot, "SKILL.md"), "# stale triage skill\n") + + const bundle: OpenCodeBundle = { + config: { $schema: "https://opencode.ai/config.json" }, + agents: [], + commandFiles: [{ name: "workflows:triage", content: "Use workflows triage." }], + plugins: [], + skillDirs: [], + } + + await writeOpenCodeBundle(outputRoot, bundle) - const backupContent = JSON.parse(await fs.readFile(path.join(outputRoot, backupFileName!), "utf8")) - expect(backupContent.custom).toBe("value") + expect(await exists(path.join(outputRoot, "commands", "triage.md"))).toBe(false) + expect(await exists(legacySkillRoot)).toBe(false) + expect(await exists(path.join(outputRoot, "commands", "workflows:triage.md"))).toBe(true) }) }) diff --git a/tests/pi-converter.test.ts b/tests/pi-converter.test.ts index d7edf95..4b6b0f7 100644 --- a/tests/pi-converter.test.ts +++ b/tests/pi-converter.test.ts @@ -20,8 +20,8 @@ describe("convertClaudeToPi", () => { expect(bundle.prompts.some((prompt) => prompt.name === "workflows-review")).toBe(true) expect(bundle.prompts.some((prompt) => prompt.name === "plan_review")).toBe(true) - // Commands with disable-model-invocation are excluded - expect(bundle.prompts.some((prompt) => prompt.name === "deploy-docs")).toBe(false) + // Commands with disable-model-invocation are still included for parity with other exports. + expect(bundle.prompts.some((prompt) => prompt.name === "deploy-docs")).toBe(true) const workflowsReview = bundle.prompts.find((prompt) => prompt.name === "workflows-review") expect(workflowsReview).toBeDefined() diff --git a/tests/pi-writer.test.ts b/tests/pi-writer.test.ts index 5af7ea6..80469e7 100644 --- a/tests/pi-writer.test.ts +++ b/tests/pi-writer.test.ts @@ -67,7 +67,7 @@ describe("writePiBundle", () => { expect(await exists(path.join(outputRoot, ".pi"))).toBe(false) }) - test("backs up existing mcporter config before overwriting", async () => { + test("overwrites existing mcporter config without backup artifacts", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-backup-")) const outputRoot = path.join(tempRoot, ".pi") const configPath = path.join(outputRoot, "compound-engineering", "mcporter.json") @@ -91,9 +91,37 @@ describe("writePiBundle", () => { const files = await fs.readdir(path.dirname(configPath)) const backupFileName = files.find((file) => file.startsWith("mcporter.json.bak.")) - expect(backupFileName).toBeDefined() + expect(backupFileName).toBeUndefined() const currentConfig = JSON.parse(await fs.readFile(configPath, "utf8")) as { mcpServers: Record } expect(currentConfig.mcpServers.linear).toBeDefined() }) + + test("prunes stale generated prompts, skills, and extensions", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-prune-")) + const outputRoot = path.join(tempRoot, ".pi") + const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one") + + const firstBundle: PiBundle = { + prompts: [{ name: "old-prompt", content: "old" }], + skillDirs: [{ name: "skill-one", sourceDir: fixtureSkillDir }], + generatedSkills: [{ name: "old-generated", content: "old" }], + extensions: [{ name: "old-ext.ts", content: "old" }], + } + await writePiBundle(outputRoot, firstBundle) + + const secondBundle: PiBundle = { + prompts: [{ name: "new-prompt", content: "new" }], + skillDirs: [], + generatedSkills: [], + extensions: [], + } + await writePiBundle(outputRoot, secondBundle) + + expect(await exists(path.join(outputRoot, "prompts", "old-prompt.md"))).toBe(false) + expect(await exists(path.join(outputRoot, "skills", "old-generated"))).toBe(false) + expect(await exists(path.join(outputRoot, "skills", "skill-one"))).toBe(false) + expect(await exists(path.join(outputRoot, "extensions", "old-ext.ts"))).toBe(false) + expect(await exists(path.join(outputRoot, "prompts", "new-prompt.md"))).toBe(true) + }) })