🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

getsuperpower

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

getsuperpower - npm Package Compare versions

Comparing version
0.3.6
to
0.4.0
+23
examples/workflows/ceo/README.md
# CEO GetSuperpower
Use this workflow when an agent should act as a startup CEO: clarify direction,
make hard tradeoffs, and turn ambiguous founder intent into a decision-ready
company narrative.
Install it from the repo root:
```bash
bun run dev -- install examples/workflows/ceo
```
Validate it while authoring:
```bash
bun run dev -- validate examples/workflows/ceo
```
Refresh the checked skill fingerprints after editing the local role skill:
```bash
bun run dev -- lock examples/workflows/ceo
```
---
name: ceo
description: "Use when acting as a startup CEO for direction, tradeoffs, strategy, fundraising, positioning, or company-level decisions."
---
# CEO
Use this role when the user needs founder-level judgment more than task
execution. Your job is to compress ambiguity into a clear direction, name the
tradeoffs, and protect the company from attractive but unfocused work.
## Required Companion Skills
- `mattpocock:decision-mapping` for turning uncertain direction into decision paths.
- `mattpocock:grill-with-docs` for stress-testing the plan and producing durable docs.
If a companion skill is unavailable, stop and tell the user which dependency is
missing.
## Operating Mode
1. State the company-level decision in one sentence.
2. Separate customer, market, product, technical, and funding constraints.
3. Identify the reversible and irreversible decisions.
4. Recommend the next company move, with the smallest evidence-gathering step.
5. Push decisions into docs or tickets only after the direction is explicit.
{
"schemaVersion": "0.1",
"name": "ceo",
"version": "0.1.0",
"description": "Act as the startup CEO for direction, tradeoffs, investor/customer framing, and company-level decisions.",
"skills": [
{
"source": "./skills/ceo",
"entry": true
},
{
"source": "mattpocock:decision-mapping",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:grill-with-docs",
"repo": "mattpocock/skills"
}
],
"steps": [
{
"id": "ceo-brief",
"title": "Set the executive frame",
"skill": "./skills/ceo",
"gate": "human_approval"
},
{
"id": "decision-map",
"title": "Map the decision and uncertainties",
"skill": "mattpocock:decision-mapping"
},
{
"id": "grill-direction",
"title": "Stress-test the company direction",
"skill": "mattpocock:grill-with-docs",
"gate": "human_approval"
}
]
}
{
"schemaVersion": "0.1",
"workflow": "ceo",
"workflowVersion": "0.1.0",
"generatedAt": "2026-07-07T12:15:26.289Z",
"skills": [
{
"source": "./skills/ceo",
"resolvedName": "ceo",
"kind": "local",
"hash": "sha256:e28e960ca32f944aad4353c9248e43ca7526a5f4451d1293cd79590878f2b25a"
},
{
"source": "mattpocock:decision-mapping",
"resolvedName": "decision-mapping",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:4f85dd52dfcf4b6a11c404fbcb461cf454a734ceb084084e4272020c7a66962e"
},
{
"source": "mattpocock:grill-with-docs",
"resolvedName": "grill-with-docs",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:69d15d68b241680fbccaccd86cb986bd4da2589428d5e11b1fc97e84b2956bf4"
}
]
}
# CTO GetSuperpower
Use this workflow when an agent should act as a startup CTO: inspect architecture,
surface technical risk, and turn product direction into engineering strategy.
Install it from the repo root:
```bash
bun run dev -- install examples/workflows/cto
```
Validate it while authoring:
```bash
bun run dev -- validate examples/workflows/cto
```
Refresh the checked skill fingerprints after editing the local role skill:
```bash
bun run dev -- lock examples/workflows/cto
```
---
name: cto
description: "Use when acting as a startup CTO for architecture, technical risk, platform direction, codebase boundaries, or engineering strategy."
---
# CTO
Use this role when the user needs technical leadership rather than direct
implementation. Your job is to protect speed without mortgaging the codebase.
## Required Companion Skills
- `mattpocock:codebase-design` for module boundaries and architecture pressure.
- `mattpocock:domain-modeling` for naming the core business concepts.
- `mattpocock:diagnosing-bugs` for isolating failures before proposing fixes.
- `mattpocock:review` for reviewing code and design risks.
If a companion skill is unavailable, stop and tell the user which dependency is
missing.
## Operating Mode
1. Identify the decision that changes the technical trajectory.
2. Map the domain language before naming abstractions.
3. Keep interfaces small and deep; avoid coordination-heavy designs.
4. Separate startup-speed shortcuts from debt that blocks future product work.
5. Recommend the next technical move and its verification gate.
{
"schemaVersion": "0.1",
"name": "cto",
"version": "0.1.0",
"description": "Act as the startup CTO for architecture, technical risk, platform boundaries, and engineering strategy.",
"skills": [
{
"source": "./skills/cto",
"entry": true
},
{
"source": "mattpocock:codebase-design",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:domain-modeling",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:diagnosing-bugs",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:review",
"repo": "mattpocock/skills"
}
],
"steps": [
{
"id": "cto-brief",
"title": "Set the technical frame",
"skill": "./skills/cto",
"gate": "human_approval"
},
{
"id": "domain-model",
"title": "Clarify the domain model",
"skill": "mattpocock:domain-modeling"
},
{
"id": "architecture",
"title": "Review the architecture boundary",
"skill": "mattpocock:codebase-design"
},
{
"id": "risk",
"title": "Diagnose technical risk",
"skill": "mattpocock:diagnosing-bugs"
},
{
"id": "review",
"title": "Review the technical decision",
"skill": "mattpocock:review"
}
]
}
{
"schemaVersion": "0.1",
"workflow": "cto",
"workflowVersion": "0.1.0",
"generatedAt": "2026-07-07T12:15:33.931Z",
"skills": [
{
"source": "./skills/cto",
"resolvedName": "cto",
"kind": "local",
"hash": "sha256:644afba52d60f4bbcf9a608c6ead98688650e9fc3f8ed0a63ac0d30ca4931156"
},
{
"source": "mattpocock:codebase-design",
"resolvedName": "codebase-design",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:d921291ee4c38b537d2cad94205e6c983798644777ed0aeac0c44d2d6e3adbcd"
},
{
"source": "mattpocock:domain-modeling",
"resolvedName": "domain-modeling",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:41c64419a43153fd1aaca8be36f03c5a6e8cbf484d53e40a88fbb1ae0e6d3ace"
},
{
"source": "mattpocock:diagnosing-bugs",
"resolvedName": "diagnosing-bugs",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:0fe0adf2995715573a8efc68d47e9fd6f5c56230aaad6f148821feb883a34083"
},
{
"source": "mattpocock:review",
"resolvedName": "review",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:f31dad85ccfc89efe4ccc5353d3b67aad00f87700c1d709eba8c758ef64757dd"
}
]
}
# Engineering Manager GetSuperpower
Use this workflow when an agent should act as a startup engineering manager:
sequence delivery, manage execution risk, and keep quality gates realistic.
Install it from the repo root:
```bash
bun run dev -- install examples/workflows/engineering-manager
```
Validate it while authoring:
```bash
bun run dev -- validate examples/workflows/engineering-manager
```
Refresh the checked skill fingerprints after editing the local role skill:
```bash
bun run dev -- lock examples/workflows/engineering-manager
```
---
name: engineering-manager
description: "Use when acting as a startup engineering manager for delivery planning, execution risk, quality gates, and team process."
---
# Engineering Manager
Use this role when the user needs delivery leadership. Your job is to turn a
product or technical direction into a sequence the team can actually execute.
## Required Companion Skills
- `superpowers:writing-plans` for concrete implementation plans.
- `mattpocock:tdd` for test-first implementation strategy.
- `mattpocock:diagnosing-bugs` for blocker triage.
- `mattpocock:review` for delivery and code-review risk.
If a companion skill is unavailable, stop and tell the user which dependency is
missing.
## Operating Mode
1. Identify the smallest shippable outcome and the biggest delivery risk.
2. Sequence work so each step leaves the repo in a verifiable state.
3. Assign quality gates based on blast radius, not ceremony.
4. Escalate unclear ownership, missing acceptance criteria, or blocked decisions.
5. Keep implementation scope smaller than the team wants and clearer than they expect.
{
"schemaVersion": "0.1",
"name": "engineering-manager",
"version": "0.1.0",
"description": "Act as the startup engineering manager for delivery sequencing, execution risk, quality gates, and team-level engineering process.",
"skills": [
{
"source": "./skills/engineering-manager",
"entry": true
},
{
"source": "superpowers:writing-plans",
"repo": "obra/superpowers"
},
{
"source": "mattpocock:tdd",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:diagnosing-bugs",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:review",
"repo": "mattpocock/skills"
}
],
"steps": [
{
"id": "em-brief",
"title": "Set the delivery frame",
"skill": "./skills/engineering-manager",
"gate": "human_approval"
},
{
"id": "plan",
"title": "Write the execution plan",
"skill": "superpowers:writing-plans"
},
{
"id": "quality",
"title": "Choose the test strategy",
"skill": "mattpocock:tdd"
},
{
"id": "debug",
"title": "Triage blockers systematically",
"skill": "mattpocock:diagnosing-bugs"
},
{
"id": "review",
"title": "Review the delivery risk",
"skill": "mattpocock:review"
}
]
}
{
"schemaVersion": "0.1",
"workflow": "engineering-manager",
"workflowVersion": "0.1.0",
"generatedAt": "2026-07-07T12:15:34.138Z",
"skills": [
{
"source": "./skills/engineering-manager",
"resolvedName": "engineering-manager",
"kind": "local",
"hash": "sha256:70d97c45ac61d3774317681dc7ae318126e14a3d0b19f00183d8227ca0fb1071"
},
{
"source": "superpowers:writing-plans",
"resolvedName": "superpowers-writing-plans",
"kind": "external",
"repo": "obra/superpowers",
"hash": "sha256:8a8ea7693023742a048a23a7da0255031bc13ee03c80c1f01d400cab99fbbd84"
},
{
"source": "mattpocock:tdd",
"resolvedName": "tdd",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:1fd1eb7a880e3e52debaa8e3b9248b7b71816e2a38f519974e22674cf37b82b9"
},
{
"source": "mattpocock:diagnosing-bugs",
"resolvedName": "diagnosing-bugs",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:0fe0adf2995715573a8efc68d47e9fd6f5c56230aaad6f148821feb883a34083"
},
{
"source": "mattpocock:review",
"resolvedName": "review",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:f31dad85ccfc89efe4ccc5353d3b67aad00f87700c1d709eba8c758ef64757dd"
}
]
}
# Founding Engineer GetSuperpower
Use this workflow when an agent should act as a startup founding engineer:
implement the plan, keep tests close to the change, debug failures, and verify
before handoff.
Install it from the repo root:
```bash
bun run dev -- install examples/workflows/founding-engineer
```
Validate it while authoring:
```bash
bun run dev -- validate examples/workflows/founding-engineer
```
Refresh the checked skill fingerprints after editing the local role skill:
```bash
bun run dev -- lock examples/workflows/founding-engineer
```
---
name: founding-engineer
description: "Use when acting as a startup founding engineer for implementation, tests, debugging, review, and verification."
---
# Founding Engineer
Use this role when the user wants implementation, not just planning. Your job is
to ship the smallest correct change, keep the repo coherent, and verify the work
before handing it back.
## Required Companion Skills
- `implement` for executing a PRD, plan, or issue set.
- `mattpocock:tdd` for test-first work where practical.
- `mattpocock:diagnosing-bugs` for failures and regressions.
- `mattpocock:review` for code-review posture.
- `superpowers:verification-before-completion` before claiming the work is done.
If a companion skill is unavailable, stop and tell the user which dependency is
missing.
## Operating Mode
1. Read the plan, acceptance criteria, and local repo instructions before editing.
2. Prefer focused tests around changed behavior and keep implementation scope tight.
3. Run the smallest useful verification loop while building.
4. Debug from evidence when tests fail; do not patch by guesswork.
5. Finish with the commands run, files changed, and any residual risk.
{
"schemaVersion": "0.1",
"name": "founding-engineer",
"version": "0.1.0",
"description": "Act as the startup founding engineer for implementation, testing, debugging, review, and verification before handoff.",
"skills": [
{
"source": "./skills/founding-engineer",
"entry": true
},
{
"source": "implement"
},
{
"source": "mattpocock:tdd",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:diagnosing-bugs",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:review",
"repo": "mattpocock/skills"
},
{
"source": "superpowers:verification-before-completion",
"repo": "obra/superpowers"
}
],
"steps": [
{
"id": "fe-brief",
"title": "Set the implementation frame",
"skill": "./skills/founding-engineer",
"gate": "human_approval"
},
{
"id": "implement",
"title": "Implement the planned change",
"skill": "implement"
},
{
"id": "tdd",
"title": "Use test-first development where practical",
"skill": "mattpocock:tdd"
},
{
"id": "debug",
"title": "Diagnose failures systematically",
"skill": "mattpocock:diagnosing-bugs"
},
{
"id": "review",
"title": "Review the implementation",
"skill": "mattpocock:review"
},
{
"id": "verify",
"title": "Verify before completion",
"skill": "superpowers:verification-before-completion"
}
]
}
{
"schemaVersion": "0.1",
"workflow": "founding-engineer",
"workflowVersion": "0.1.0",
"generatedAt": "2026-07-07T12:15:34.156Z",
"skills": [
{
"source": "./skills/founding-engineer",
"resolvedName": "founding-engineer",
"kind": "local",
"hash": "sha256:2c1ee7f8710c90004a958f81aa84321fad2efc83d8839fede97689f6ebf1b078"
},
{
"source": "implement",
"resolvedName": "implement",
"kind": "external",
"hash": "sha256:6c84d6b8b3e4e2624d5009841023404b9aec0ac06134f3d2c2aff473b25f259d"
},
{
"source": "mattpocock:tdd",
"resolvedName": "tdd",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:1fd1eb7a880e3e52debaa8e3b9248b7b71816e2a38f519974e22674cf37b82b9"
},
{
"source": "mattpocock:diagnosing-bugs",
"resolvedName": "diagnosing-bugs",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:0fe0adf2995715573a8efc68d47e9fd6f5c56230aaad6f148821feb883a34083"
},
{
"source": "mattpocock:review",
"resolvedName": "review",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:f31dad85ccfc89efe4ccc5353d3b67aad00f87700c1d709eba8c758ef64757dd"
},
{
"source": "superpowers:verification-before-completion",
"resolvedName": "superpowers:verification-before-completion",
"kind": "external",
"repo": "obra/superpowers",
"hash": "sha256:c0cd676b75a7eed20cc419b3c5fe46abbac00fd28cd87b6617fbc3607b1d3fd8"
}
]
}
# Haaland GetSuperpower
Use this workflow when an agent should make exactly one playful Haaland/JTS meme:
a compact football-finisher joke, social caption, or parody post concept with a
clear punchline.
The included profile icon is an original parody asset at
`skills/haaland/assets/haaland-profile-icon.svg`. It is not an official player
photo, club, league, or sponsor mark.
Install it from the repo root:
```bash
rtk bun run dev -- install examples/workflows/haaland
```
Validate it while authoring:
```bash
rtk bun run dev -- validate examples/workflows/haaland
```
Refresh the checked skill fingerprints after editing the local meme skill:
```bash
rtk bun run dev -- lock examples/workflows/haaland
```
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
<title id="title">Haaland profile icon</title>
<desc id="desc">Original profile-style parody icon of a blond football finisher with H9 lettering.</desc>
<defs>
<linearGradient id="bg" x1="88" x2="424" y1="80" y2="432" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#d8ff4f" />
<stop offset="0.45" stop-color="#31d38c" />
<stop offset="1" stop-color="#0b1220" />
</linearGradient>
<linearGradient id="hair" x1="162" x2="354" y1="96" y2="224" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff27a" />
<stop offset="0.62" stop-color="#f7bd2e" />
<stop offset="1" stop-color="#cf7d14" />
</linearGradient>
</defs>
<rect width="512" height="512" rx="128" fill="#071015" />
<circle cx="256" cy="256" r="205" fill="url(#bg)" />
<circle cx="256" cy="256" r="184" fill="none" stroke="#ecfff5" stroke-opacity="0.34" stroke-width="12" />
<path d="M125 399c20-76 74-120 131-120s111 44 131 120c-35 33-82 52-131 52s-96-19-131-52Z" fill="#09111f" />
<path d="M168 360c19-45 54-69 88-69s69 24 88 69c-24 23-54 36-88 36s-64-13-88-36Z" fill="#9df72f" opacity="0.95" />
<path d="M182 186c0-61 34-101 82-101 49 0 81 40 81 100 0 68-35 111-82 111s-81-43-81-110Z" fill="#f3c5a6" />
<path d="M171 192c11-73 53-112 107-104 48 7 78 43 80 108-31-26-60-31-94-17-35 14-63 16-93 13Z" fill="url(#hair)" />
<path d="M166 187c13-62 51-101 101-101 55 0 91 43 91 106-28-33-61-42-99-25-31 14-60 20-93 20Z" fill="#ffe35a" />
<path d="M210 206c17 17 73 17 91 0" fill="none" stroke="#101827" stroke-width="11" stroke-linecap="round" />
<path d="M224 243c17 12 61 12 78 0" fill="none" stroke="#101827" stroke-width="9" stroke-linecap="round" opacity="0.55" />
<circle cx="206" cy="214" r="7" fill="#101827" />
<circle cx="319" cy="214" r="7" fill="#101827" />
<text x="256" y="433" text-anchor="middle" font-family="Arial Black, Arial, sans-serif" font-size="58" fill="#ffffff">H9</text>
<path d="M121 127 151 95 181 127" fill="none" stroke="#ecfff5" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" opacity="0.85" />
<path d="M331 127 361 95 391 127" fill="none" stroke="#ecfff5" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" opacity="0.85" />
</svg>
---
name: haaland
description: "Use when making one playful Haaland, JTS, tap-in, striker, or football-finisher meme caption with an original profile icon."
---
# Haaland
Use this role when the user wants exactly one meme, joke caption, or social post
concept in the spirit of a deadpan football finisher: direct run, absurd
confidence, tap-in punchline.
## Brand Safety
This is parody fan energy, not official branding. Do not use club crests,
sponsor marks, official photos, or claims of endorsement. Use the original
profile icon in `assets/haaland-profile-icon.svg` when a workflow asset is
needed.
## Operating Mode
1. Restate the meme target in one sentence.
2. Write one caption under 120 characters.
3. Add one posting note with the profile icon placement and tone guardrail.
Create exactly one finished meme concept. Do not branch into multiple angles,
options, companion-skill handoffs, or conditional fallback paths.
## Output Shape
```text
Meme target:
Caption:
Posting note:
```
{
"schemaVersion": "0.1",
"name": "haaland",
"version": "0.1.0",
"description": "Create one direct Haaland/JTS meme concept with an original profile icon asset.",
"skills": [
{
"source": "./skills/haaland",
"entry": true
}
],
"steps": [
{
"id": "finish",
"title": "Create one Haaland meme",
"skill": "./skills/haaland"
}
]
}
{
"schemaVersion": "0.1",
"workflow": "haaland",
"workflowVersion": "0.1.0",
"generatedAt": "2026-07-08T14:15:19.041Z",
"skills": [
{
"source": "./skills/haaland",
"resolvedName": "haaland",
"kind": "local",
"hash": "sha256:d10bf16eca98054b3a23bbe0aac21ccb00e7f904c5f3b1c3480bb1009c575583"
}
]
}
# Product Manager GetSuperpower
Use this workflow when an agent should act as a startup product manager: discover
the real user problem, write a PRD, and slice the work into delivery-ready
issues.
Install it from the repo root:
```bash
bun run dev -- install examples/workflows/product-manager
```
Validate it while authoring:
```bash
bun run dev -- validate examples/workflows/product-manager
```
Refresh the checked skill fingerprints after editing the local role skill:
```bash
bun run dev -- lock examples/workflows/product-manager
```
---
name: product-manager
description: "Use when acting as a startup product manager for discovery, PRDs, issue slicing, roadmap tradeoffs, or customer-value sequencing."
---
# Product Manager
Use this role when the user needs product judgment before implementation. Your
job is to connect customer pain, product scope, and delivery sequence.
## Required Companion Skills
- `superpowers:brainstorming` for exploring options before locking scope.
- `mattpocock:to-prd` for producing a PRD.
- `mattpocock:to-issues` for slicing the PRD into issues.
- `superpowers:writing-plans` for executable delivery planning.
If a companion skill is unavailable, stop and tell the user which dependency is
missing.
## Operating Mode
1. Name the target user, pain, and desired behavior change.
2. Distinguish must-have product behavior from launch polish.
3. Write acceptance criteria that engineering and QA can verify.
4. Slice issues so each one creates visible product progress.
5. Keep the roadmap honest about learning risk and delivery risk.
{
"schemaVersion": "0.1",
"name": "product-manager",
"version": "0.1.0",
"description": "Act as the startup product manager for discovery, PRDs, issue slicing, and customer-value sequencing.",
"skills": [
{
"source": "./skills/product-manager",
"entry": true
},
{
"source": "superpowers:brainstorming",
"repo": "obra/superpowers"
},
{
"source": "mattpocock:to-prd",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:to-issues",
"repo": "mattpocock/skills"
},
{
"source": "superpowers:writing-plans",
"repo": "obra/superpowers"
}
],
"steps": [
{
"id": "pm-brief",
"title": "Frame the product problem",
"skill": "./skills/product-manager",
"gate": "human_approval"
},
{
"id": "brainstorm",
"title": "Explore product options",
"skill": "superpowers:brainstorming"
},
{
"id": "prd",
"title": "Write the PRD",
"skill": "mattpocock:to-prd"
},
{
"id": "issues",
"title": "Slice the work into issues",
"skill": "mattpocock:to-issues"
},
{
"id": "plan",
"title": "Write the delivery plan",
"skill": "superpowers:writing-plans"
}
]
}
{
"schemaVersion": "0.1",
"workflow": "product-manager",
"workflowVersion": "0.1.0",
"generatedAt": "2026-07-07T12:15:33.921Z",
"skills": [
{
"source": "./skills/product-manager",
"resolvedName": "product-manager",
"kind": "local",
"hash": "sha256:c0c7094ce1e2d9c614bd9939d9a379f488d809b0316d568017f584263f1eab8f"
},
{
"source": "superpowers:brainstorming",
"resolvedName": "superpowers-brainstorming",
"kind": "external",
"repo": "obra/superpowers",
"hash": "sha256:9d82390ff4b859cc1c9a3a4bcc9a548dc60587ebebbf44a1df8b48fff5d5020f"
},
{
"source": "mattpocock:to-prd",
"resolvedName": "to-prd",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:f5a4c297fd4a5fa0370a1ec2a92acd55f1c8f2647982cedf5b359ecdcc61d0f4"
},
{
"source": "mattpocock:to-issues",
"resolvedName": "to-issues",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:a8f15faddfb49ce73ae581ed91447285037de158bcc1911e02c5a7b9c1d004e3"
},
{
"source": "superpowers:writing-plans",
"resolvedName": "superpowers-writing-plans",
"kind": "external",
"repo": "obra/superpowers",
"hash": "sha256:8a8ea7693023742a048a23a7da0255031bc13ee03c80c1f01d400cab99fbbd84"
}
]
}
# QA Lead GetSuperpower
Use this workflow when an agent should act as a startup QA lead: identify release
risk, sharpen acceptance checks, and verify the work before completion.
Install it from the repo root:
```bash
bun run dev -- install examples/workflows/qa-lead
```
Validate it while authoring:
```bash
bun run dev -- validate examples/workflows/qa-lead
```
Refresh the checked skill fingerprints after editing the local role skill:
```bash
bun run dev -- lock examples/workflows/qa-lead
```
---
name: qa-lead
description: "Use when acting as a startup QA lead for acceptance checks, release risk, regression focus, and verification evidence."
---
# QA Lead
Use this role when the user needs a release-risk lens. Your job is to prove what
works, expose what is untested, and keep verification connected to real user
behavior.
## Required Companion Skills
- `mattpocock:review` for review posture and risk ordering.
- `mattpocock:diagnosing-bugs` for reproducible failure analysis.
- `superpowers:verification-before-completion` before claiming acceptance.
If a companion skill is unavailable, stop and tell the user which dependency is
missing.
## Operating Mode
1. Restate the user-facing behavior that must be true.
2. Identify the highest-risk regression paths.
3. Prefer concrete reproduction steps and command output over broad assurance.
4. Separate verified facts from residual risk.
5. Block release only on risks that can plausibly hurt the user or business.
{
"schemaVersion": "0.1",
"name": "qa-lead",
"version": "0.1.0",
"description": "Act as the startup QA lead for release risk, acceptance checks, regression focus, and verification evidence.",
"skills": [
{
"source": "./skills/qa-lead",
"entry": true
},
{
"source": "mattpocock:review",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:diagnosing-bugs",
"repo": "mattpocock/skills"
},
{
"source": "superpowers:verification-before-completion",
"repo": "obra/superpowers"
}
],
"steps": [
{
"id": "qa-brief",
"title": "Set the release-risk frame",
"skill": "./skills/qa-lead",
"gate": "human_approval"
},
{
"id": "review",
"title": "Review acceptance and risk",
"skill": "mattpocock:review"
},
{
"id": "debug",
"title": "Diagnose failures and reproduction gaps",
"skill": "mattpocock:diagnosing-bugs"
},
{
"id": "verify",
"title": "Verify before completion",
"skill": "superpowers:verification-before-completion"
}
]
}
{
"schemaVersion": "0.1",
"workflow": "qa-lead",
"workflowVersion": "0.1.0",
"generatedAt": "2026-07-07T12:15:34.149Z",
"skills": [
{
"source": "./skills/qa-lead",
"resolvedName": "qa-lead",
"kind": "local",
"hash": "sha256:17b5f20fa744bdbc0791717b5705e8be940b7cbdfaf4d5604e9d6a6a19124a53"
},
{
"source": "mattpocock:review",
"resolvedName": "review",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:f31dad85ccfc89efe4ccc5353d3b67aad00f87700c1d709eba8c758ef64757dd"
},
{
"source": "mattpocock:diagnosing-bugs",
"resolvedName": "diagnosing-bugs",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:0fe0adf2995715573a8efc68d47e9fd6f5c56230aaad6f148821feb883a34083"
},
{
"source": "superpowers:verification-before-completion",
"resolvedName": "superpowers:verification-before-completion",
"kind": "external",
"repo": "obra/superpowers",
"hash": "sha256:c0cd676b75a7eed20cc419b3c5fe46abbac00fd28cd87b6617fbc3607b1d3fd8"
}
]
}
# Startup Goal GetSuperpower
Use this workflow when one agent session needs a realistic startup operating
bench organized around a goal. It installs role entry skills for CEO, CTO,
product manager, engineering manager, founding engineer, and QA lead, plus the
companion Superpowers and Matt Pocock skills those roles expect.
Install it from the repo root:
```bash
bun run dev -- install examples/workflows/startup-goal
```
Validate it while authoring:
```bash
bun run dev -- validate examples/workflows/startup-goal
```
Refresh the checked skill fingerprints after editing the bundled role skills:
```bash
bun run dev -- lock examples/workflows/startup-goal
```
---
name: ceo
description: "Startup-team CEO role for company direction, tradeoffs, and strategic decisions."
---
# CEO
Use this role for company direction, market/customer tradeoffs, fundraising
framing, and irreversible startup decisions.
Required companion skills: `mattpocock:decision-mapping`,
`mattpocock:grill-with-docs`.
If a companion skill is unavailable, stop and name the missing dependency.
---
name: cto
description: "Startup-team CTO role for architecture, technical risk, and engineering strategy."
---
# CTO
Use this role for architecture boundaries, platform choices, technical risk, and
engineering strategy.
Required companion skills: `mattpocock:codebase-design`,
`mattpocock:domain-modeling`, `mattpocock:diagnosing-bugs`,
`mattpocock:review`.
If a companion skill is unavailable, stop and name the missing dependency.
---
name: engineering-manager
description: "Startup-team engineering manager role for delivery sequencing and quality gates."
---
# Engineering Manager
Use this role for delivery planning, execution risk, sequencing, and practical
quality gates.
Required companion skills: `superpowers:writing-plans`, `mattpocock:tdd`,
`mattpocock:diagnosing-bugs`, `mattpocock:review`.
If a companion skill is unavailable, stop and name the missing dependency.
---
name: founding-engineer
description: "Startup-team founding engineer role for implementation, tests, debugging, review, and verification."
---
# Founding Engineer
Use this role for implementation. Keep the change small, test close to behavior,
debug from evidence, and verify before handoff.
Required companion skills: `implement`, `mattpocock:tdd`,
`mattpocock:diagnosing-bugs`, `mattpocock:review`,
`superpowers:verification-before-completion`.
If a companion skill is unavailable, stop and name the missing dependency.
---
name: product-manager
description: "Startup-team product manager role for discovery, PRDs, and issue slicing."
---
# Product Manager
Use this role for customer-value framing, scope decisions, PRDs, acceptance
criteria, and issue slicing.
Required companion skills: `superpowers:brainstorming`, `mattpocock:to-prd`,
`mattpocock:to-issues`, `superpowers:writing-plans`.
If a companion skill is unavailable, stop and name the missing dependency.
---
name: qa-lead
description: "Startup-team QA lead role for acceptance checks, regression risk, and release verification."
---
# QA Lead
Use this role for acceptance checks, regression focus, release risk, and
verification evidence.
Required companion skills: `mattpocock:review`, `mattpocock:diagnosing-bugs`,
`superpowers:verification-before-completion`.
If a companion skill is unavailable, stop and name the missing dependency.
---
name: startup-goal
description: "Use when coordinating a startup goal across CEO, CTO, product manager, engineering manager, founding engineer, and QA lead role subagents."
---
# Startup Goal
Use this role when the user wants to move a startup goal through a realistic
operating workflow rather than ask one specialist. Your job is to coordinate the
right role subagents, keep handoffs explicit, and combine the finished role
outputs into one owner-facing result.
## Bundled Roles
- `ceo` for company direction and tradeoffs.
- `product-manager` for customer value, PRDs, and issue slicing.
- `cto` for architecture and technical risk.
- `engineering-manager` for execution sequencing and quality gates.
- `founding-engineer` for implementation.
- `qa-lead` for acceptance and release verification.
## Operating Mode
1. Clarify the startup goal and decide which roles are needed.
2. Dispatch a separate role-scoped subagent for each needed role.
3. Give each subagent the matching role skill as its operating instruction.
4. Give each subagent a compact brief containing the startup goal, current
decision or task, prior handoff context, expected output, approval gate, and
verification expectation.
5. Wait for all dispatched role subagents to finish.
6. Combine the role outputs into one owner-facing decision log.
7. Name which role is accountable for each decision.
8. Recommend the next action from the combined result.
9. Stop at human approval gates before advancing to the next role or phase.
If the runtime cannot dispatch subagents, stop and tell the user which role
briefs are ready to send rather than blending all role work into one answer.
{
"schemaVersion": "0.1",
"name": "startup-goal",
"version": "0.1.0",
"description": "Install the core startup operating roles as one goal-oriented GetSuperpower: CEO, CTO, product manager, engineering manager, founding engineer, and QA lead.",
"skills": [
{
"source": "./skills/startup-goal",
"entry": true
},
{
"source": "./skills/ceo"
},
{
"source": "./skills/cto"
},
{
"source": "./skills/product-manager"
},
{
"source": "./skills/engineering-manager"
},
{
"source": "./skills/founding-engineer"
},
{
"source": "./skills/qa-lead"
},
{
"source": "superpowers:brainstorming",
"repo": "obra/superpowers"
},
{
"source": "superpowers:writing-plans",
"repo": "obra/superpowers"
},
{
"source": "superpowers:verification-before-completion",
"repo": "obra/superpowers"
},
{
"source": "mattpocock:decision-mapping",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:grill-with-docs",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:to-prd",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:to-issues",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:codebase-design",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:domain-modeling",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:tdd",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:diagnosing-bugs",
"repo": "mattpocock/skills"
},
{
"source": "mattpocock:review",
"repo": "mattpocock/skills"
},
{
"source": "implement"
}
],
"steps": [
{
"id": "route",
"title": "Route the startup goal",
"skill": "./skills/startup-goal",
"gate": "human_approval"
},
{
"id": "strategy",
"title": "Clarify company direction",
"skill": "./skills/ceo",
"gate": "human_approval"
},
{
"id": "product",
"title": "Shape product scope",
"skill": "./skills/product-manager"
},
{
"id": "technology",
"title": "Set technical direction",
"skill": "./skills/cto"
},
{
"id": "delivery",
"title": "Plan engineering execution",
"skill": "./skills/engineering-manager"
},
{
"id": "implementation",
"title": "Implement the slice",
"skill": "./skills/founding-engineer"
},
{
"id": "qa",
"title": "Verify release readiness",
"skill": "./skills/qa-lead"
}
]
}
{
"schemaVersion": "0.1",
"workflow": "startup-goal",
"workflowVersion": "0.1.0",
"generatedAt": "2026-07-08T10:10:04.181Z",
"skills": [
{
"source": "./skills/startup-goal",
"resolvedName": "startup-goal",
"kind": "local",
"hash": "sha256:255ed6ff48f4072507df7c2e804711bfb10bb9e816b5598d6f0c0f51a7aee600"
},
{
"source": "./skills/ceo",
"resolvedName": "ceo",
"kind": "local",
"hash": "sha256:1c20329a35a67c75a1279d59aa69551701cdab2df703c7682ec977c97e29f528"
},
{
"source": "./skills/cto",
"resolvedName": "cto",
"kind": "local",
"hash": "sha256:5b95da7970f4cf9f240139b3d898b17f09948525b0428954a32c18763e400d26"
},
{
"source": "./skills/product-manager",
"resolvedName": "product-manager",
"kind": "local",
"hash": "sha256:6222bc1a42109c77b87ce8dc52410bc17ed776c967cc79c919ea087b2fa6e4bf"
},
{
"source": "./skills/engineering-manager",
"resolvedName": "engineering-manager",
"kind": "local",
"hash": "sha256:e41617993cb66fc44e0bc3cba8835522b17a39a41c8dcd1dc129b6d292bf2fce"
},
{
"source": "./skills/founding-engineer",
"resolvedName": "founding-engineer",
"kind": "local",
"hash": "sha256:fa5746026c1f2b8685cbb77bbf7bf9bdd9d27f0d08bbb2a37b7370253ba5603f"
},
{
"source": "./skills/qa-lead",
"resolvedName": "qa-lead",
"kind": "local",
"hash": "sha256:04c18eae220e8ab77f07ff602ae008f40e60df73ec101333fa6a4488ce10347d"
},
{
"source": "superpowers:brainstorming",
"resolvedName": "superpowers-brainstorming",
"kind": "external",
"repo": "obra/superpowers",
"hash": "sha256:9d82390ff4b859cc1c9a3a4bcc9a548dc60587ebebbf44a1df8b48fff5d5020f"
},
{
"source": "superpowers:writing-plans",
"resolvedName": "superpowers-writing-plans",
"kind": "external",
"repo": "obra/superpowers",
"hash": "sha256:8a8ea7693023742a048a23a7da0255031bc13ee03c80c1f01d400cab99fbbd84"
},
{
"source": "superpowers:verification-before-completion",
"resolvedName": "superpowers-verification-before-completion",
"kind": "external",
"repo": "obra/superpowers",
"hash": "sha256:c0cd676b75a7eed20cc419b3c5fe46abbac00fd28cd87b6617fbc3607b1d3fd8"
},
{
"source": "mattpocock:decision-mapping",
"resolvedName": "decision-mapping",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:4f85dd52dfcf4b6a11c404fbcb461cf454a734ceb084084e4272020c7a66962e"
},
{
"source": "mattpocock:grill-with-docs",
"resolvedName": "grill-with-docs",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:69d15d68b241680fbccaccd86cb986bd4da2589428d5e11b1fc97e84b2956bf4"
},
{
"source": "mattpocock:to-prd",
"resolvedName": "to-prd",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:f5a4c297fd4a5fa0370a1ec2a92acd55f1c8f2647982cedf5b359ecdcc61d0f4"
},
{
"source": "mattpocock:to-issues",
"resolvedName": "to-issues",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:a8f15faddfb49ce73ae581ed91447285037de158bcc1911e02c5a7b9c1d004e3"
},
{
"source": "mattpocock:codebase-design",
"resolvedName": "codebase-design",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:d921291ee4c38b537d2cad94205e6c983798644777ed0aeac0c44d2d6e3adbcd"
},
{
"source": "mattpocock:domain-modeling",
"resolvedName": "domain-modeling",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:41c64419a43153fd1aaca8be36f03c5a6e8cbf484d53e40a88fbb1ae0e6d3ace"
},
{
"source": "mattpocock:tdd",
"resolvedName": "tdd",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:1fd1eb7a880e3e52debaa8e3b9248b7b71816e2a38f519974e22674cf37b82b9"
},
{
"source": "mattpocock:diagnosing-bugs",
"resolvedName": "diagnosing-bugs",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:0fe0adf2995715573a8efc68d47e9fd6f5c56230aaad6f148821feb883a34083"
},
{
"source": "mattpocock:review",
"resolvedName": "review",
"kind": "external",
"repo": "mattpocock/skills",
"hash": "sha256:f31dad85ccfc89efe4ccc5353d3b67aad00f87700c1d709eba8c758ef64757dd"
},
{
"source": "implement",
"resolvedName": "implement",
"kind": "external",
"hash": "sha256:6c84d6b8b3e4e2624d5009841023404b9aec0ac06134f3d2c2aff473b25f259d"
}
]
}
export * from "./instruction-context";
export * from "./snapshots";
export * from "./workflow-bundles";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { constants } from "node:fs";
import { access, readdir, readFile, stat } from "node:fs/promises";
import { join, relative, resolve, sep } from "node:path";
export type InstructionContextMode = "opt_in";
export type InstructionContextFileStatus = "captured" | "missing" | "unreadable";
export interface InstructionContextFile {
path: string;
status: InstructionContextFileStatus;
sha256?: string | undefined;
bytes?: number | undefined;
}
export interface InstructionContextSkill {
name: string;
status: InstructionContextFileStatus;
path?: string | undefined;
version_or_sha256?: string | undefined;
}
export interface InstructionContextGit {
branch?: string | undefined;
commit?: string | undefined;
dirty?: boolean | undefined;
}
export interface InstructionContext {
mode: InstructionContextMode;
captured_at: string;
session_id_hash: string;
git: InstructionContextGit;
files: InstructionContextFile[];
skills: InstructionContextSkill[];
warnings: string[];
}
export interface CaptureInstructionContextInput {
rootDir: string;
sessionId: string;
timestampUtc?: string | undefined;
git?: InstructionContextGit | undefined;
}
export interface ShouldCaptureInstructionContextInput {
instructionContext?: boolean | undefined;
env?: Record<string, string | undefined> | undefined;
}
const rootInstructionFiles = ["AGENTS.md", "CLAUDE.md"];
const singleInstructionFiles = [".github/copilot-instructions.md"];
const globInstructionDirs = [".cursor/rules"];
const skillMetadataDirs = ["bundled-skills", ".codex/skills", ".agents/skills", ".claude/skills"];
export function shouldCaptureInstructionContext(
input: ShouldCaptureInstructionContextInput,
): boolean {
if (input.instructionContext !== undefined) {
return input.instructionContext;
}
const value = input.env?.PONYTRAIL_INSTRUCTION_CONTEXT;
return value === "1" || value === "true" || value === "yes";
}
export async function captureInstructionContext(
input: CaptureInstructionContextInput,
): Promise<InstructionContext> {
const rootDir = resolve(input.rootDir);
const warnings: string[] = [];
const files = await captureInstructionFiles(rootDir, warnings);
const skills = await captureSkillMetadata(rootDir, warnings);
return {
mode: "opt_in",
captured_at: input.timestampUtc ?? new Date().toISOString(),
session_id_hash: hashString(input.sessionId),
git: input.git ?? readGitContext(rootDir),
files,
skills,
warnings,
};
}
async function captureInstructionFiles(
rootDir: string,
warnings: string[],
): Promise<InstructionContextFile[]> {
const candidates = [
...rootInstructionFiles,
...singleInstructionFiles,
...(await discoverFilesUnderDirs(rootDir, globInstructionDirs, warnings)),
];
const unique = Array.from(new Set(candidates)).sort();
return Promise.all(unique.map((path) => captureFile(rootDir, path, warnings)));
}
async function captureSkillMetadata(
rootDir: string,
warnings: string[],
): Promise<InstructionContextSkill[]> {
const paths = await discoverSkillMetadataPaths(rootDir, warnings);
const entries = await Promise.all(
paths.map(async (path): Promise<InstructionContextSkill> => {
const file = await captureFile(rootDir, path, warnings);
return {
name: skillNameFromPath(path),
status: file.status,
path,
version_or_sha256: file.sha256,
};
}),
);
return entries.sort((left, right) => (left.path ?? "").localeCompare(right.path ?? ""));
}
async function discoverFilesUnderDirs(
rootDir: string,
dirs: string[],
warnings: string[],
): Promise<string[]> {
const paths: string[] = [];
for (const dir of dirs) {
await collectFiles(rootDir, dir, paths, warnings);
}
return paths;
}
async function discoverSkillMetadataPaths(rootDir: string, warnings: string[]): Promise<string[]> {
const paths: string[] = [];
for (const dir of skillMetadataDirs) {
await collectFiles(rootDir, dir, paths, warnings, isSkillMetadataPath);
}
return Array.from(new Set(paths)).sort();
}
async function collectFiles(
rootDir: string,
relativeDir: string,
paths: string[],
warnings: string[],
predicate: (path: string) => boolean = () => true,
): Promise<void> {
const absoluteDir = resolve(rootDir, relativeDir);
assertInside(absoluteDir, rootDir, relativeDir);
let entries: Array<{
name: string;
isDirectory(): boolean;
isFile(): boolean;
}>;
try {
entries = await readdir(absoluteDir, { withFileTypes: true });
} catch (error) {
if (isMissingFileError(error)) {
return;
}
warnings.push(`${relativeDir} unreadable`);
return;
}
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
const absolutePath = join(absoluteDir, entry.name);
const relativePath = toRelativePath(rootDir, absolutePath);
if (entry.isDirectory()) {
await collectFiles(rootDir, relativePath, paths, warnings, predicate);
continue;
}
if (entry.isFile() && predicate(relativePath)) {
paths.push(relativePath);
}
}
}
async function captureFile(
rootDir: string,
path: string,
warnings: string[],
): Promise<InstructionContextFile> {
const absolutePath = resolve(rootDir, path);
assertInside(absolutePath, rootDir, path);
try {
await access(absolutePath, constants.R_OK);
const fileStat = await stat(absolutePath);
if (!fileStat.isFile()) {
warnings.push(`${path} unreadable`);
return { path, status: "unreadable" };
}
const content = await readFile(absolutePath);
return {
path,
status: "captured",
sha256: hashBuffer(content),
bytes: content.byteLength,
};
} catch (error) {
if (isMissingFileError(error)) {
return { path, status: "missing" };
}
warnings.push(`${path} unreadable`);
return { path, status: "unreadable" };
}
}
function readGitContext(rootDir: string): InstructionContextGit {
const branch = runGit(rootDir, ["branch", "--show-current"]);
const commit = runGit(rootDir, ["rev-parse", "--short", "HEAD"]);
const status = runGit(rootDir, ["status", "--porcelain"]);
return {
branch: branch || undefined,
commit: commit || undefined,
dirty: status === undefined ? undefined : status.length > 0,
};
}
function runGit(rootDir: string, args: string[]): string | undefined {
const result = spawnSync("git", args, { cwd: rootDir, env: withoutInheritedGitEnv() });
if (result.status !== 0) {
return undefined;
}
return result.stdout.toString("utf8").trim();
}
function withoutInheritedGitEnv(): Record<string, string | undefined> {
const env = { ...process.env };
for (const key of Object.keys(env)) {
if (key.startsWith("GIT_")) {
delete env[key];
}
}
return env;
}
function isSkillMetadataPath(path: string): boolean {
return path.endsWith("/SKILL.md") || path.endsWith(".yaml") || path.endsWith(".yml");
}
function skillNameFromPath(path: string): string {
const parts = path.split("/");
const skillIndex = parts.findIndex((part) => part === "skills" || part === "bundled-skills");
const skillName = parts[skillIndex + 1];
if (skillIndex >= 0 && skillName) {
return skillName;
}
return path;
}
function hashString(value: string): string {
return hashBuffer(Buffer.from(value, "utf8"));
}
function hashBuffer(value: Buffer): string {
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
}
function toRelativePath(rootDir: string, absolutePath: string): string {
return relative(rootDir, absolutePath).split(sep).join("/");
}
function assertInside(path: string, rootDir: string, label: string): void {
const normalizedRoot = resolve(rootDir);
if (path !== normalizedRoot && !path.startsWith(`${normalizedRoot}${sep}`)) {
throw new Error(`Instruction context path escapes workspace: ${label}`);
}
}
function isMissingFileError(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
(error as { code?: unknown }).code === "ENOENT"
);
}
import { randomBytes } from "node:crypto";
import { constants } from "node:fs";
import { access, appendFile, cp, mkdir, readFile, rm } from "node:fs/promises";
import { dirname, join, resolve, sep } from "node:path";
import {
captureInstructionContext,
type InstructionContext,
type InstructionContextFile,
type InstructionContextGit,
type InstructionContextSkill,
shouldCaptureInstructionContext,
} from "./instruction-context";
const snapshotStoreDir = ".getsuperpower";
const snapshotLogFile = "snapshots.jsonl";
export interface SnapshotHistoryInput {
rootDir: string;
sessionId?: string | undefined;
}
export interface SnapshotFileState {
path: string;
exists: boolean;
type?: string | undefined;
stored_copy?: string | undefined;
sha256?: string | undefined;
}
export interface SnapshotLogEntry {
snapshot_id: string;
session_id: string;
phase: "pre" | "post";
timestamp_utc: string;
action?: string | null | undefined;
purpose?: string | null | undefined;
reason?: string | null | undefined;
expected?: string | null | undefined;
verify?: string | null | undefined;
rollback?: string | null | undefined;
summary?: string | null | undefined;
checks?: string | null | undefined;
result?: string | null | undefined;
files: SnapshotFileState[];
instruction_context?: InstructionContext | undefined;
}
export interface SnapshotHistory {
logPath: string;
sessions: SnapshotSession[];
}
export interface SnapshotSession {
sessionId: string;
commits: SnapshotCommit[];
}
export interface SnapshotCommit {
snapshotId: string;
sessionId: string;
timestampUtc: string;
hasPre: boolean;
hasPost: boolean;
action?: string | undefined;
summary?: string | undefined;
checks?: string | undefined;
result?: string | undefined;
rollback?: string | undefined;
files: SnapshotFileState[];
instructionContexts: {
pre?: InstructionContext | undefined;
post?: InstructionContext | undefined;
};
}
export type SnapshotRevertAction =
| {
type: "restore";
path: string;
source: string;
}
| {
type: "delete";
path: string;
};
export interface SnapshotRevertPlan {
rootDir: string;
snapshotId: string;
actions: SnapshotRevertAction[];
}
export interface RecordedSnapshotCommit {
snapshotId: string;
sessionId: string;
logPath: string;
treePath: string;
}
export interface RecordSnapshotPreInput {
rootDir: string;
sessionId: string;
snapshotId?: string | undefined;
idPrefix?: string | undefined;
timestampUtc?: string | undefined;
action: string;
purpose: string;
reason: string;
expected: string;
verify: string;
rollback: string;
files?: SnapshotFileState[] | undefined;
instructionContext?: boolean | undefined;
}
export interface RecordSnapshotPostInput {
rootDir: string;
sessionId: string;
snapshotId: string;
timestampUtc?: string | undefined;
summary: string;
checks: string;
result: string;
files?: SnapshotFileState[] | undefined;
instructionContext?: boolean | undefined;
}
interface SnapshotPair {
pre?: SnapshotLogEntry;
post?: SnapshotLogEntry;
}
export async function recordSnapshotPre(
input: RecordSnapshotPreInput,
): Promise<RecordedSnapshotCommit> {
const rootDir = resolve(input.rootDir);
const timestampUtc = input.timestampUtc ?? new Date().toISOString();
const snapshotId =
input.snapshotId ?? createSnapshotId(input.idPrefix ?? "snapshot", timestampUtc);
const entry: SnapshotLogEntry = {
snapshot_id: snapshotId,
session_id: input.sessionId,
phase: "pre",
timestamp_utc: timestampUtc,
action: input.action,
purpose: input.purpose,
reason: input.reason,
expected: input.expected,
verify: input.verify,
rollback: input.rollback,
files: input.files ?? [],
};
if (
shouldCaptureInstructionContext({
instructionContext: input.instructionContext,
env: process.env,
})
) {
entry.instruction_context = await captureInstructionContext({
rootDir,
sessionId: input.sessionId,
timestampUtc,
});
}
return appendSnapshotEntry(rootDir, entry);
}
export async function recordSnapshotPost(
input: RecordSnapshotPostInput,
): Promise<RecordedSnapshotCommit> {
const rootDir = resolve(input.rootDir);
const timestampUtc = input.timestampUtc ?? new Date().toISOString();
const entry: SnapshotLogEntry = {
snapshot_id: input.snapshotId,
session_id: input.sessionId,
phase: "post",
timestamp_utc: timestampUtc,
summary: input.summary,
checks: input.checks,
result: input.result,
files: input.files ?? [],
};
if (
shouldCaptureInstructionContext({
instructionContext: input.instructionContext,
env: process.env,
})
) {
entry.instruction_context = await captureInstructionContext({
rootDir,
sessionId: input.sessionId,
timestampUtc,
});
}
return appendSnapshotEntry(rootDir, entry);
}
export async function readSnapshotHistory(input: SnapshotHistoryInput): Promise<SnapshotHistory> {
const logPath = getSnapshotLogPath(input.rootDir);
const entries = await readSnapshotEntries(logPath);
const pairsBySession = new Map<string, Map<string, SnapshotPair>>();
for (const entry of entries) {
if (input.sessionId && entry.session_id !== input.sessionId) {
continue;
}
let sessionPairs = pairsBySession.get(entry.session_id);
if (!sessionPairs) {
sessionPairs = new Map();
pairsBySession.set(entry.session_id, sessionPairs);
}
const pair = sessionPairs.get(entry.snapshot_id) ?? {};
pair[entry.phase] = entry;
sessionPairs.set(entry.snapshot_id, pair);
}
const sessions = Array.from(pairsBySession.entries())
.map(([sessionId, pairs]) => ({
sessionId,
commits: Array.from(pairs.entries())
.map(([snapshotId, pair]) => toSnapshotCommit(sessionId, snapshotId, pair))
.sort((left, right) => left.timestampUtc.localeCompare(right.timestampUtc)),
}))
.sort((left, right) =>
(left.commits[0]?.timestampUtc ?? "").localeCompare(right.commits[0]?.timestampUtc ?? ""),
);
return { logPath, sessions };
}
export async function planSnapshotRevert(input: {
rootDir: string;
snapshotId: string;
}): Promise<SnapshotRevertPlan> {
const rootDir = resolve(input.rootDir);
const entries = await readSnapshotEntries(getSnapshotLogPath(rootDir));
const preEntry = entries.find(
(entry) => entry.snapshot_id === input.snapshotId && entry.phase === "pre",
);
if (!preEntry) {
throw new Error(`Unknown snapshot: ${input.snapshotId}`);
}
const storeDir = join(rootDir, snapshotStoreDir);
const actions: SnapshotRevertAction[] = [];
for (const file of preEntry.files) {
assertSafeRelativePath(rootDir, file.path);
if (!file.exists) {
actions.push({ type: "delete", path: file.path });
continue;
}
if (!file.stored_copy) {
throw new Error(`Missing stored pre snapshot copy for ${file.path}`);
}
const source = resolve(storeDir, file.stored_copy);
assertInside(source, storeDir, `Stored copy escapes snapshot store: ${file.stored_copy}`);
if (!(await pathExists(source))) {
throw new Error(`Missing stored pre snapshot copy for ${file.path}`);
}
actions.push({ type: "restore", path: file.path, source });
}
return {
rootDir,
snapshotId: input.snapshotId,
actions,
};
}
export async function applySnapshotRevert(plan: SnapshotRevertPlan): Promise<void> {
for (const action of plan.actions) {
const target = resolve(plan.rootDir, action.path);
assertInside(target, plan.rootDir, `Target path escapes workspace: ${action.path}`);
if (action.type === "restore") {
await mkdir(dirname(target), { recursive: true });
await cp(action.source, target);
continue;
}
await rm(target, { recursive: true, force: true });
}
}
function getSnapshotLogPath(rootDir: string): string {
return join(rootDir, snapshotStoreDir, snapshotLogFile);
}
async function appendSnapshotEntry(
rootDir: string,
entry: SnapshotLogEntry,
): Promise<RecordedSnapshotCommit> {
assertSafePathSegment(entry.session_id, "session id");
const logPath = getSnapshotLogPath(rootDir);
await mkdir(dirname(logPath), { recursive: true });
await appendFile(logPath, `${JSON.stringify(entry)}\n`);
const sessionDir = join(rootDir, snapshotStoreDir, "sessions", entry.session_id);
await mkdir(sessionDir, { recursive: true });
await appendFile(join(sessionDir, "commits.jsonl"), `${JSON.stringify(entry)}\n`);
const treePath = join(sessionDir, "tree.md");
if (!(await pathExists(treePath))) {
await appendFile(treePath, `# Ponytrail Session Tree\n\nSession: \`${entry.session_id}\`\n`);
}
await appendFile(treePath, formatSessionTreeEntry(entry));
return {
snapshotId: entry.snapshot_id,
sessionId: entry.session_id,
logPath,
treePath,
};
}
function formatSessionTreeEntry(entry: SnapshotLogEntry): string {
const lines = [
"",
`## commit ${entry.snapshot_id}`,
"",
`- phase: \`${entry.phase}\``,
`- time: \`${entry.timestamp_utc}\``,
];
appendTreeField(lines, "action", entry.action);
appendTreeField(lines, "purpose", entry.purpose);
appendTreeField(lines, "reason", entry.reason);
appendTreeField(lines, "expected", entry.expected);
appendTreeField(lines, "verify", entry.verify);
appendTreeField(lines, "rollback", entry.rollback);
appendTreeField(lines, "summary", entry.summary);
appendTreeField(lines, "checks", entry.checks);
appendTreeField(lines, "result", entry.result);
lines.push("- files:");
if (entry.files.length === 0) {
lines.push(" - none");
} else {
for (const file of entry.files) {
lines.push(formatSessionTreeFile(file));
}
}
if (entry.instruction_context) {
lines.push("- instruction_context:");
lines.push(formatInstructionContextTreeLine(entry.instruction_context));
}
return `${lines.join("\n")}\n`;
}
function appendTreeField(lines: string[], label: string, value: string | null | undefined): void {
if (value) {
lines.push(`- ${label}: ${value}`);
}
}
function formatSessionTreeFile(file: SnapshotFileState): string {
if (!file.exists) {
return ` - \`${file.path}\` missing`;
}
const parts = [` - \`${file.path}\` ${file.type ?? "file"}`];
if (file.sha256) {
parts.push(`sha256=\`${file.sha256}\``);
}
if (file.stored_copy) {
parts.push(`stored: \`${file.stored_copy}\``);
}
return parts.join(" ");
}
function formatInstructionContextTreeLine(context: InstructionContext): string {
const capturedFiles = context.files.filter((file) => file.status === "captured").length;
const capturedSkills = context.skills.filter((skill) => skill.status === "captured").length;
const warningText =
context.warnings.length === 0 ? "0 warnings" : `${context.warnings.length} warning(s)`;
const gitText = [
context.git.branch ? `branch=${context.git.branch}` : "",
context.git.commit ? `commit=${context.git.commit}` : "",
context.git.dirty === undefined ? "" : `dirty=${String(context.git.dirty)}`,
]
.filter(Boolean)
.join(" ");
return ` - mode=${context.mode} files=${capturedFiles} skills=${capturedSkills} ${warningText}${
gitText ? ` ${gitText}` : ""
}`;
}
function createSnapshotId(prefix: string, timestampUtc: string): string {
const compactTimestamp = timestampUtc.replace(/\D/g, "").slice(0, 14);
return `${prefix}-${compactTimestamp}Z-${randomBytes(4).toString("hex")}`;
}
async function readSnapshotEntries(logPath: string): Promise<SnapshotLogEntry[]> {
let rawLog: string;
try {
rawLog = await readFile(logPath, "utf8");
} catch (error) {
if (isMissingFileError(error)) {
return [];
}
throw error;
}
const entries: SnapshotLogEntry[] = [];
const lines = rawLog.split("\n");
for (const [index, line] of lines.entries()) {
if (!line.trim()) {
continue;
}
try {
entries.push(parseSnapshotEntry(JSON.parse(line), index + 1));
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`Malformed snapshot log JSON on line ${index + 1}: ${error.message}`);
}
throw error;
}
}
return entries;
}
function parseSnapshotEntry(value: unknown, lineNumber: number): SnapshotLogEntry {
if (!isRecord(value)) {
throw new Error(`Malformed snapshot log entry on line ${lineNumber}: expected object`);
}
const snapshotId = readRequiredString(value, "snapshot_id", lineNumber);
const sessionId = readRequiredString(value, "session_id", lineNumber);
const phase = readRequiredString(value, "phase", lineNumber);
if (phase !== "pre" && phase !== "post") {
throw new Error(`Malformed snapshot log entry on line ${lineNumber}: invalid phase`);
}
const files = value.files;
if (!Array.isArray(files)) {
throw new Error(`Malformed snapshot log entry on line ${lineNumber}: files must be an array`);
}
return {
snapshot_id: snapshotId,
session_id: sessionId,
phase,
timestamp_utc: readRequiredString(value, "timestamp_utc", lineNumber),
action: readOptionalString(value.action),
purpose: readOptionalString(value.purpose),
reason: readOptionalString(value.reason),
expected: readOptionalString(value.expected),
verify: readOptionalString(value.verify),
rollback: readOptionalString(value.rollback),
summary: readOptionalString(value.summary),
checks: readOptionalString(value.checks),
result: readOptionalString(value.result),
files: files.map((file, index) => parseSnapshotFile(file, lineNumber, index)),
instruction_context: parseInstructionContext(value.instruction_context, lineNumber),
};
}
function parseSnapshotFile(value: unknown, lineNumber: number, index: number): SnapshotFileState {
if (!isRecord(value)) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: file ${index} must be an object`,
);
}
const exists = value.exists;
if (typeof exists !== "boolean") {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: file ${index} exists must be boolean`,
);
}
return {
path: readRequiredString(value, "path", lineNumber),
exists,
type: readOptionalString(value.type),
stored_copy: readOptionalString(value.stored_copy),
sha256: readOptionalString(value.sha256),
};
}
function parseInstructionContext(
value: unknown,
lineNumber: number,
): InstructionContext | undefined {
if (value === undefined) {
return undefined;
}
if (!isRecord(value)) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: instruction_context must be an object`,
);
}
const mode = readRequiredString(value, "mode", lineNumber);
if (mode !== "opt_in") {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: instruction_context mode is invalid`,
);
}
const files = value.files;
const skills = value.skills;
const warnings = value.warnings;
if (!Array.isArray(files) || !Array.isArray(skills) || !Array.isArray(warnings)) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: instruction_context arrays are required`,
);
}
return {
mode,
captured_at: readRequiredString(value, "captured_at", lineNumber),
session_id_hash: readRequiredString(value, "session_id_hash", lineNumber),
git: parseInstructionContextGit(value.git, lineNumber),
files: files.map((file, index) => parseInstructionContextFile(file, lineNumber, index)),
skills: skills.map((skill, index) => parseInstructionContextSkill(skill, lineNumber, index)),
warnings: warnings.map((warning, index) =>
readRequiredArrayString(warning, "instruction_context.warnings", lineNumber, index),
),
};
}
function parseInstructionContextGit(value: unknown, lineNumber: number): InstructionContextGit {
if (!isRecord(value)) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: instruction_context.git must be an object`,
);
}
return {
branch: readOptionalString(value.branch),
commit: readOptionalString(value.commit),
dirty: typeof value.dirty === "boolean" ? value.dirty : undefined,
};
}
function parseInstructionContextFile(
value: unknown,
lineNumber: number,
index: number,
): InstructionContextFile {
if (!isRecord(value)) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: instruction_context.files ${index} must be an object`,
);
}
const status = readRequiredString(value, "status", lineNumber);
if (!isInstructionContextStatus(status)) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: instruction_context.files ${index} has invalid status`,
);
}
return {
path: readRequiredString(value, "path", lineNumber),
status,
sha256: readOptionalString(value.sha256),
bytes: typeof value.bytes === "number" ? value.bytes : undefined,
};
}
function parseInstructionContextSkill(
value: unknown,
lineNumber: number,
index: number,
): InstructionContextSkill {
if (!isRecord(value)) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: instruction_context.skills ${index} must be an object`,
);
}
const status = readRequiredString(value, "status", lineNumber);
if (!isInstructionContextStatus(status)) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: instruction_context.skills ${index} has invalid status`,
);
}
return {
name: readRequiredString(value, "name", lineNumber),
status,
path: readOptionalString(value.path),
version_or_sha256: readOptionalString(value.version_or_sha256),
};
}
function isInstructionContextStatus(status: string): status is InstructionContextFile["status"] {
return status === "captured" || status === "missing" || status === "unreadable";
}
function toSnapshotCommit(
sessionId: string,
snapshotId: string,
pair: SnapshotPair,
): SnapshotCommit {
const pre = pair.pre;
const post = pair.post;
const filesByPath = new Map<string, SnapshotFileState>();
for (const file of [...(pre?.files ?? []), ...(post?.files ?? [])]) {
if (!filesByPath.has(file.path)) {
filesByPath.set(file.path, file);
}
}
return {
snapshotId,
sessionId,
timestampUtc: pre?.timestamp_utc ?? post?.timestamp_utc ?? "",
hasPre: Boolean(pre),
hasPost: Boolean(post),
action: pre?.action ?? undefined,
summary: post?.summary ?? undefined,
checks: post?.checks ?? undefined,
result: post?.result ?? undefined,
rollback: pre?.rollback ?? undefined,
files: Array.from(filesByPath.values()),
instructionContexts: {
pre: pre?.instruction_context,
post: post?.instruction_context,
},
};
}
function readRequiredString(
value: Record<string, unknown>,
key: string,
lineNumber: number,
): string {
const field = value[key];
if (typeof field !== "string" || !field) {
throw new Error(`Malformed snapshot log entry on line ${lineNumber}: ${key} is required`);
}
return field;
}
function readOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value ? value : undefined;
}
function readRequiredArrayString(
value: unknown,
field: string,
lineNumber: number,
index: number,
): string {
if (typeof value !== "string" || !value) {
throw new Error(
`Malformed snapshot log entry on line ${lineNumber}: ${field} ${index} is required`,
);
}
return value;
}
function assertSafeRelativePath(rootDir: string, path: string): void {
const target = resolve(rootDir, path);
assertInside(target, rootDir, `Target path escapes workspace: ${path}`);
}
function assertInside(path: string, rootDir: string, message: string): void {
const normalizedRoot = resolve(rootDir);
if (path !== normalizedRoot && !path.startsWith(`${normalizedRoot}${sep}`)) {
throw new Error(message);
}
}
function assertSafePathSegment(value: string, label: string): void {
if (!value || value === "." || value === ".." || value.includes("/") || value.includes("\\")) {
throw new Error(`Invalid ${label}: ${value}`);
}
}
async function pathExists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
return true;
} catch (error) {
if (isMissingFileError(error)) {
return false;
}
throw error;
}
}
function isMissingFileError(error: unknown): boolean {
return isRecord(error) && error.code === "ENOENT";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { z } from "zod";
import { runSubprocess } from "../../process";
export const workflowFileName = "workflow.json";
export const workflowLockFileName = "workflow.lock.json";
const workflowStoreDir = ".getsuperpower/workflows";
const canonicalExamplesGitUrl = "https://github.com/0xroylee/getsuperpower.git";
const canonicalExamplesWorkflowPath = "examples/workflows";
const workflowAliasPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const WorkflowSkillSchema = z.object({
source: z.string().min(1),
repo: z.string().min(1).optional(),
optional: z.boolean().optional(),
entry: z.boolean().optional(),
});
const WorkflowStepVerifySchema = z.object({
type: z.enum(["human_approval", "event", "manual"]),
event: z.string().min(1).optional(),
message_includes: z.string().min(1).optional(),
description: z.string().min(1).optional(),
});
const WorkflowStepSchema = z.object({
id: z.string().min(1),
title: z.string().min(1),
skill: z.string().min(1),
gate: z.enum(["human_approval"]).optional(),
instruction: z.string().min(1).optional(),
verify: WorkflowStepVerifySchema.optional(),
});
const WorkflowLoopSchema = z.object({
script: z.string().min(1),
state: z.literal("global"),
execution: z.literal("action-only"),
type: z.enum(["goal_based"]).optional(),
goal: z.string().min(1).optional(),
done_when: z.array(z.string().min(1)).min(1).optional(),
stop_when: z.array(z.string().min(1)).min(1).optional(),
});
export const WorkflowBundleManifestSchema = z
.object({
schemaVersion: z.literal("0.1"),
name: z.string().min(1),
version: z.string().min(1),
description: z.string().min(1),
loop: WorkflowLoopSchema.optional(),
skills: z.array(WorkflowSkillSchema).min(1),
steps: z.array(WorkflowStepSchema).min(1),
})
.superRefine((manifest, context) => {
const stepIds = new Set<string>();
for (const [index, step] of manifest.steps.entries()) {
if (stepIds.has(step.id)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate workflow step id: ${step.id}`,
path: ["steps", index, "id"],
});
}
stepIds.add(step.id);
}
const skillSources = new Set(manifest.skills.map((skill) => skill.source));
for (const [index, step] of manifest.steps.entries()) {
if (!skillSources.has(step.skill)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Workflow step ${step.id} references unknown skill: ${step.skill}`,
path: ["steps", index, "skill"],
});
}
}
const entrySkillIndexes = manifest.skills
.map((skill, index) => (skill.entry === true ? index : -1))
.filter((index) => index >= 0);
if (entrySkillIndexes.length > 1) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "Only one workflow skill can be marked as entry",
path: ["skills"],
});
}
if (manifest.loop && entrySkillIndexes.length !== 1) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "Looped workflows must declare exactly one entry skill",
path: ["skills"],
});
}
if (manifest.loop && entrySkillIndexes.length === 1) {
const entrySkillIndex = entrySkillIndexes[0] ?? 0;
const entrySkill = manifest.skills[entrySkillIndex];
if (entrySkill && !isLocalWorkflowSkillSource(entrySkill.source)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "Looped workflow entry skill must be a local skill path",
path: ["skills", entrySkillIndex, "source"],
});
}
}
if (manifest.loop?.type === "goal_based") {
if (!manifest.loop.goal) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "Goal-based loops must declare loop.goal",
path: ["loop", "goal"],
});
}
if (!manifest.loop.done_when) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "Goal-based loops must declare loop.done_when",
path: ["loop", "done_when"],
});
}
if (!manifest.loop.stop_when) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "Goal-based loops must declare loop.stop_when",
path: ["loop", "stop_when"],
});
}
}
});
export type WorkflowBundleManifest = z.infer<typeof WorkflowBundleManifestSchema>;
const WorkflowSkillLockEntrySchema = z.object({
source: z.string().min(1),
resolvedName: z.string().min(1),
kind: z.enum(["local", "external"]),
repo: z.string().min(1).optional(),
hash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
});
export const WorkflowLockFileSchema = z.object({
schemaVersion: z.literal("0.1"),
workflow: z.string().min(1),
workflowVersion: z.string().min(1),
generatedAt: z.string().datetime(),
skills: z.array(WorkflowSkillLockEntrySchema).min(1),
});
export type WorkflowSkillLockEntry = z.infer<typeof WorkflowSkillLockEntrySchema>;
export type WorkflowLockFile = z.infer<typeof WorkflowLockFileSchema>;
export interface WorkflowGitCommand {
executable: string;
args: string[];
cwd: string;
env: Record<string, string | undefined>;
}
export interface WorkflowGitCommandResult {
stdout: string;
stderr: string;
exitCode: number;
}
export type WorkflowGitCommandRunner = (
command: WorkflowGitCommand,
) => Promise<WorkflowGitCommandResult>;
export type WorkflowBundleSource =
| {
kind: "local";
path: string;
}
| {
kind: "git";
url: string;
commit?: string;
subdirectory?: string;
};
export interface WorkflowBundle {
manifest: WorkflowBundleManifest;
sourceDir: string;
manifestPath: string;
lock?: WorkflowLockFile;
lockPath?: string;
source: WorkflowBundleSource;
cleanup?: () => Promise<void>;
}
export interface WorkflowBundleScaffold {
bundleDir: string;
manifestPath: string;
readmePath: string;
entrySkillPath: string;
}
export interface WorkflowSkillInstallDependency {
source: string;
repo?: string;
}
export interface WorkflowInstallSkillArtifact {
source: string;
skillName: string;
agent: string;
status: string;
paths: string[];
}
export interface WorkflowRemovalArtifact {
source: string;
skillName: string;
agent: string;
status: string;
path: string;
}
export interface WorkflowRemovalKeptArtifact extends WorkflowRemovalArtifact {
usedByWorkflows: string[];
}
export interface WorkflowRemovalSkippedArtifact {
source: string;
reason: string;
}
export interface WorkflowRemovalPlan {
workflow: InstalledWorkflowBundle;
workflowRecordPath: string;
legacy: boolean;
artifactsToRemove: WorkflowRemovalArtifact[];
artifactsToKeep: WorkflowRemovalKeptArtifact[];
skippedArtifacts: WorkflowRemovalSkippedArtifact[];
}
export interface WorkflowLoopMetadata {
schemaVersion: "0.1";
workflow: string;
entrySkill: string;
loopScript: string;
state: "global";
execution: "action-only";
type?: "goal_based";
goal?: string;
done_when?: string[];
stop_when?: string[];
commands: ["start", "status", "log", "advance", "summary"];
}
export interface PreparedWorkflowSkillInstallDependencies {
dependencies: WorkflowSkillInstallDependency[];
cleanup?: () => Promise<void>;
}
export interface InstalledWorkflowBundle extends WorkflowBundleManifest {
source: WorkflowBundleSource;
installArtifacts?: WorkflowInstallSkillArtifact[];
}
export interface WorkflowInstallResult {
workflow: InstalledWorkflowBundle;
path: string;
}
export async function loadWorkflowBundle(
source: string,
options: {
cwd?: string;
runGitCommand?: WorkflowGitCommandRunner;
tempDir?: string;
} = {},
): Promise<WorkflowBundle> {
const resolvedSource = await resolveWorkflowBundleSource(source, {
cwd: options.cwd ?? process.cwd(),
...(options.runGitCommand ? { runGitCommand: options.runGitCommand } : {}),
...(options.tempDir ? { tempDir: options.tempDir } : {}),
});
const manifestPath = resolvedSource.sourceDir.endsWith(workflowFileName)
? resolvedSource.sourceDir
: join(resolvedSource.sourceDir, workflowFileName);
const manifestDir = resolvedSource.sourceDir.endsWith(workflowFileName)
? dirname(resolvedSource.sourceDir)
: resolvedSource.sourceDir;
try {
const rawManifest = await readFile(manifestPath, "utf8");
const manifest = WorkflowBundleManifestSchema.parse(JSON.parse(rawManifest));
validateWorkflowBundleFiles({ manifest, sourceDir: manifestDir });
const loadedLock = await loadWorkflowLockFileForManifest({ manifest, sourceDir: manifestDir });
return {
manifest,
sourceDir: manifestDir,
manifestPath,
...(loadedLock ? { lock: loadedLock.lock, lockPath: loadedLock.path } : {}),
source: resolvedSource.source,
...(resolvedSource.cleanup ? { cleanup: resolvedSource.cleanup } : {}),
};
} catch (error) {
await resolvedSource.cleanup?.();
if (resolvedSource.alias && resolvedSource.source.kind === "git" && isMissingFileError(error)) {
throw new Error(
`GetSuperpower workflow alias not found: ${resolvedSource.alias}\nChecked: ${resolvedSource.source.url}`,
);
}
if (resolvedSource.source.kind === "git" && isMissingFileError(error)) {
throw new Error(
`No GetSuperpower workflow manifest was found at ${manifestPath} from public git source: ${resolvedSource.source.url}`,
);
}
throw error;
}
}
export async function loadWorkflowLockFile(
bundle: WorkflowBundle,
): Promise<WorkflowLockFile | null> {
return (
(
await loadWorkflowLockFileForManifest({
manifest: bundle.manifest,
sourceDir: bundle.sourceDir,
})
)?.lock ?? null
);
}
export async function createWorkflowLockFile(
bundle: WorkflowBundle,
options: { generatedAt?: Date | string } = {},
): Promise<WorkflowLockFile> {
const generatedAt =
typeof options.generatedAt === "string"
? options.generatedAt
: (options.generatedAt ?? new Date()).toISOString();
return WorkflowLockFileSchema.parse({
schemaVersion: "0.1",
workflow: bundle.manifest.name,
workflowVersion: bundle.manifest.version,
generatedAt,
skills: await Promise.all(
bundle.manifest.skills.map(async (skill) => {
const isLocal = isLocalWorkflowSkillSource(skill.source);
const entry = {
source: skill.source,
resolvedName: getWorkflowLockResolvedName(skill.source),
kind: isLocal ? "local" : "external",
...(skill.repo ? { repo: skill.repo } : {}),
hash: isLocal
? await hashLocalWorkflowSkill(resolve(bundle.sourceDir, skill.source))
: hashExternalWorkflowSkill(skill.source, skill.repo),
} satisfies WorkflowSkillLockEntry;
return entry;
}),
),
});
}
export async function writeWorkflowLockFile(
bundle: WorkflowBundle,
options: { generatedAt?: Date | string } = {},
): Promise<{ lock: WorkflowLockFile; path: string }> {
const lock = await createWorkflowLockFile(bundle, options);
const path = join(bundle.sourceDir, workflowLockFileName);
await writeFile(path, `${JSON.stringify(lock, null, 2)}\n`);
return { lock, path };
}
export async function createWorkflowBundleScaffold(input: {
rootDir: string;
name: string;
}): Promise<WorkflowBundleScaffold> {
const bundleDir = join(input.rootDir, input.name);
const entrySkillDir = join(bundleDir, "skills", input.name);
const localSkillDir = join(bundleDir, "skills", "custom-review");
const manifestPath = join(bundleDir, workflowFileName);
const readmePath = join(bundleDir, "README.md");
const entrySkillPath = join(entrySkillDir, "SKILL.md");
const manifest = createScaffoldManifest(input.name);
await mkdir(entrySkillDir, { recursive: true });
await mkdir(localSkillDir, { recursive: true });
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
await writeFile(
readmePath,
`# ${input.name}\n\nA GetSuperpower that composes reusable agent skills.\n`,
);
await writeFile(entrySkillPath, createScaffoldEntrySkill(input.name, manifest));
await writeFile(
join(localSkillDir, "SKILL.md"),
[
"---",
"name: custom-review",
'description: "Review this GetSuperpower from the author perspective."',
"---",
"",
"# Custom Review",
"",
"Check whether the workflow output matches the GetSuperpower author's stated outcome.",
"",
].join("\n"),
);
return { bundleDir, manifestPath, readmePath, entrySkillPath };
}
export async function installWorkflowBundle(input: {
rootDir: string;
bundle: WorkflowBundle;
installArtifacts?: WorkflowInstallSkillArtifact[];
}): Promise<WorkflowInstallResult> {
const workflow = createInstalledWorkflowBundle(input.bundle, input.installArtifacts ?? []);
const workflowDir = join(input.rootDir, workflowStoreDir);
const path = join(workflowDir, `${workflow.name}.json`);
await mkdir(workflowDir, { recursive: true });
await writeFile(path, `${JSON.stringify(workflow, null, 2)}\n`);
return { workflow, path };
}
export async function listInstalledWorkflowBundles(input: {
rootDir: string;
}): Promise<InstalledWorkflowBundle[]> {
const workflowDir = join(input.rootDir, workflowStoreDir);
if (!existsSync(workflowDir)) {
return [];
}
const entries = await readdir(workflowDir, { withFileTypes: true });
const workflows: InstalledWorkflowBundle[] = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".json")) {
continue;
}
const raw = await readFile(join(workflowDir, entry.name), "utf8");
workflows.push(JSON.parse(raw) as InstalledWorkflowBundle);
}
return workflows.sort((left, right) => left.name.localeCompare(right.name));
}
export function getInstalledWorkflowBundlePath(input: {
rootDir: string;
workflowName: string;
}): string {
return join(input.rootDir, workflowStoreDir, `${input.workflowName}.json`);
}
export async function loadInstalledWorkflowBundle(input: {
rootDir: string;
workflowName: string;
}): Promise<{ workflow: InstalledWorkflowBundle; path: string }> {
const path = getInstalledWorkflowBundlePath(input);
try {
const raw = await readFile(path, "utf8");
return { workflow: JSON.parse(raw) as InstalledWorkflowBundle, path };
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
throw new Error(`GetSuperpower is not installed: ${input.workflowName}`);
}
throw error;
}
}
export async function createWorkflowRemovalPlan(input: {
rootDir: string;
homeDir: string;
workflowName: string;
}): Promise<WorkflowRemovalPlan> {
const installed = await loadInstalledWorkflowBundle({
rootDir: input.rootDir,
workflowName: input.workflowName,
});
const otherWorkflows = (await listInstalledWorkflowBundles({ rootDir: input.rootDir })).filter(
(workflow) => workflow.name !== input.workflowName,
);
const legacy = !installed.workflow.installArtifacts?.length;
const skippedArtifacts: WorkflowRemovalSkippedArtifact[] = [];
const candidates = legacy
? inferLegacyRemovalArtifacts(installed.workflow, input.homeDir, skippedArtifacts)
: flattenInstallArtifacts(installed.workflow.installArtifacts ?? []);
const otherPathOwners = getArtifactPathOwners(otherWorkflows);
const artifactsToRemove: WorkflowRemovalArtifact[] = [];
const artifactsToKeep: WorkflowRemovalKeptArtifact[] = [];
const seen = new Set<string>();
for (const candidate of candidates) {
if (seen.has(candidate.path)) {
continue;
}
seen.add(candidate.path);
const usedByWorkflows = otherPathOwners.get(candidate.path) ?? [];
if (usedByWorkflows.length > 0) {
artifactsToKeep.push({ ...candidate, usedByWorkflows });
continue;
}
artifactsToRemove.push(candidate);
}
return {
workflow: installed.workflow,
workflowRecordPath: installed.path,
legacy,
artifactsToRemove,
artifactsToKeep,
skippedArtifacts,
};
}
export async function executeWorkflowRemovalPlan(plan: WorkflowRemovalPlan): Promise<void> {
for (const artifact of plan.artifactsToRemove) {
await rm(artifact.path, { recursive: true, force: true });
}
await rm(plan.workflowRecordPath, { force: true });
}
export function getWorkflowSkillInstallSources(bundle: WorkflowBundle): string[] {
return getWorkflowSkillInstallDependencies(bundle).map((skill) => skill.source);
}
export function getWorkflowSkillInstallDependencies(
bundle: WorkflowBundle,
): WorkflowSkillInstallDependency[] {
return bundle.manifest.skills.map((skill) => {
const source = isLocalWorkflowSkillSource(skill.source)
? resolve(bundle.sourceDir, skill.source)
: skill.source;
return {
source,
...(skill.repo ? { repo: skill.repo } : {}),
};
});
}
export function createWorkflowLoopMetadata(bundle: WorkflowBundle): WorkflowLoopMetadata | null {
if (!bundle.manifest.loop) {
return null;
}
const entrySkill = getWorkflowEntrySkill(bundle.manifest);
if (!entrySkill) {
throw new Error("Looped workflow entry skill could not be resolved");
}
return {
schemaVersion: "0.1",
workflow: bundle.manifest.name,
entrySkill: entrySkill.source,
loopScript: bundle.manifest.loop.script,
state: bundle.manifest.loop.state,
execution: bundle.manifest.loop.execution,
...(bundle.manifest.loop.type ? { type: bundle.manifest.loop.type } : {}),
...(bundle.manifest.loop.goal ? { goal: bundle.manifest.loop.goal } : {}),
...(bundle.manifest.loop.done_when ? { done_when: bundle.manifest.loop.done_when } : {}),
...(bundle.manifest.loop.stop_when ? { stop_when: bundle.manifest.loop.stop_when } : {}),
commands: ["start", "status", "log", "advance", "summary"],
};
}
export async function getPreparedWorkflowSkillInstallDependencies(input: {
bundle: WorkflowBundle;
tempDir?: string;
}): Promise<PreparedWorkflowSkillInstallDependencies> {
const dependencies = getWorkflowSkillInstallDependencies(input.bundle);
if (!input.bundle.manifest.loop) {
return { dependencies };
}
const entrySkill = getWorkflowEntrySkill(input.bundle.manifest);
if (!entrySkill) {
throw new Error("Looped workflow entry skill could not be resolved");
}
const entryIndex = input.bundle.manifest.skills.findIndex((skill) => skill.entry === true);
const sourceDependency = dependencies[entryIndex];
if (!sourceDependency) {
throw new Error("Looped workflow entry dependency could not be resolved");
}
const tempRoot = input.tempDir ?? tmpdir();
await mkdir(tempRoot, { recursive: true });
const preparedRoot = await mkdtemp(join(tempRoot, "looped-workflow-entry-"));
const preparedSkillDir = join(preparedRoot, basename(sourceDependency.source));
const preparedLoopScriptPath = resolveWorkflowLoopScriptPath({
sourceDir: preparedSkillDir,
script: input.bundle.manifest.loop.script,
requireExists: false,
});
const metadata = createWorkflowLoopMetadata(input.bundle);
if (!metadata) {
throw new Error("Looped workflow metadata could not be generated");
}
await cp(sourceDependency.source, preparedSkillDir, { recursive: true });
await cp(input.bundle.manifestPath, join(preparedSkillDir, workflowFileName));
await mkdir(dirname(preparedLoopScriptPath), { recursive: true });
await writeFile(preparedLoopScriptPath, renderGeneratedWorkflowLoopRunner());
await writeFile(
join(preparedSkillDir, "loop.metadata.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
);
const preparedDependencies = dependencies.map((dependency, index) =>
index === entryIndex ? { ...dependency, source: preparedSkillDir } : dependency,
);
return {
dependencies: preparedDependencies,
cleanup: () => rm(preparedRoot, { recursive: true, force: true }),
};
}
function renderGeneratedWorkflowLoopRunner(): string {
return [
"#!/usr/bin/env node",
"",
'import { spawnSync } from "node:child_process";',
'import { fileURLToPath } from "node:url";',
"",
'const workflowJson = fileURLToPath(new URL("./workflow.json", import.meta.url));',
'const cliCommand = process.env.GETSUPERPOWER_BIN ?? "getsuperpower";',
"const [command, ...args] = process.argv.slice(2);",
"",
"if (!command) {",
' console.error("Usage: node loop.mjs <start|status|log|advance|summary> [options]");',
" process.exitCode = 1;",
"} else {",
' const result = spawnSync(cliCommand, ["loop", command, workflowJson, ...args], {',
' stdio: "inherit",',
" });",
"",
" if (result.error) {",
' if (result.error.code === "ENOENT") {',
' console.error("GetSuperpower CLI is required to run loop.mjs. Install or expose getsuperpower on PATH.");',
" process.exitCode = 1;",
" } else {",
" console.error(result.error.message);",
" process.exitCode = 1;",
" }",
' } else if (typeof result.status === "number") {',
" process.exitCode = result.status;",
" } else {",
" if (result.signal) {",
' console.error("getsuperpower terminated by signal " + result.signal);',
" }",
" process.exitCode = 1;",
" }",
"}",
"",
].join("\n");
}
function getWorkflowEntrySkill(manifest: WorkflowBundleManifest) {
return manifest.skills.find((skill) => skill.entry === true) ?? null;
}
function createInstalledWorkflowBundle(
bundle: WorkflowBundle,
installArtifacts: WorkflowInstallSkillArtifact[],
): InstalledWorkflowBundle {
return {
...bundle.manifest,
source: bundle.source,
...(installArtifacts.length > 0 ? { installArtifacts } : {}),
};
}
function flattenInstallArtifacts(
artifacts: WorkflowInstallSkillArtifact[],
): WorkflowRemovalArtifact[] {
return artifacts.flatMap((artifact) =>
isRemovableInstallStatus(artifact.status)
? artifact.paths.map((path) => ({
source: artifact.source,
skillName: artifact.skillName,
agent: artifact.agent,
status: artifact.status,
path,
}))
: [],
);
}
function isRemovableInstallStatus(status: string): boolean {
return status === "installed" || status === "updated" || status === "overwritten";
}
function getArtifactPathOwners(workflows: InstalledWorkflowBundle[]): Map<string, string[]> {
const owners = new Map<string, string[]>();
for (const workflow of workflows) {
for (const artifact of workflow.installArtifacts ?? []) {
for (const path of artifact.paths) {
const existing = owners.get(path) ?? [];
existing.push(workflow.name);
owners.set(path, existing);
}
}
}
return owners;
}
function inferLegacyRemovalArtifacts(
workflow: InstalledWorkflowBundle,
homeDir: string,
skippedArtifacts: WorkflowRemovalSkippedArtifact[],
): WorkflowRemovalArtifact[] {
const artifacts: WorkflowRemovalArtifact[] = [];
for (const skill of workflow.skills) {
const inferred = inferLegacySkillName(skill.source);
if (!("skillName" in inferred)) {
skippedArtifacts.push({ source: skill.source, reason: inferred.reason });
continue;
}
for (const path of getLegacySkillArtifactPaths(homeDir, inferred.skillName)) {
artifacts.push({
source: skill.source,
skillName: inferred.skillName,
agent: "legacy",
status: "legacy_inferred",
path,
});
}
}
return artifacts;
}
function inferLegacySkillName(source: string): { skillName: string } | { reason: string } {
if (isLocalWorkflowSkillSource(source)) {
const skillName = basename(source);
return skillName ? { skillName } : { reason: "Local skill source has no folder name" };
}
if (source === "superpowers:brainstorming") {
return { skillName: "superpowers-brainstorming" };
}
if (source === "superpowers:writing-plans") {
return { skillName: "superpowers-writing-plans" };
}
if (source === "superpowers:verification-before-completion") {
return { skillName: "superpowers-verification-before-completion" };
}
if (source.startsWith("mattpocock:")) {
const skillName = source.slice("mattpocock:".length).trim();
return skillName ? { skillName } : { reason: "Matt Pocock skill source has no skill name" };
}
const mattPocockGithubPrefix = "github:mattpocock/skills/";
if (source.startsWith(mattPocockGithubPrefix)) {
const suffix = source.slice(mattPocockGithubPrefix.length);
const skillName = suffix.startsWith("skills/") ? suffix.slice("skills/".length) : suffix;
return skillName ? { skillName } : { reason: "Matt Pocock GitHub source has no skill name" };
}
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(source)) {
return { skillName: source };
}
return { reason: `Cannot safely infer installed skill name from ${source}` };
}
function getLegacySkillArtifactPaths(homeDir: string, skillName: string): string[] {
return [
join(homeDir, ".claude", "skills", skillName),
join(homeDir, ".agents", "skills", skillName),
join(homeDir, ".codex", "skills", skillName),
join(homeDir, ".cursor", "rules", `${skillName}.mdc`),
];
}
function validateWorkflowBundleFiles(input: {
manifest: WorkflowBundleManifest;
sourceDir: string;
}): void {
if (!input.manifest.loop) {
return;
}
resolveWorkflowLoopScriptPath({
sourceDir: input.sourceDir,
script: input.manifest.loop.script,
requireExists: false,
});
}
function resolveWorkflowLoopScriptPath(input: {
sourceDir: string;
script: string;
requireExists: boolean;
}): string {
if (isAbsolute(input.script)) {
throw new Error("Workflow loop script must be a relative path");
}
if (extname(input.script) !== ".mjs") {
throw new Error("Workflow loop script must use a .mjs extension");
}
const scriptPath = resolve(input.sourceDir, input.script);
const relativePath = relative(input.sourceDir, scriptPath);
if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
throw new Error("Workflow loop script must stay inside the workflow bundle");
}
if (input.requireExists && !existsSync(scriptPath)) {
throw new Error(`Workflow loop script was not found: ${scriptPath}`);
}
return scriptPath;
}
async function loadWorkflowLockFileForManifest(input: {
manifest: WorkflowBundleManifest;
sourceDir: string;
}): Promise<{ lock: WorkflowLockFile; path: string } | null> {
const path = join(input.sourceDir, workflowLockFileName);
if (!existsSync(path)) {
return null;
}
const rawLock = await readFile(path, "utf8");
const lock = WorkflowLockFileSchema.parse(JSON.parse(rawLock));
validateWorkflowLockFile({ lock, manifest: input.manifest, path });
return { lock, path };
}
function validateWorkflowLockFile(input: {
lock: WorkflowLockFile;
manifest: WorkflowBundleManifest;
path: string;
}): void {
const issues: string[] = [];
if (input.lock.workflow !== input.manifest.name) {
issues.push(`workflow is ${input.lock.workflow}, expected ${input.manifest.name}`);
}
if (input.lock.workflowVersion !== input.manifest.version) {
issues.push(
`workflowVersion is ${input.lock.workflowVersion}, expected ${input.manifest.version}`,
);
}
if (input.lock.skills.length !== input.manifest.skills.length) {
issues.push(
`skills length is ${input.lock.skills.length}, expected ${input.manifest.skills.length}`,
);
}
for (const [index, skill] of input.manifest.skills.entries()) {
const lockedSkill = input.lock.skills[index];
if (!lockedSkill) {
continue;
}
if (lockedSkill.source !== skill.source) {
issues.push(`skills[${index}].source is ${lockedSkill.source}, expected ${skill.source}`);
}
if (lockedSkill.repo !== skill.repo) {
issues.push(
`skills[${index}].repo is ${lockedSkill.repo ?? "<none>"}, expected ${
skill.repo ?? "<none>"
}`,
);
}
const expectedKind = isLocalWorkflowSkillSource(skill.source) ? "local" : "external";
if (lockedSkill.kind !== expectedKind) {
issues.push(`skills[${index}].kind is ${lockedSkill.kind}, expected ${expectedKind}`);
}
}
if (issues.length > 0) {
throw new Error(
`Workflow lock file does not match manifest: ${input.path}\n${issues.join("\n")}`,
);
}
}
function getWorkflowLockResolvedName(source: string): string {
if (isLocalWorkflowSkillSource(source)) {
return basename(source);
}
const inferred = inferLegacySkillName(source);
if ("skillName" in inferred) {
return inferred.skillName;
}
return source;
}
async function hashLocalWorkflowSkill(skillDir: string): Promise<string> {
const files = await listWorkflowSkillFiles(skillDir);
if (files.length === 0) {
throw new Error(`Local workflow skill has no files to lock: ${skillDir}`);
}
const hash = createHash("sha256");
hash.update("getsuperpower-workflow-lock-local-skill-v0.1\n");
for (const filePath of files) {
const relativePath = normalizeLockPath(relative(skillDir, filePath));
const content = await readFile(filePath);
hash.update(`file:${relativePath}\n`);
hash.update(`bytes:${content.length}\n`);
hash.update(content);
hash.update("\n");
}
return `sha256:${hash.digest("hex")}`;
}
async function listWorkflowSkillFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await listWorkflowSkillFiles(path)));
continue;
}
if (entry.isFile()) {
files.push(path);
}
}
return files.sort((left, right) =>
normalizeLockPath(left).localeCompare(normalizeLockPath(right)),
);
}
function hashExternalWorkflowSkill(source: string, repo: string | undefined): string {
const hash = createHash("sha256");
hash.update("getsuperpower-workflow-lock-external-skill-v0.1\n");
hash.update(`source:${source}\n`);
hash.update(`repo:${repo ?? ""}\n`);
return `sha256:${hash.digest("hex")}`;
}
function normalizeLockPath(path: string): string {
return path.split(sep).join("/");
}
function createScaffoldManifest(name: string): WorkflowBundleManifest {
return WorkflowBundleManifestSchema.parse({
schemaVersion: "0.1",
name,
version: "0.1.0",
description: `GetSuperpower for ${name}.`,
skills: [
{ source: `./skills/${name}` },
{ source: "superpowers:brainstorming", repo: "obra/superpowers" },
{ source: "./skills/custom-review" },
{ source: "superpowers:writing-plans", repo: "obra/superpowers" },
],
steps: [
{
id: "shape",
title: "Shape the request",
skill: "superpowers:brainstorming",
gate: "human_approval",
},
{
id: "custom-review",
title: "Run the bundle-specific review",
skill: "./skills/custom-review",
gate: "human_approval",
},
{
id: "plan",
title: "Write the implementation plan",
skill: "superpowers:writing-plans",
},
],
});
}
function createScaffoldEntrySkill(name: string, manifest: WorkflowBundleManifest): string {
return [
"---",
`name: ${name}`,
`description: "Use when running the ${name} GetSuperpower workflow, bundle skill, skill tree, or orchestrated multi-skill workflow."`,
"---",
"",
`# ${name} GetSuperpower`,
"",
`This is the entry skill for the ${name} GetSuperpower.`,
"",
"When this skill is used, run the workflow below in order. Load/use every required sub-skill before doing the work for its phase.",
"",
"## Required Sub-Skills",
"",
...manifest.steps.flatMap((step, index) => [
`${index + 1}. ${formatEntrySkillSource(step.skill)} - ${step.title}`,
]),
"",
"If any required sub-skill is unavailable, stop and tell the user which dependency is missing.",
"",
"## Workflow",
"",
...manifest.steps.flatMap((step, index) => [
`${index + 1}. ${step.title}`,
` - Skill: ${formatEntrySkillSource(step.skill)}`,
...(step.gate === "human_approval" ? [" - Gate: wait for explicit human approval."] : []),
]),
"",
"## Author Notes",
"",
"- Keep this entry skill, `workflow.json`, and `README.md` aligned when adding or removing steps.",
"- The entry skill orchestrates through required instructions; GetSuperpower installs and validates the dependency skills.",
"- Do not silently skip a missing sub-skill.",
"",
].join("\n");
}
function formatEntrySkillSource(source: string): string {
if (source.startsWith("./skills/")) {
const skillName = source.slice("./skills/".length);
return `${skillName} (${source})`;
}
return source;
}
function isLocalWorkflowSkillSource(source: string): boolean {
return source.startsWith("./") || source.startsWith("../");
}
interface ResolvedWorkflowBundleSource {
sourceDir: string;
source: WorkflowBundleSource;
cleanup?: () => Promise<void>;
alias?: string;
}
async function resolveWorkflowBundleSource(
source: string,
options: {
cwd: string;
runGitCommand?: WorkflowGitCommandRunner;
tempDir?: string;
},
): Promise<ResolvedWorkflowBundleSource> {
const aliasSource = parseWorkflowAliasSource(source);
if (aliasSource) {
return cloneGitWorkflowSource(aliasSource, options);
}
const gitSource = parseGitWorkflowSource(source);
if (gitSource) {
return cloneGitWorkflowSource(gitSource, options);
}
if (isLocalWorkflowSource(source)) {
const sourceDir = isAbsolute(source) ? source : resolve(options.cwd, source);
return { sourceDir, source: { kind: "local", path: sourceDir } };
}
throw new Error(
`Unsupported GetSuperpower source: ${source}. Use a local path, workflow.json path, public git URL, or lowercase workflow alias.`,
);
}
function parseWorkflowAliasSource(source: string): GitWorkflowSource | null {
if (!workflowAliasPattern.test(source)) {
return null;
}
const url = `${canonicalExamplesGitUrl}#${canonicalExamplesWorkflowPath}/${source}`;
const gitSource = parseGitWorkflowSource(url);
if (!gitSource) {
throw new Error(`Could not build canonical GetSuperpower workflow alias URL: ${source}`);
}
return { ...gitSource, alias: source };
}
function isLocalWorkflowSource(source: string): boolean {
return (
source === "." ||
source === ".." ||
source.startsWith("./") ||
source.startsWith("../") ||
source.startsWith("/") ||
source.includes("/") ||
source.endsWith(".json")
);
}
interface GitWorkflowSource {
cloneUrl: string;
url: string;
subdirectory?: string;
alias?: string;
}
function parseGitWorkflowSource(source: string): GitWorkflowSource | null {
let url: URL;
try {
url = new URL(source);
} catch {
return null;
}
if (url.protocol !== "https:" && url.protocol !== "file:") {
return null;
}
const subdirectory = parseGitWorkflowSubdirectory(url.hash);
url.hash = "";
return {
cloneUrl: url.toString(),
url: source,
...(subdirectory ? { subdirectory } : {}),
};
}
function parseGitWorkflowSubdirectory(hash: string): string | undefined {
if (!hash) {
return undefined;
}
const rawHash = decodeURIComponent(hash.slice(1));
const params = new URLSearchParams(rawHash);
const candidate = params.get("path") ?? (rawHash.includes("=") ? undefined : rawHash);
if (!candidate) {
return undefined;
}
const normalized = candidate
.split("/")
.filter((part) => part.length > 0 && part !== ".")
.join("/");
if (!normalized) {
return undefined;
}
if (
normalized === ".." ||
normalized.startsWith("../") ||
normalized.includes("/../") ||
isAbsolute(normalized)
) {
throw new Error(`Unsupported public git workflow subdirectory: ${candidate}`);
}
return normalized;
}
async function cloneGitWorkflowSource(
source: GitWorkflowSource,
options: {
cwd: string;
runGitCommand?: WorkflowGitCommandRunner;
tempDir?: string;
},
): Promise<ResolvedWorkflowBundleSource> {
const tempRoot = await mkdtemp(join(options.tempDir ?? tmpdir(), "getsuperpower-git-"));
const checkoutDir = join(tempRoot, "checkout");
const cleanup = () => rm(tempRoot, { recursive: true, force: true });
try {
await runRequiredGitCommand(
{
executable: "git",
args: ["clone", "--depth", "1", source.cloneUrl, checkoutDir],
cwd: options.cwd,
env: process.env,
},
options.runGitCommand,
`Public git workflow source could not be fetched: ${source.url}`,
);
const commitResult = await runOptionalGitCommand(
{
executable: "git",
args: ["rev-parse", "HEAD"],
cwd: checkoutDir,
env: process.env,
},
options.runGitCommand,
);
const commit = commitResult.exitCode === 0 ? commitResult.stdout.trim() : undefined;
const sourceDir = source.subdirectory ? join(checkoutDir, source.subdirectory) : checkoutDir;
return {
sourceDir,
source: {
kind: "git",
url: source.url,
...(commit ? { commit } : {}),
...(source.subdirectory ? { subdirectory: source.subdirectory } : {}),
},
cleanup,
...(source.alias ? { alias: source.alias } : {}),
};
} catch (error) {
await cleanup();
throw error;
}
}
async function runRequiredGitCommand(
command: WorkflowGitCommand,
runGitCommand: WorkflowGitCommandRunner | undefined,
failureMessage: string,
): Promise<WorkflowGitCommandResult> {
const result = await runOptionalGitCommand(command, runGitCommand);
if (result.exitCode !== 0) {
const detail = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
throw new Error(detail ? `${failureMessage}\n${detail}` : failureMessage);
}
return result;
}
async function runOptionalGitCommand(
command: WorkflowGitCommand,
runGitCommand: WorkflowGitCommandRunner | undefined,
): Promise<WorkflowGitCommandResult> {
if (runGitCommand) {
return runGitCommand(command);
}
return runSubprocess(command);
}
function isMissingFileError(error: unknown): boolean {
return (error as NodeJS.ErrnoException).code === "ENOENT";
}
import { constants } from "node:fs";
import { access, appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const commandNames = new Set(["start", "status", "log", "advance", "summary"]);
const eventTypes = new Set([
"start",
"status",
"question",
"answer",
"approval",
"phase_result",
"error",
"summary",
"advance",
"force_advance",
"complete",
]);
export async function runWorkflowLoopCli(input = {}) {
const stdout = input.stdout ?? ((value) => process.stdout.write(value));
const stderr = input.stderr ?? ((value) => process.stderr.write(value));
try {
const parsed = parseArgs(input.argv ?? process.argv.slice(2));
if (!parsed.command || !commandNames.has(parsed.command)) {
throw new Error(
`Usage: ${formatInputCommand(input, "<start|status|log|advance|summary>")} [options]`,
);
}
const manifestPath = resolveManifestPath(input.workflowJson, parsed.options.workflowJson);
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const context = {
manifest,
manifestPath,
workflowDir: dirname(manifestPath),
workflowName: manifest.name,
runsRoot: join(input.homeDir ?? homedir(), ".getsuperpower", "runs", manifest.name),
json: parsed.options.json === true,
cwd: input.cwd ?? process.cwd(),
stdout,
commandPrefix:
typeof input.commandPrefix === "function" ? input.commandPrefix : defaultCommandPrefix,
};
await runCommand(context, parsed.command, parsed.options);
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
stderr(`${message}\n`);
return 1;
}
}
async function runCommand(context, command, options) {
switch (command) {
case "start":
await startRun(context, options);
return;
case "status":
await showStatus(context, options);
return;
case "log":
await logEventCommand(context, options);
return;
case "advance":
await advanceRun(context, options);
return;
case "summary":
await writeSummaryCommand(context, options);
return;
default:
throw new Error(`Unknown loop command: ${command}`);
}
}
function resolveManifestPath(workflowJson, override) {
if (override) {
return resolve(override);
}
if (workflowJson instanceof URL) {
return fileURLToPath(workflowJson);
}
if (typeof workflowJson === "string") {
return resolve(workflowJson);
}
return fileURLToPath(new URL("./workflow.json", import.meta.url));
}
async function startRun(context, options) {
const now = new Date();
const runId = options.run ?? createRunId(now, context.workflowName);
const runDir = runPath(context, runId);
if (await pathExists(runDir)) {
throw new Error(`Run already exists: ${runId}`);
}
const state = {
schemaVersion: "0.1",
workflow: context.workflowName,
runId,
status: "active",
currentStepIndex: 0,
currentStep: context.manifest.steps[0]?.id ?? null,
goal: buildGoalPayload(context),
cwd: context.cwd,
startedAt: now.toISOString(),
updatedAt: now.toISOString(),
};
await mkdir(runDir, { recursive: true });
await writeState(context, state);
await appendEvent(context, runId, {
timestamp: now.toISOString(),
type: "start",
step: state.currentStep,
message: `Started ${context.workflowName}`,
metadata: { cwd: state.cwd },
});
writeOutput(context, buildStatusPayload(context, state));
}
async function showStatus(context, options) {
const runId = await resolveRunId(context, options);
const state = await readState(context, runId);
const step = getCurrentStep(context, state);
await appendEvent(context, runId, {
timestamp: new Date().toISOString(),
type: "status",
step: step?.id ?? null,
message: step ? `Status requested for ${step.id}` : "Status requested for completed run",
metadata: { selectedByLatest: options.latest === true },
});
writeOutput(context, buildStatusPayload(context, state, { selectedByLatest: options.latest }));
}
async function logEventCommand(context, options) {
const runId = requireOption(options, "run", "log requires --run <id>");
const type = requireOption(options, "type", "log requires --type <event-type>");
if (!eventTypes.has(type)) {
throw new Error(`Unsupported event type: ${type}`);
}
const state = await readState(context, runId);
const metadata = options.metadata ? parseJsonOption(options.metadata, "--metadata") : {};
const step = options.step ?? state.currentStep;
await appendEvent(context, runId, {
timestamp: new Date().toISOString(),
type,
step,
message: options.message ?? "",
metadata,
});
await touchState(context, state);
writeOutput(context, {
workflow: context.workflowName,
runId,
event: { type, step, message: options.message ?? "", metadata },
actions: buildActions(context, state),
});
}
async function advanceRun(context, options) {
const runId = requireOption(options, "run", "advance requires --run <id>");
const state = await readState(context, runId);
const now = new Date().toISOString();
const previousStep = getCurrentStep(context, state);
if (options.to) {
if (options.force !== true) {
throw new Error("advance --to requires --force and --reason");
}
const reason = requireOption(options, "reason", "advance --to requires --force and --reason");
const targetIndex = context.manifest.steps.findIndex((step) => step.id === options.to);
if (targetIndex < 0) {
throw new Error(`Unknown workflow step: ${options.to}`);
}
state.currentStepIndex = targetIndex;
state.currentStep = context.manifest.steps[targetIndex].id;
state.status = "active";
state.updatedAt = now;
await writeState(context, state);
await appendEvent(context, runId, {
timestamp: now,
type: "force_advance",
step: state.currentStep,
message: reason,
metadata: { from: previousStep?.id ?? null, to: state.currentStep },
});
writeOutput(context, buildStatusPayload(context, state));
return;
}
const nextIndex = state.currentStepIndex + 1;
if (nextIndex >= context.manifest.steps.length) {
state.currentStepIndex = context.manifest.steps.length;
state.currentStep = null;
state.status = "complete";
state.completedAt = now;
state.updatedAt = now;
await writeState(context, state);
await appendEvent(context, runId, {
timestamp: now,
type: "complete",
step: previousStep?.id ?? null,
message: `Completed ${context.workflowName}`,
metadata: {},
});
writeOutput(context, buildStatusPayload(context, state));
return;
}
state.currentStepIndex = nextIndex;
state.currentStep = context.manifest.steps[nextIndex].id;
state.updatedAt = now;
await writeState(context, state);
await appendEvent(context, runId, {
timestamp: now,
type: "advance",
step: state.currentStep,
message: `Advanced to ${state.currentStep}`,
metadata: { from: previousStep?.id ?? null, to: state.currentStep },
});
writeOutput(context, buildStatusPayload(context, state));
}
async function writeSummaryCommand(context, options) {
const runId = await resolveRunId(context, options);
const state = await readState(context, runId);
const events = await readEvents(context, runId);
const summary = buildSummary(context, state, events);
const summaryPath = join(runPath(context, runId), "summary.md");
await writeFile(summaryPath, summary);
await appendEvent(context, runId, {
timestamp: new Date().toISOString(),
type: "summary",
step: state.currentStep,
message: "Generated mechanical summary",
metadata: { path: summaryPath },
});
await touchState(context, state);
writeOutput(context, {
workflow: context.workflowName,
runId,
summaryPath,
actions: buildActions(context, state),
});
}
function buildStatusPayload(context, state, extra = {}) {
const step = getCurrentStep(context, state);
return {
workflow: context.workflowName,
runId: state.runId,
status: state.status,
selectedByLatest: extra.selectedByLatest === true,
step: step
? {
id: step.id,
title: step.title,
skill: step.skill,
gate: step.gate ?? null,
instruction: step.instruction ?? "",
verify: step.verify ?? null,
index: state.currentStepIndex,
}
: null,
instruction: step?.instruction ?? "Workflow is complete.",
goal: buildGoalPayload(context),
actions: buildActions(context, state),
};
}
function buildGoalPayload(context) {
const loop = context.manifest.loop;
if (!loop?.goal) {
return null;
}
return {
type: loop.type ?? "goal_based",
goal: loop.goal,
done_when: loop.done_when ?? [],
stop_when: loop.stop_when ?? [],
};
}
function buildActions(context, state) {
const step = getCurrentStep(context, state);
if (!step) {
return [
{
type: "summary",
command: `${formatCommand(context, "summary")} --run ${state.runId}`,
description: "Generate or refresh the mechanical workflow summary.",
},
];
}
return [
{
type: "run_phase",
step: step.id,
skill: step.skill,
instruction: step.instruction ?? "",
description: "Use the named skill and perform the phase work yourself.",
},
{
type: "log_event",
command: `${formatCommand(context, "log")} --run ${state.runId} --type phase_result --message "..."`,
description: "Record what happened before advancing.",
},
...(step.verify
? [
{
type: "verify",
step: step.id,
verify: step.verify,
description: "Check the phase verification rule before advancing.",
},
]
: []),
{
type: "advance",
command: `${formatCommand(context, "advance")} --run ${state.runId}`,
description:
step.gate === "human_approval"
? "Run only after explicit human approval."
: "Run when the phase is done.",
},
];
}
function buildSummary(context, state, events) {
const approvals = events.filter((event) => event.type === "approval");
const errors = events.filter((event) => event.type === "error");
const forceAdvances = events.filter((event) => event.type === "force_advance");
const latestPhaseResult = [...events].reverse().find((event) => event.type === "phase_result");
const goal = buildGoalPayload(context);
const completedSteps = context.manifest.steps
.slice(0, Math.min(state.currentStepIndex, context.manifest.steps.length))
.map((step) => `- ${step.id}: ${step.title}`);
return [
`# ${context.workflowName} Run ${state.runId}`,
"",
`Status: ${state.status}`,
`Current step: ${state.currentStep ?? "complete"}`,
"",
"## Goal",
goal ? formatGoal(goal) : "- none",
"",
"## Completed Steps",
completedSteps.length > 0 ? completedSteps.join("\n") : "- none",
"",
"## Approvals",
formatEvents(approvals),
"",
"## Errors",
formatEvents(errors),
"",
"## Force Advances",
formatEvents(forceAdvances),
"",
"## Latest Phase Result",
latestPhaseResult ? formatEvent(latestPhaseResult) : "- none",
"",
"## Next Action",
buildActions(context, state)
.map((action) => `- ${action.type}: ${action.description}`)
.join("\n"),
"",
].join("\n");
}
function formatGoal(goal) {
return [
`Goal: ${goal.goal}`,
`Type: ${goal.type}`,
...goal.done_when.map((condition) => `- done_when: ${condition}`),
...goal.stop_when.map((condition) => `- stop_when: ${condition}`),
].join("\n");
}
function formatEvents(events) {
return events.length > 0 ? events.map(formatEvent).join("\n") : "- none";
}
function formatEvent(event) {
return `- ${event.timestamp} ${event.step ?? "workflow"} ${event.message}`;
}
async function resolveRunId(context, options) {
if (options.run) {
return options.run;
}
if (options.latest === true) {
return latestActiveRunId(context);
}
throw new Error("Pass --run <id> or --latest.");
}
async function latestActiveRunId(context) {
if (!(await pathExists(context.runsRoot))) {
throw new Error(
`No active runs found for ${context.workflowName}. Start one with: ${formatCommand(context, "start")}`,
);
}
const entries = await readdir(context.runsRoot, { withFileTypes: true });
const states = [];
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
try {
const state = await readState(context, entry.name);
if (state.status === "active") {
states.push(state);
}
} catch {
// Ignore partial or unrelated run folders.
}
}
states.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
const latest = states[0];
if (!latest) {
throw new Error(
`No active runs found for ${context.workflowName}. Start one with: ${formatCommand(context, "start")}`,
);
}
return latest.runId;
}
function getCurrentStep(context, state) {
if (state.status === "complete") {
return null;
}
return context.manifest.steps[state.currentStepIndex] ?? null;
}
async function readState(context, runId) {
return JSON.parse(await readFile(join(runPath(context, runId), "state.json"), "utf8"));
}
async function writeState(context, state) {
const runDir = runPath(context, state.runId);
await mkdir(runDir, { recursive: true });
await writeFile(join(runDir, "state.json"), `${JSON.stringify(state, null, 2)}\n`);
}
async function touchState(context, state) {
state.updatedAt = new Date().toISOString();
await writeState(context, state);
}
async function appendEvent(context, runId, event) {
const runDir = runPath(context, runId);
await mkdir(runDir, { recursive: true });
await appendFile(join(runDir, "events.jsonl"), `${JSON.stringify(event)}\n`);
}
async function readEvents(context, runId) {
const path = join(runPath(context, runId), "events.jsonl");
if (!(await pathExists(path))) {
return [];
}
const content = await readFile(path, "utf8");
return content
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
}
function runPath(context, runId) {
return join(context.runsRoot, sanitizePathSegment(runId));
}
function createRunId(date, workflowName) {
const stamp = date.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
return `${stamp}-${sanitizePathSegment(workflowName)}`;
}
function sanitizePathSegment(value) {
return String(value).replace(/[^a-zA-Z0-9._-]/g, "-");
}
function parseJsonOption(value, name) {
try {
return JSON.parse(value);
} catch {
throw new Error(`${name} must be valid JSON`);
}
}
function requireOption(options, name, message) {
const value = options[name];
if (value === undefined || value === "") {
throw new Error(message);
}
return value;
}
function formatInputCommand(input, command) {
if (typeof input.commandPrefix === "function") {
return input.commandPrefix(command);
}
return defaultCommandPrefix(command);
}
function formatCommand(context, command) {
return context.commandPrefix(command);
}
function defaultCommandPrefix(command) {
return `node loop.mjs ${command}`;
}
function writeOutput(context, payload) {
if (context.json) {
context.stdout(`${JSON.stringify(payload, null, 2)}\n`);
return;
}
const lines = [];
const step = payload.step;
lines.push(`${payload.workflow} ${payload.runId}`);
lines.push(`Status: ${payload.status ?? "ok"}`);
if (step) {
if (payload.goal?.goal) {
lines.push(`Goal: ${payload.goal.goal}`);
}
lines.push(`Step: ${step.id} - ${step.title}`);
lines.push(`Skill: ${step.skill}`);
lines.push(`Instruction: ${step.instruction}`);
if (step.verify) {
lines.push(`Verify: ${formatVerifyRule(step.verify)}`);
}
} else if (payload.instruction) {
lines.push(`Instruction: ${payload.instruction}`);
}
if (payload.summaryPath) {
lines.push(`Summary: ${payload.summaryPath}`);
}
lines.push("Actions:");
for (const action of payload.actions ?? []) {
lines.push(`- ${action.type}: ${action.description}`);
}
context.stdout(`${lines.join("\n")}\n`);
}
function formatVerifyRule(verify) {
const parts = [`type=${verify.type}`];
if (verify.event) {
parts.push(`event=${verify.event}`);
}
if (verify.message_includes) {
parts.push(`message_includes=${verify.message_includes}`);
}
if (verify.description) {
parts.push(`description=${verify.description}`);
}
return parts.join(" ");
}
function parseArgs(args) {
const [command, ...rest] = args;
const options = {};
for (let index = 0; index < rest.length; index += 1) {
const arg = rest[index];
if (!arg.startsWith("--")) {
throw new Error(`Unexpected argument: ${arg}`);
}
const name = toCamelCase(arg.slice(2));
if (["json", "latest", "force"].includes(name)) {
options[name] = true;
continue;
}
const value = rest[index + 1];
if (value === undefined || value.startsWith("--")) {
throw new Error(`Missing value for ${arg}`);
}
options[name] = value;
index += 1;
}
return { command, options };
}
function toCamelCase(value) {
return value.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
}
async function pathExists(path) {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
+3
-6

@@ -49,5 +49,5 @@ # Development Design Delivery GetSuperpower

`getsuperpower install` and `getsuperpower clone` automatically use the Skills
CLI to fetch missing `mattpocock:*` dependencies. If that automatic bootstrap
fails, run the same package install through the CLI and retry:
`getsuperpower install` automatically uses the Skills CLI to fetch missing
`mattpocock:*` dependencies. If that automatic bootstrap fails, run the same
package install through the CLI and retry:

@@ -76,5 +76,2 @@ ```bash

bun run dev -- install examples/workflows/development-design-delivery
bun run dev -- clone examples/workflows/development-design-delivery
```
`clone <source>` is equivalent to `install <source>`.

@@ -21,2 +21,20 @@ # Grilled Product Dev GetSuperpower

This example also declares a generated loop runner:
```text
workflow.json -> loop.script: ./loop.mjs
```
The loop is goal-based and action-only. Its goal is to produce an approved
implementation plan, with explicit `done_when`, `stop_when`, and per-step
`verify` rules in `workflow.json`. The runtime prints those rules as actions;
the agent still performs the work and decides whether the evidence is good
enough to advance.
The generated runner is written into the installed entry skill during
`getsuperpower install`. Loop state lives under
`~/.getsuperpower/runs/grilled-product-dev/<run-id>/`, and the runtime only
returns suggested actions. It never executes tools or shell commands for the
agent.
After install and agent restart, invoke:

@@ -37,5 +55,5 @@

`getsuperpower install` and `getsuperpower clone` automatically use the Skills
CLI to fetch missing `mattpocock:*` dependencies. If that automatic bootstrap
fails, install the Matt Pocock skills package and retry:
`getsuperpower install` automatically uses the Skills CLI to fetch missing
`mattpocock:*` dependencies. If that automatic bootstrap fails, install the
Matt Pocock skills package and retry:

@@ -64,8 +82,15 @@ ```bash

bun run dev -- install examples/workflows/grilled-product-dev
bun run dev -- clone examples/workflows/grilled-product-dev
```
`clone <source>` is equivalent to `install <source>`.
Try the loop runtime through the GetSuperpower CLI:
```bash
bun run dev -- loop start examples/workflows/grilled-product-dev --json
bun run dev -- loop status examples/workflows/grilled-product-dev --latest --json
```
The installed `loop.mjs` remains runnable with plain Node for compatibility, but
agents should use `getsuperpower loop ...`.
Restart the agent app after install so the `$grilled-product-dev` entry skill
and its sub-skills are available.

@@ -13,2 +13,22 @@ ---

## Loop Runtime
Use the GetSuperpower CLI to track the workflow run. The runtime only prints
actions; you still decide and perform the actual work. Use the `workflow.json`
file copied next to this skill as the loop `<source>` when running from an
installed skill directory.
1. Start or resume a run:
- New run: `getsuperpower loop start <source> --json`
- Existing run: `getsuperpower loop status <source> --latest --json`
2. Before every phase, run `getsuperpower loop status <source> --run <run-id> --json`.
3. Follow the returned `actions` and the exact phase `instruction`.
4. After phase work, log a structured event, for example:
`getsuperpower loop log <source> --run <run-id> --type phase_result --message "..."`
5. When the human gate is satisfied, run `getsuperpower loop advance <source> --run <run-id> --json`.
6. Repeat status, phase work, log, and advance until the workflow is complete.
The installed entry skill also gets a generated `loop.mjs` compatibility
wrapper, but agents should prefer the CLI command above.
## Required Sub-Skills

@@ -29,8 +49,11 @@

- Skill: mattpocock:grilling
- Instruction: Ask one grilling question, include your recommended answer, and wait for explicit human approval before advancing.
- Gate: wait for explicit human approval that the grilled direction is ready.
2. Turn the approved direction into a Superpowers design.
- Skill: superpowers:brainstorming
- Instruction: Turn the approved direction into a Superpowers design spec, then wait for explicit human approval before advancing.
- Gate: wait for explicit human approval of the design and written spec.
3. Write the implementation plan.
- Skill: superpowers:writing-plans
- Instruction: Write the approved implementation plan as small executable tasks, then log the plan result.

@@ -37,0 +60,0 @@ ## Author Notes

{
"schemaVersion": "0.1",
"name": "grilled-product-dev",
"version": "0.1.0",
"version": "0.1.1",
"description": "Sharpen a product-development request with grilling before Superpowers design and planning.",
"loop": {
"script": "./loop.mjs",
"state": "global",
"execution": "action-only",
"type": "goal_based",
"goal": "Produce an approved implementation plan for a product-development request.",
"done_when": [
"grilled_direction_approved",
"design_spec_approved",
"implementation_plan_written"
],
"stop_when": ["human_blocks", "verification_fails", "workflow_complete"]
},
"skills": [
{
"source": "./skills/grilled-product-dev"
"source": "./skills/grilled-product-dev",
"entry": true
},

@@ -28,3 +42,9 @@ {

"skill": "mattpocock:grilling",
"gate": "human_approval"
"gate": "human_approval",
"instruction": "Ask one grilling question, include your recommended answer, and wait for explicit human approval before advancing.",
"verify": {
"type": "human_approval",
"event": "approval",
"message_includes": "direction ready"
}
},

@@ -35,3 +55,9 @@ {

"skill": "superpowers:brainstorming",
"gate": "human_approval"
"gate": "human_approval",
"instruction": "Turn the approved direction into a Superpowers design spec, then wait for explicit human approval before advancing.",
"verify": {
"type": "human_approval",
"event": "approval",
"message_includes": "design approved"
}
},

@@ -41,5 +67,11 @@ {

"title": "Write the implementation plan",
"skill": "superpowers:writing-plans"
"skill": "superpowers:writing-plans",
"instruction": "Write the approved implementation plan as small executable tasks, then log the plan result.",
"verify": {
"type": "event",
"event": "phase_result",
"message_includes": "implementation plan written"
}
}
]
}

@@ -57,3 +57,3 @@ # OpenSpec Delivery GetSuperpower

`getsuperpower install` and `getsuperpower clone` automatically use the Skills CLI to fetch missing
`getsuperpower install` automatically uses the Skills CLI to fetch missing
`mattpocock:*` dependencies. If that automatic bootstrap fails, run the same

@@ -84,8 +84,5 @@ package install through the CLI and retry:

bun run dev -- install examples/workflows/openspec-superpowers
bun run dev -- clone examples/workflows/openspec-superpowers
```
`clone <source>` is equivalent to `install <source>`.
Restart the agent app after install so the `$openspec-delivery` entry skill
and its sub-skills are available.

@@ -10,3 +10,3 @@ # Real Engineering GetSuperpower

`getsuperpower install` and `getsuperpower clone` automatically use the Skills CLI to fetch missing
`getsuperpower install` automatically uses the Skills CLI to fetch missing
`mattpocock:*` dependencies. If that automatic bootstrap fails, run the same

@@ -22,3 +22,3 @@ package install through the CLI and retry:

```bash
bun run dev -- getsuperpower validate examples/workflows/real-engineering
bun run dev -- validate examples/workflows/real-engineering
```

@@ -29,6 +29,3 @@

```bash
bun run dev -- getsuperpower install examples/workflows/real-engineering
bun run dev -- getsuperpower clone examples/workflows/real-engineering
bun run dev -- install examples/workflows/real-engineering
```
`getsuperpower clone <source>` is equivalent to `getsuperpower install <source>`.

@@ -9,12 +9,9 @@ # Release Review GetSuperpower

```bash
getsuperpower install examples/workflows/release-review
getsuperpower clone examples/workflows/release-review
bun run dev -- install examples/workflows/release-review
```
`getsuperpower clone <source>` is equivalent to `getsuperpower install <source>`.
Validate it while authoring:
```bash
getsuperpower validate examples/workflows/release-review
bun run dev -- validate examples/workflows/release-review
```

@@ -21,0 +18,0 @@

{
"name": "getsuperpower",
"version": "0.3.6",
"version": "0.4.0",
"type": "module",

@@ -20,2 +20,5 @@ "repository": {

"dev": "bun src/cli.ts",
"dev:alias": "scripts/dev-cli-alias.sh install",
"dev:alias:status": "scripts/dev-cli-alias.sh status",
"dev:unalias": "scripts/dev-cli-alias.sh uninstall",
"build": "bun build --target=node --outfile=dist/cli.js src/cli.ts",

@@ -22,0 +25,0 @@ "test": "bun test tests",

+123
-67

@@ -1,57 +0,137 @@

<img src="/assets/getsupwerpower.jpg" alt="GetSuperpower" width="640" />
# GetSuperpower
GetSuperpower packages a whole AI-agent workflow as one callable skill.
Power your ability.
A **GetSuperpower** is an all-in-one workflow skill. Install it once, call one
entry skill, and the agent follows the required sub-skills in order. A workflow
can plan a spec, brainstorm design, write an implementation plan, build with
TDD, and archive the result without the user calling each skill separately.
GetSuperpower is a many-skill bank for AI agents: install one workflow skill
tree, call one entry skill with a goal, and give your agent the right roles,
playbooks, and verification habits for the problem in front of it. The point is
simple: 3x your ability without manually juggling every specialist skill.
Start with Startup Goal when you want to move a company-building goal through many role workflows instead of manually juggling every skill: CEO, CTO, Product Manager, Engineering Manager, Founding Engineer, and QA Lead.
## Quick Start
Install a workflow example by alias:
Install the full startup goal workflow:
```bash
npx getsuperpower@latest install openspec-superpowers
npx getsuperpower@latest install startup-goal
```
The alias is shorthand for the checked-in workflow path:
Then ask your agent to run the entry skill with a goal:
```text
$startup-goal help me launch this product from idea to shipped v1
```
The alias points to the checked-in workflow bundle:
```bash
npx getsuperpower@latest install 'https://github.com/0xroylee/getsuperpower.git#examples/workflows/openspec-superpowers'
npx getsuperpower@latest install 'https://github.com/0xroylee/getsuperpower.git#examples/workflows/startup-goal'
```
Install other workflow examples the same way:
Install individual startup roles when you want one specialist workflow:
```bash
npx getsuperpower@latest install release-review
npx getsuperpower@latest install real-engineering
npx getsuperpower@latest install development-design-delivery
npx getsuperpower@latest install ceo
npx getsuperpower@latest install cto
npx getsuperpower@latest install product-manager
npx getsuperpower@latest install engineering-manager
npx getsuperpower@latest install founding-engineer
npx getsuperpower@latest install qa-lead
```
List installed GetSuperpowers:
Restart your agent after installing skills so it reloads the new entry skills.
## Workflow Registry
Use the registry view to pick a GetSuperpower, inspect its role workflow, and
copy the install command.
<img src="assets/getsuperpower-startup-role-registry.png" alt="GetSuperpower startup role workflow registry" width="920" />
## Startup Role Workflows
| GetSuperpower | Entry skill | What it helps with |
| --- | --- | --- |
| Startup Goal | `$startup-goal` | Route a company-building goal through strategy, product, architecture, delivery, implementation, and QA role subagents. |
| CEO | `$ceo` | Direction, hard tradeoffs, fundraising/customer framing, and company decisions. |
| CTO | `$cto` | Architecture, domain model, platform direction, and engineering risk. |
| Product Manager | `$product-manager` | Product discovery, PRDs, acceptance criteria, roadmap tradeoffs, and issue slicing. |
| Engineering Manager | `$engineering-manager` | Delivery sequencing, execution risk, quality gates, blocker triage, and engineering process. |
| Founding Engineer | `$founding-engineer` | The smallest correct implementation change: tests, debugging, review, and verification. |
| QA Lead | `$qa-lead` | Release-risk review, acceptance checks, regression focus, reproduction gaps, and verification evidence. |
Each workflow is still just files you can inspect: a `workflow.json`, README,
and local skills. The power comes from installing the skill tree once and then
calling the entry skill that knows which companion skills to use.
## Goal Loops
Some GetSuperpowers also expose a loop runner for goals that should keep moving
until the goal is done. A loop is resumable workflow state: `loop start` creates
a run, `loop status` shows where it is, and `loop advance` returns the next suggested action.
The runtime is action-only. It records state and returns the next suggested action;
it does not silently execute tools or shell commands for the agent.
Try the loop-capable product-development workflow:
```bash
npx getsuperpower@latest list
npx getsuperpower@latest loop start grilled-product-dev --json
npx getsuperpower@latest loop status grilled-product-dev --latest --json
npx getsuperpower@latest loop advance grilled-product-dev --run <run-id> --json
```
Supported agents: Claude, Codex, Cursor, opencode/OpenCodex, and GitHub Copilot. CLI aliases include `opencode`, `opencodex`, `copilot`, and `github-copilot`.
That shape is useful for complex work: clarify the goal, move one action
forward, verify evidence, and keep advancing until the goal is done.
Restart your agent after installing skills so it reloads them.
## Built-In Workflow Ecosystem
## How It Works
GetSuperpower workflows can compose local skills, bundled skills, and external
skill packs:
<img src="assets/diagrams/getsuperpower-how-it-works.svg" alt="GetSuperpower workflow diagram" width="720" />
- Matt Pocock skills for TDD, review, design pressure-testing, domain modeling,
PRDs, and issue slicing.
- Superpowers skills for brainstorming, planning, execution, and verification.
- Ponytrail evidence for file-change rationale, verification, and rollback
context in workflows that declare `pony-trail`.
- More workflow packs are coming.
`workflow.json` installs the skill tree. The entry skill is the one command users invoke. Sub-skills are the steps the agent follows behind that call.
`getsuperpower install` uses each workflow skill's `repo` metadata to fetch
missing external skills through the Skills CLI. For example,
`{ "source": "superpowers:brainstorming", "repo": "obra/superpowers" }`
keeps the original skill name in `source` and installs it with
`npx skills add obra/superpowers --skill brainstorming`.
### Install And Run Sequence
If automatic bootstrap fails, run the package install through GetSuperpower and
retry:
<img src="assets/diagrams/getsuperpower-install-sequence.svg" alt="GetSuperpower install and run sequence diagram" width="920" />
```bash
npx getsuperpower@latest skills install mattpocock/skills
```
## Command Reference
```bash
npx getsuperpower@latest install <alias-or-path-or-git-url>
npx getsuperpower@latest list
npx getsuperpower@latest deps <source>
npx getsuperpower@latest lock <source>
npx getsuperpower@latest loop <start|status|log|advance|summary> <source>
npx getsuperpower@latest remove <workflow-name>
npx getsuperpower@latest init <name>
npx getsuperpower@latest validate <source>
npx getsuperpower@latest skills install
npx getsuperpower@latest skills update
```
Run `npx getsuperpower@latest --help` or
`npx getsuperpower@latest <command> --help` for detailed usage.
The older `bundle` and `workflow` commands still work as compatibility aliases.
## Create Your Own
Start with the [Create Your Own Workflow guide](docs/workflow-author-guide.md) if you want to author and share a workflow bundle.
Start with the [Create Your Own Workflow guide](docs/workflow-author-guide.md)
if you want to author and share a workflow bundle.

@@ -77,3 +157,4 @@ Create a new GetSuperpower:

`skills/release-review/SKILL.md` is the entry skill. Edit it when you want users to call one skill that coordinates many sub-skills.
`skills/release-review/SKILL.md` is the entry skill. Edit it when you want
users to call one skill that coordinates many sub-skills.

@@ -92,5 +173,2 @@ Install the authoring helper if you want an agent to help design bundle skills:

That skill should help you choose a focused workflow, draft the entry skill,
align `workflow.json`, and validate the bundle before you share it.
Validate before sharing:

@@ -109,37 +187,15 @@

| --- | --- | --- |
| `examples/workflows/openspec-superpowers` | OpenSpec Delivery: proposal -> design -> plan -> TDD -> verification -> archive. | Includes `$openspec-delivery`. |
| `examples/workflows/development-design-delivery` | Product-minded engineering: shape -> interface design -> plan -> TDD -> review -> evidence. | Includes `$development-design-delivery`. |
| `examples/workflows/real-engineering` | RTK, `pony-trail`, Superpowers, and Matt Pocock skills together. | Fetches Matt Pocock skills if missing. |
| `examples/workflows/release-review` | Small release-risk review workflow. | Good starter example. |
| `examples/workflows/startup-goal` | Install a realistic startup operating bench around one goal. | Includes `$startup-goal`, `$ceo`, `$cto`, `$product-manager`, `$engineering-manager`, `$founding-engineer`, and `$qa-lead`. |
| `examples/workflows/ceo` | Company direction, strategy, tradeoffs, and decision mapping. | Uses Matt Pocock decision and grilling skills. |
| `examples/workflows/cto` | Architecture, domain model, technical risk, and review. | Uses Matt Pocock architecture/review skills. |
| `examples/workflows/product-manager` | Discovery, PRD, issue slicing, and product planning. | Uses Superpowers plus Matt Pocock PRD/issue skills. |
| `examples/workflows/engineering-manager` | Delivery sequencing, quality gates, and execution risk. | Uses planning, TDD, diagnosing, and review skills. |
| `examples/workflows/founding-engineer` | Implementation, tests, debugging, review, and final verification. | Uses `$implement` as the implementation role. |
| `examples/workflows/qa-lead` | Acceptance checks, regression focus, and release verification. | Uses review, diagnosing, and verification skills. |
| `examples/workflows/grilled-product-dev` | Goal loops for shaping product-development work into an approved plan. | Provides `loop start`, `loop status`, and `loop advance`. |
| `examples/workflows/openspec-superpowers` | Compatibility/demo workflow for OpenSpec delivery. | Kept for one release while the role catalog becomes the primary example set. |
| `examples/workflows/development-design-delivery` | Compatibility/demo workflow for product-minded engineering. | Richer composition example with Ponytrail evidence. |
| `examples/workflows/real-engineering` | Compatibility/demo workflow combining RTK, Ponytrail, Superpowers, and Matt Pocock skills. | Fetches Matt Pocock skills if missing. |
| `examples/workflows/release-review` | Compatibility/demo workflow for release-risk review. | Good minimal example. |
GetSuperpower install automatically uses each workflow skill's `repo` metadata
to fetch missing external skills through the Skills CLI. For example,
`{ "source": "superpowers:brainstorming", "repo": "obra/superpowers" }`
keeps the original skill name in `source` and installs it with
`npx skills add obra/superpowers --skill brainstorming`.
If automatic bootstrap fails, run the same package install through the CLI and
retry:
```bash
npx getsuperpower@latest skills install mattpocock/skills
```
## Commands
The GetSuperpower CLI supports workflow install, inspection, authoring, and skill management. Below are some of the most used commands:
- `npx getsuperpower@latest install <alias-or-path-or-git-url>`
- `npx getsuperpower@latest deps <source>`
- `npx getsuperpower@latest list`
- `npx getsuperpower@latest init <name>`
- `npx getsuperpower@latest validate <source>`
- `npx getsuperpower@latest skills install`
- `npx getsuperpower@latest skills install mattpocock/skills`
- `npx getsuperpower@latest skills install creating-bundle-skills`
Run `npx getsuperpower@latest --help` or `npx getsuperpower@latest <command> --help` for detailed usage.
The older `bundle` and `workflow` commands still work as compatibility aliases.
## Installed Files

@@ -155,8 +211,8 @@

Use `--dir <project>` when you intentionally want a project-local workflow record.
Use `--dir <project>` when you intentionally want a project-local workflow
record.
Pony Trail history, revert, and prehook features are paused while GetSuperpower focuses on bundle skills.
Keep project-local `.getsuperpower/` folders out of git unless you
intentionally want to share installed workflow records.
Keep project-local `.getsuperpower/` folders out of git unless you intentionally want to share installed workflow records.
## Local Development

@@ -163,0 +219,0 @@

@@ -24,3 +24,3 @@ #!/usr/bin/env node

const CLI_VERSION = "0.3.6";
const CLI_VERSION = "0.4.0";

@@ -27,0 +27,0 @@ interface CommanderVersionInternals {

@@ -24,3 +24,5 @@ import { existsSync } from "node:fs";

createWorkflowBundleScaffold,
getWorkflowSkillInstallDependencies,
createWorkflowRemovalPlan,
executeWorkflowRemovalPlan,
getPreparedWorkflowSkillInstallDependencies,
installWorkflowBundle,

@@ -30,3 +32,6 @@ listInstalledWorkflowBundles,

type WorkflowGitCommandRunner,
} from "./runtimes/ponytrail";
type WorkflowInstallSkillArtifact,
type WorkflowRemovalPlan,
writeWorkflowLockFile,
} from "./runtimes/getsuperpower";

@@ -113,2 +118,12 @@ export interface GetSuperpowerInstallSkillInput {

export interface GetSuperpowerRemovePromptInput {
workflowName: string;
artifactsToRemove: number;
artifactsToKeep: number;
}
export interface GetSuperpowerRemovePrompt {
confirmRemove(input: GetSuperpowerRemovePromptInput): Promise<boolean>;
}
export interface GetSuperpowerOnboardCommand {

@@ -137,2 +152,3 @@ executable: string;

installPrompt?: GetSuperpowerInstallPrompt;
removePrompt?: GetSuperpowerRemovePrompt;
workflowGitCommandRunner?: WorkflowGitCommandRunner;

@@ -149,2 +165,35 @@ onboardPrompt?: GetSuperpowerOnboardPrompt;

interface GetSuperpowerRemoveCommandOptions {
dir?: string;
home: string;
dryRun: boolean;
yes: boolean;
}
type GetSuperpowerLoopCommandName = "start" | "status" | "log" | "advance" | "summary";
interface GetSuperpowerLoopCommandOptions {
home: string;
run?: string;
latest?: boolean;
json?: boolean;
type?: string;
step?: string;
message?: string;
metadata?: string;
to?: string;
force?: boolean;
reason?: string;
}
interface WorkflowLoopRuntimeModule {
runWorkflowLoopCli(input: {
argv: string[];
workflowJson: string;
cwd: string;
homeDir: string;
commandPrefix: (command: string) => string;
}): Promise<number>;
}
export function configureGetSuperpowerCommand(

@@ -160,8 +209,10 @@ program: Command,

configureAuthorCommands(bundleCommand, options);
configureLockCommand(bundleCommand, options);
const workflowCommand = program
.command("workflow")
.description("Compatibility alias for GetSuperpower install and list commands.");
.description("Compatibility alias for GetSuperpower install, list, and remove commands.");
configureInstallCommand(workflowCommand, options);
configureListCommand(workflowCommand, options.rootDir);
configureRemoveCommand(workflowCommand, options);

@@ -176,6 +227,9 @@ return program;

configureAuthorCommands(command, options);
configureLockCommand(command, options);
configureInstallCommand(command, options);
configureListCommand(command, options.rootDir);
configureRemoveCommand(command, options);
configureDependencyCommand(command, options);
configureOnboardCommand(command, options);
configureLoopCommand(command, options);
}

@@ -226,2 +280,35 @@

function configureLockCommand(
command: Command,
options: ConfigureGetSuperpowerCommandOptions,
): void {
command
.command("lock")
.description("Generate workflow.lock.json skill fingerprints for a local GetSuperpower.")
.argument("<source>", "local GetSuperpower directory or workflow.json path")
.action(async (source: string) => {
const bundle = await loadWorkflowBundle(source, {
cwd: options.rootDir,
...(options.workflowGitCommandRunner
? { runGitCommand: options.workflowGitCommandRunner }
: {}),
});
try {
if (bundle.source.kind !== "local") {
throw new Error(
"GetSuperpower lock can only write local workflow sources. Clone or open the workflow source directory, then run lock there.",
);
}
const result = await writeWorkflowLockFile(bundle);
console.log(success(`GetSuperpower lock written: ${bundle.manifest.name}`));
console.log(keyValue("Lock file", result.path));
console.log(keyValue("Skills", String(result.lock.skills.length)));
} finally {
await bundle.cleanup?.();
}
});
}
function configureInstallCommand(

@@ -271,2 +358,5 @@ command: Command,

const installPrompt = options.installPrompt ?? createDefaultInstallPrompt();
let preparedDependencies:
| Awaited<ReturnType<typeof getPreparedWorkflowSkillInstallDependencies>>
| undefined;

@@ -298,3 +388,5 @@ try {

const skillDependencies = getWorkflowSkillInstallDependencies(bundle);
preparedDependencies = await getPreparedWorkflowSkillInstallDependencies({ bundle });
const skillDependencies = preparedDependencies.dependencies;
const installArtifacts: WorkflowInstallSkillArtifact[] = [];
for (const [index, skillDependency] of skillDependencies.entries()) {

@@ -315,2 +407,13 @@ const displaySkill = skillPlans[index]?.source ?? skillDependency.source;

const manifestSource = skillPlans[index]?.source ?? skillDependency.source;
for (const target of skillResult.skillInstall.targets) {
installArtifacts.push({
source: manifestSource,
skillName: skillResult.skillInstall.skillName,
agent: target.agent,
status: target.status,
paths: target.artifactPaths,
});
}
options.printSkillInstallResult(skillResult.skillInstall, "install", {

@@ -322,3 +425,3 @@ showPostSkillChangeWelcome: false,

const install = await installWorkflowBundle({ rootDir: targetDir, bundle });
const install = await installWorkflowBundle({ rootDir: targetDir, bundle, installArtifacts });

@@ -336,2 +439,3 @@ console.log(success(`GetSuperpower installed: ${install.workflow.name}`));

} finally {
await preparedDependencies?.cleanup?.();
await bundle.cleanup?.();

@@ -610,2 +714,86 @@ }

function configureRemoveCommand(
command: Command,
options: ConfigureGetSuperpowerCommandOptions,
): void {
command
.command("remove")
.description("Remove an installed GetSuperpower and its recorded skill artifacts.")
.argument("<workflow-name>", "installed GetSuperpower workflow name")
.option("--dir <dir>", "override directory with .getsuperpower/workflows")
.option("--home <dir>", "home directory with global GetSuperpower records", homedir())
.option("--dry-run", "print the removal plan without deleting files", false)
.option("--yes", "remove without prompting for confirmation", false)
.action((workflowName: string, commandOptions: GetSuperpowerRemoveCommandOptions) =>
runGetSuperpowerRemove(workflowName, commandOptions, options),
);
}
async function runGetSuperpowerRemove(
workflowName: string,
commandOptions: GetSuperpowerRemoveCommandOptions,
options: ConfigureGetSuperpowerCommandOptions,
): Promise<void> {
const homeDir = resolveHomePath(commandOptions.home);
const targetDir = commandOptions.dir ? resolvePath(options.rootDir, commandOptions.dir) : homeDir;
const plan = await createWorkflowRemovalPlan({
rootDir: targetDir,
homeDir,
workflowName,
});
printGetSuperpowerRemovePlan(plan, commandOptions.dryRun);
if (commandOptions.dryRun) {
return;
}
const prompt = options.removePrompt ?? createDefaultRemovePrompt();
const approved =
commandOptions.yes ||
(await prompt.confirmRemove({
workflowName,
artifactsToRemove: plan.artifactsToRemove.length,
artifactsToKeep: plan.artifactsToKeep.length,
}));
if (!approved) {
console.log(warning("GetSuperpower remove cancelled."));
return;
}
await executeWorkflowRemovalPlan(plan);
console.log(success(`GetSuperpower removed: ${workflowName}`));
}
function printGetSuperpowerRemovePlan(plan: WorkflowRemovalPlan, dryRun: boolean): void {
console.log(success(`GetSuperpower remove plan: ${plan.workflow.name}`));
console.log(keyValue("Workflow record", plan.workflowRecordPath));
if (plan.legacy) {
console.log(warning("Legacy workflow record detected; removal paths are inferred."));
}
const removeHeading = dryRun ? "Artifacts that would be removed:" : "Artifacts to remove:";
console.log(removeHeading);
if (plan.artifactsToRemove.length === 0) {
console.log("- none");
} else {
for (const artifact of plan.artifactsToRemove) {
console.log(`- ${artifact.path}`);
}
}
if (plan.artifactsToKeep.length > 0) {
console.log("Artifacts kept:");
for (const artifact of plan.artifactsToKeep) {
console.log(`- ${artifact.path} (still used by ${artifact.usedByWorkflows.join(", ")})`);
}
}
if (plan.skippedArtifacts.length > 0) {
console.log("Skipped artifacts:");
for (const artifact of plan.skippedArtifacts) {
console.log(`- ${artifact.source}: ${artifact.reason}`);
}
}
}
function configureDependencyCommand(

@@ -639,2 +827,141 @@ command: Command,

function configureLoopCommand(
command: Command,
options: ConfigureGetSuperpowerCommandOptions,
): void {
const loopCommand = command.command("loop").description("Control looped workflow runs.");
configureLoopSubcommand(loopCommand, "start", "Start a looped workflow run.", options)
.option("--run <id>", "run id to create")
.option("--json", "print JSON output", false);
configureLoopSubcommand(loopCommand, "status", "Show looped workflow run status.", options)
.option("--run <id>", "run id to inspect")
.option("--latest", "select the latest active run", false)
.option("--json", "print JSON output", false);
configureLoopSubcommand(loopCommand, "log", "Append a structured loop event.", options)
.option("--run <id>", "run id to append to")
.option("--type <event-type>", "structured event type")
.option("--step <step-id>", "override the event step id")
.option("--message <message>", "event message")
.option("--metadata <json>", "event metadata as JSON")
.option("--json", "print JSON output", false);
configureLoopSubcommand(loopCommand, "advance", "Advance a looped workflow run.", options)
.option("--run <id>", "run id to advance")
.option("--to <step-id>", "force advancement to a specific step")
.option("--force", "allow forced advancement when paired with --reason", false)
.option("--reason <reason>", "reason for forced advancement")
.option("--json", "print JSON output", false);
configureLoopSubcommand(loopCommand, "summary", "Write a looped workflow run summary.", options)
.option("--run <id>", "run id to summarize")
.option("--latest", "select the latest active run", false)
.option("--json", "print JSON output", false);
}
function configureLoopSubcommand(
command: Command,
name: GetSuperpowerLoopCommandName,
description: string,
options: ConfigureGetSuperpowerCommandOptions,
): Command {
return command
.command(name)
.description(description)
.argument(
"<source>",
"workflow alias, local GetSuperpower path, workflow.json path, or public git source",
)
.option("--home <dir>", "home directory for global GetSuperpower loop run state", homedir())
.action((source: string, commandOptions: GetSuperpowerLoopCommandOptions) =>
runGetSuperpowerLoop(name, source, commandOptions, options),
);
}
async function runGetSuperpowerLoop(
command: GetSuperpowerLoopCommandName,
source: string,
commandOptions: GetSuperpowerLoopCommandOptions,
options: ConfigureGetSuperpowerCommandOptions,
): Promise<void> {
const homeDir = resolveHomePath(commandOptions.home);
const bundle = await loadWorkflowBundle(source, {
cwd: options.rootDir,
...(options.workflowGitCommandRunner
? { runGitCommand: options.workflowGitCommandRunner }
: {}),
});
try {
if (!bundle.manifest.loop) {
throw new Error(`GetSuperpower is not loop-enabled: ${bundle.manifest.name}`);
}
const { runWorkflowLoopCli } = (await importWorkflowLoopRuntime()) as WorkflowLoopRuntimeModule;
const exitCode = await runWorkflowLoopCli({
argv: buildLoopRuntimeArgs(command, commandOptions),
workflowJson: bundle.manifestPath,
cwd: options.rootDir,
homeDir,
commandPrefix: createLoopCommandPrefix(source, homeDir),
});
if (exitCode !== 0) {
process.exitCode = exitCode;
}
} finally {
await bundle.cleanup?.();
}
}
async function importWorkflowLoopRuntime(): Promise<unknown> {
const runtimeModulePath = "./runtimes/getsuperpower/workflow-loop-runtime.mjs";
return import(runtimeModulePath);
}
function createLoopCommandPrefix(source: string, homeDir: string): (command: string) => string {
const defaultHomeDir = resolveHomePath(homedir());
const homeOption = homeDir === defaultHomeDir ? "" : ` --home ${quoteShellArg(homeDir)}`;
return (command) => `getsuperpower loop ${command} ${quoteShellArg(source)}${homeOption}`;
}
function buildLoopRuntimeArgs(
command: GetSuperpowerLoopCommandName,
options: GetSuperpowerLoopCommandOptions,
): string[] {
const args = [command];
appendStringOption(args, "run", options.run);
appendStringOption(args, "type", options.type);
appendStringOption(args, "step", options.step);
appendStringOption(args, "message", options.message);
appendStringOption(args, "metadata", options.metadata);
appendStringOption(args, "to", options.to);
appendStringOption(args, "reason", options.reason);
appendBooleanOption(args, "latest", options.latest);
appendBooleanOption(args, "force", options.force);
appendBooleanOption(args, "json", options.json);
return args;
}
function appendStringOption(args: string[], name: string, value: string | undefined): void {
if (value !== undefined) {
args.push(`--${name}`, value);
}
}
function appendBooleanOption(args: string[], name: string, value: boolean | undefined): void {
if (value === true) {
args.push(`--${name}`);
}
}
function quoteShellArg(value: string): string {
if (/^[a-zA-Z0-9_./:@#-]+$/.test(value)) {
return value;
}
return `'${value.replaceAll("'", "'\\''")}'`;
}
function configureOnboardCommand(

@@ -769,2 +1096,25 @@ command: Command,

function createDefaultRemovePrompt(): GetSuperpowerRemovePrompt {
return {
confirmRemove: async (input) => {
if (!process.stdin.isTTY) {
console.log(muted("Non-interactive shell detected; pass --yes to remove."));
return false;
}
const result = await clackConfirm({
message: `Remove ${input.workflowName} and ${input.artifactsToRemove} skill artifacts?`,
initialValue: false,
});
if (isCancel(result)) {
clackCancel("GetSuperpower remove cancelled");
return false;
}
return result;
},
};
}
function resolvePath(rootDir: string, path: string): string {

@@ -771,0 +1121,0 @@ return isAbsolute(path) ? path : resolve(join(rootDir, path));

export * from "./plugins";
export * from "./runtimes/ponytrail";
export * from "./runtimes/getsuperpower";
export * from "./skills";

@@ -29,2 +29,3 @@ import { constants, type Dirent } from "node:fs";

destination: string;
artifactPaths: string[];
status: SkillInstallStatus;

@@ -106,2 +107,8 @@ }

},
{
source: "superpowers:verification-before-completion",
displayName: "verification-before-completion",
skillFolder: "verification-before-completion",
installName: "superpowers-verification-before-completion",
},
] as const satisfies readonly SupportedSuperpowersSkill[];

@@ -158,2 +165,3 @@ const bundledSkillAliases: Record<string, string> = {

const mirrorDestinations = getSkillMirrorDestinations(input.homeDir, agent, source.name);
const artifactPaths = [destination, ...mirrorDestinations];
const handledStatus = handledDestinations.get(destination);

@@ -170,3 +178,3 @@ if (handledStatus) {

}
targets.push({ agent, destination, status: handledStatus });
targets.push({ agent, destination, artifactPaths, status: handledStatus });
if (input.installPrehook && hasPrehookSupport(agent)) {

@@ -211,3 +219,3 @@ prehooks.push(

handledDestinations.set(destination, status);
targets.push({ agent, destination, status });
targets.push({ agent, destination, artifactPaths, status });

@@ -265,2 +273,10 @@ if (input.installPrehook && hasPrehookSupport(agent)) {

if (!looksLikePath(sourceOrName)) {
const homeDir = options.homeDir ?? process.env.HOME ?? process.cwd();
const installedSkillPath = await findInstalledSkillPath(homeDir, sourceOrName);
if (installedSkillPath) {
return resolvePathSkill(installedSkillPath);
}
}
if (await pathExists(pathCandidate)) {

@@ -267,0 +283,0 @@ return resolvePathSkill(pathCandidate);

Sorry, the diff of this file is too big to display