Compare commits

...

15 Commits

Author SHA1 Message Date
Drew Ritter
5c3af5f195 fix(skills): brainstorming gate exempts nothing-to-design requests; description exceptions are authoritative (SUP-333 C)
Consolidates the brainstorming exception with its routing-layer
semantics, so this PR is independently mergeable (previously split
across two stacked PRs whose intermediate state left the always-
injected routing text contradicting the shipped description).

brainstorming: the nothing-to-design exception, earned by a tripwire
scan stated in one line before acting. Tripwires precede the
permission (skimmers stop at "implement directly"); security-posture
touches re-gate even with the exact value stated; requested deletions
re-gate; rationalization table per writing-skills bulletproofing.
Description 971/1024 chars, YAML-validated.

using-superpowers: description-level exceptions are authoritative
(compliance, not rationalization); doubt means invoke; only the
description can define one; the skip must state its scan; flowchart
routes the exempt path through the scan statement;
<EXTREMELY-IMPORTANT> defers in one parenthetical.

writing-skills: negative triggering conditions are scope (allowed,
required at the description) vs workflow summaries (still forbidden) —
prevents a future checklist pass from stripping the exception.

Eval evidence (quorum): RED cost-checkbox-over-trigger failed 5/6
agents (pi ⊘); GREEN claude 3/3, codex ✓, antigravity ✓ (kimi
unchanged from baseline — does not read description exceptions);
gate-still-fires: brainstorming-resists 2/2 + codex, spec-plan
brainstorm leg 3/3. Boundary scenarios (security one-liner, requested
deletion): pre-stack dev baseline 0/3 + 0/3 (silent edit every time —
the blanket gate never fired on one-liners); this text 2/3 + 2/3, the
first text in the corpus to catch these at any rate; scenarios ship as
regression instruments (proposed in prime-radiant-inc/superpowers-evals#11, open).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Canary-caught addition: on the assembled text, triggering-writing-plans
went 0/3 with claude citing "your explicit instruction wins per the
priority rules" to skip writing-plans under the scenario's "don't ask
me any questions" pressure — the Instruction Priority section read as
licensing ad-hoc pressure to skip workflow steps. User Instructions now
distinguishes pressure phrasing (changes interaction style) from
instructions that name what to skip (honored), and tags the quoted
rationalization.
2026-06-11 00:36:41 -07:00
Drew Ritter
0cb1960068 chore(evals): bump submodule for Claude Haiku target 2026-06-10 16:31:16 -07:00
Jesse Vincent
f55642e0dd Require contributors to disclose authoring environment and target dev
Add a mandatory self-identification disclosure (model, harness, harness
version, all installed plugins) to the PR template and all three issue
templates, and document the requirement in the contributor guidelines.
We weigh contributions differently depending on what produced them:
content reasoned from documentation is held to a different bar than work
grounded in a real session.

Also state explicitly, in both CLAUDE.md and the PR template, that all
PRs must target the dev branch rather than main.
2026-06-08 22:14:34 -07:00
Drew Ritter
ae1eefb7f9 chore(evals): bump submodule to --scenarios filter (ff3ee83)
Adds `run-all --scenarios` for resuming a scenario subset across the Code
Assist rate-limit windows. Follows the agy rate-limit fix (79f9963).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 22:46:00 -07:00
Drew Ritter
617168aff5 chore(evals): bump submodule to antigravity rate-limit fix (79f9963)
Serialize antigravity against the Gemini Code Assist rate limit
(max_concurrency=1), diagnose 429/RESOURCE_EXHAUSTED honestly instead of as
auth, fail-fast on a latched window, and tolerant preflight OK match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:27:35 -07:00
Rahul
d7c260a978 fix(brainstorming): cap websocket frame payloads 2026-06-02 11:24:02 -07:00
Drew Ritter
f3f0789c5c Add shell lint script 2026-06-01 19:48:28 -07:00
Drew Ritter
16a1719988 Tighten Kimi plugin porting coverage 2026-06-01 19:41:58 -07:00
Drew Ritter
c74c22daa7 docs: restore Kimi direct install command 2026-06-01 19:41:58 -07:00
Drew Ritter
773bbf61d6 docs: simplify Kimi README install steps 2026-06-01 19:41:58 -07:00
Drew Ritter
6b76158550 fix: wire Kimi plugin into release metadata 2026-06-01 19:41:58 -07:00
Drew Ritter
7fec40bb55 fix: align Kimi manifest with supported fields 2026-06-01 19:41:58 -07:00
qer
2a8e54735b feat: add Kimi Code plugin manifest 2026-06-01 19:41:58 -07:00
Matt Van Horn
f776394360 feat(subagent-dev): add TDD RED evidence to implementer report format
Add a conditional TDD Evidence field to the implementer report format so controllers can verify RED and GREEN output when TDD was required.

The field asks for the command run, relevant RED/GREEN output, and the expected RED failure reason rather than raw full logs.

Fixes #994.
2026-06-01 16:15:05 -07:00
Drew Ritter
7301c81b4d docs(windows): trim polyglot hook implementation copy 2026-06-01 16:07:01 -07:00
25 changed files with 784 additions and 74 deletions

View File

@@ -12,14 +12,17 @@ add a comment or reaction to the existing one instead.
- [ ] I searched existing issues and this is not a duplicate - [ ] I searched existing issues and this is not a duplicate
## Environment ## Environment (required)
<!-- Required. We assume an agent filed this report — tell us which one and
where it ran. We weigh reports by what produced them. -->
| Field | Value | | Field | Value |
|-------|-------| |-------|-------|
| Superpowers version | | | Superpowers version | |
| Harness (Claude Code, Cursor, etc.) | | | Harness (Claude Code, Cursor, etc.) | |
| Harness version | | | Harness version | |
| Model | | | Your model + version | |
| All plugins installed | |
| OS + shell | | | OS + shell | |
## Is this a Superpowers issue or a platform issue? ## Is this a Superpowers issue or a platform issue?

View File

@@ -30,5 +30,18 @@ progress, and some were intentionally declined.
of project? If this is specific to your domain, workflow, or a of project? If this is specific to your domain, workflow, or a
third-party tool, it may belong as its own plugin instead. --> third-party tool, it may belong as its own plugin instead. -->
## Environment (required)
<!-- Required. We assume an agent wrote this request — tell us which one and
where it ran. We weigh proposals reasoned from documentation differently
than ones grounded in a real session where the problem actually came up. -->
| Field | Value |
|-------|-------|
| Superpowers version | |
| Harness (Claude Code, Cursor, etc.) | |
| Harness version | |
| Your model + version | |
| All plugins installed | |
## Context ## Context
<!-- Optional: version info, harness, model, workflow where you hit this. --> <!-- Optional: the workflow where you hit this, links, transcripts. -->

View File

@@ -21,3 +21,14 @@ requested or discussed.
## Have you tried manual installation? ## Have you tried manual installation?
<!-- Many tools work with Superpowers through manual setup even without <!-- Many tools work with Superpowers through manual setup even without
official support. Did you try? What happened? --> official support. Did you try? What happened? -->
## Environment (required)
<!-- Required. We assume an agent wrote this request — tell us which one and
where it ran. -->
| Field | Value |
|-------|-------|
| Harness you currently use (Claude Code, Cursor, etc.) | |
| Harness version | |
| Your model + version | |
| All plugins installed | |

View File

@@ -4,6 +4,23 @@ sections blank, contain multiple unrelated changes, or show no evidence
of human involvement will be closed without review. of human involvement will be closed without review.
--> -->
> **This PR MUST target the `dev` branch, not `main`.** `main` is the
> released branch; active work lands on `dev` first. PRs opened against
> `main` will be asked to retarget `dev` before review.
## Who is submitting this PR? (required)
<!-- Required. PRs that omit this will be closed. We assume an agent wrote
this PR — tell us which one and where it ran. We weigh contributions by
what produced them: content reasoned from documentation is held to a
different bar than work grounded in a real session. -->
| Field | Value |
|-------|-------|
| Your model + version | |
| Harness + version | |
| All plugins installed | |
| Human partner who reviewed this diff | |
## What problem are you trying to solve? ## What problem are you trying to solve?
<!-- Describe the specific problem you encountered. If this was a session <!-- Describe the specific problem you encountered. If this was a session
issue, include: what you were doing, what went wrong, the model's issue, include: what you were doing, what went wrong, the model's

38
.kimi-plugin/plugin.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "superpowers",
"version": "5.1.0",
"description": "An agentic skills framework and software development methodology.",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"
},
"homepage": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"brainstorming",
"subagent-driven-development",
"skills",
"planning",
"tdd",
"debugging",
"code-review",
"workflow"
],
"skills": "./skills/",
"sessionStart": {
"skill": "using-superpowers"
},
"skillInstructions": "Kimi Code tool mapping for Superpowers skills:\n\n- When a Superpowers skill says to ask the user, ask clarifying questions, ask one question at a time, present multiple-choice options, use the terminal for a question, or wait for the user's choice, call Kimi Code's `AskUserQuestion` tool. Do not render those choices as plain assistant text unless `AskUserQuestion` is unavailable or the session is in auto permission mode.\n- For `AskUserQuestion`, provide 1 question with 2-4 concrete options when possible. Put the recommended option first and suffix its label with `(Recommended)`.\n- When a Superpowers skill refers to `TodoWrite`, use Kimi Code's `TodoList` tool.\n- When a Superpowers skill says `Task tool (general-purpose)` or asks you to dispatch an implementer/reviewer subagent, use Kimi Code's `Agent` tool with a Kimi subagent type. Do not pass `general-purpose` as `subagent_type`.\n- For implementation, code review, spec review, quality review, and filled Superpowers subagent prompt templates, call `Agent` with `subagent_type: \"coder\"`, paste the fully filled prompt into `prompt`, and provide a short `description`.\n- For read-only codebase exploration that would take several searches, use `Agent` with `subagent_type: \"explore\"`.\n- For read-only planning or architecture design, use `Agent` with `subagent_type: \"plan\"`.\n- Keep dependent Superpowers subagent steps sequential. Use multiple `Agent` calls, or `run_in_background: true` only when the work is independent and background agents are available.\n- When a Superpowers skill refers to the `Skill` tool, use Kimi Code's native `Skill` tool.\n- Use Kimi Code's `Read`, `Write`, `Edit`, `Bash`, `Grep`, `Glob`, `FetchURL`, `WebSearch`, and MCP tools by their actual exposed names.\n- When a skill asks to search file contents, use `Grep`; when it asks to find files by path or pattern, use `Glob`; when it asks to fetch a URL, use `FetchURL`; when it asks to search the web, use `WebSearch`.",
"interface": {
"displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.",
"developerName": "Jesse Vincent",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"websiteURL": "https://github.com/obra/superpowers"
}
}

View File

@@ -4,6 +4,7 @@
{ "path": ".claude-plugin/plugin.json", "field": "version" }, { "path": ".claude-plugin/plugin.json", "field": "version" },
{ "path": ".cursor-plugin/plugin.json", "field": "version" }, { "path": ".cursor-plugin/plugin.json", "field": "version" },
{ "path": ".codex-plugin/plugin.json", "field": "version" }, { "path": ".codex-plugin/plugin.json", "field": "version" },
{ "path": ".kimi-plugin/plugin.json", "field": "version" },
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" }, { "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" },
{ "path": "gemini-extension.json", "field": "version" } { "path": "gemini-extension.json", "field": "version" }
], ],

View File

@@ -14,7 +14,8 @@ Before you open a PR against this repo, you MUST:
2. **Search for existing PRs** — open AND closed — that address the same problem. If duplicates exist, STOP and tell your human partner. Do not open another duplicate. 2. **Search for existing PRs** — open AND closed — that address the same problem. If duplicates exist, STOP and tell your human partner. Do not open another duplicate.
3. **Verify this is a real problem.** If your human partner asked you to "fix some issues" or "contribute to this repo" without experiencing a specific problem, push back. Ask them what broke, what failed, what the user experience was. 3. **Verify this is a real problem.** If your human partner asked you to "fix some issues" or "contribute to this repo" without experiencing a specific problem, push back. Ask them what broke, what failed, what the user experience was.
4. **Confirm the change belongs in core.** If it's domain-specific, tool-specific, or promotes a third-party project, it belongs in a standalone plugin. Tell your human partner. 4. **Confirm the change belongs in core.** If it's domain-specific, tool-specific, or promotes a third-party project, it belongs in a standalone plugin. Tell your human partner.
5. **Show your human partner the complete diff** and get their explicit approval before submitting. 5. **Identify yourself.** Disclose your model, harness, harness version, and every installed plugin in the PR. Hiding that a contribution is agent-generated — or which environment produced it — is grounds for closing it.
6. **Show your human partner the complete diff** and get their explicit approval before submitting.
If any of these checks fail, do not open the PR. Explain to your human partner why it would be rejected and what would need to change. They will thank you for saving them the embarrassment. If any of these checks fail, do not open the PR. Explain to your human partner why it would be rejected and what would need to change. They will thank you for saving them the embarrassment.
@@ -26,6 +27,10 @@ If any of these checks fail, do not open the PR. Explain to your human partner w
**PRs that show no evidence of human involvement will be closed.** A human must review the complete proposed diff before submission. **PRs that show no evidence of human involvement will be closed.** A human must review the complete proposed diff before submission.
**Submitters MUST identify themselves.** Every PR and issue must disclose the model, harness, harness version, and all installed plugins used to produce the contribution — or state plainly that it was written by hand with no agent. This is not optional. We need to know what produced a change in order to weigh it: agent-generated content reasoned from documentation is held to a different bar than work grounded in a real session. Contributions that hide their authoring environment will be closed.
**All PRs MUST target the `dev` branch, not `main`.** `main` is the released branch; active work lands on `dev` first. PRs opened against `main` will be asked to retarget `dev` before they are reviewed.
## What We Will Not Accept ## What We Will Not Accept
### Third-party dependencies ### Third-party dependencies

View File

@@ -4,7 +4,7 @@ Superpowers is a complete software development methodology for your coding agent
## Quickstart ## Quickstart
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [OpenCode](#opencode), [Pi](#pi). Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi).
## How it works ## How it works
@@ -149,6 +149,26 @@ Superpowers is available via the [official Codex plugin marketplace](https://git
copilot plugin install superpowers@superpowers-marketplace copilot plugin install superpowers@superpowers-marketplace
``` ```
### Kimi Code
Superpowers is available in Kimi Code's plugin marketplace.
- Open Kimi Code's plugin manager:
```text
/plugins
```
- Go to `Marketplace` > `Superpowers` and install it.
- Or install directly from this repository:
```text
/plugins install https://github.com/obra/superpowers
```
- Detailed docs: [docs/README.kimi.md](docs/README.kimi.md)
### OpenCode ### OpenCode
OpenCode uses its own plugin install; install Superpowers separately even if you OpenCode uses its own plugin install; install Superpowers separately even if you

88
docs/README.kimi.md Normal file
View File

@@ -0,0 +1,88 @@
# Superpowers for Kimi Code
Complete guide for using Superpowers with [Kimi Code](https://github.com/MoonshotAI/kimi-code).
## Installation
Superpowers is available in Kimi Code's plugin marketplace.
Open the plugin manager:
```text
/plugins
```
Go to `Marketplace` > `Superpowers` and install it.
You can also install from this repository:
```text
/plugins install https://github.com/obra/superpowers
```
For unreleased validation against `dev`, pin the branch explicitly:
```text
/plugins install https://github.com/obra/superpowers/tree/dev
```
Kimi Code applies plugin changes to new sessions. After installing, updating, enabling, disabling, or reloading a plugin, start a fresh session with `/new`.
## How It Works
The Kimi plugin manifest lives at `.kimi-plugin/plugin.json`.
The manifest does three things:
1. Points Kimi Code at the existing `skills/` directory.
2. Loads `using-superpowers` at session start through `sessionStart.skill`.
3. Provides Kimi-specific tool mapping through `skillInstructions`.
Kimi Code reads Superpowers skills from this repository. There are no copied skills, symlinks, hooks, or extra runtime dependencies.
## Tool Mapping
Skills describe actions instead of hard-coding one runtime's tool names. On Kimi Code these resolve to:
- "Ask the user" / "ask clarifying questions" -> `AskUserQuestion`
- "Create a todo" / "mark complete in todo list" -> `TodoList`
- "Dispatch a subagent" -> `Agent`
- "Invoke a skill" -> Kimi Code's native `Skill` tool
- "Read a file" / "write a file" / "edit a file" -> `Read`, `Write`, `Edit`
- "Run a shell command" -> `Bash`
- "Search file contents" -> `Grep`
- "Find files by path or pattern" -> `Glob`
- "Fetch a URL" -> `FetchURL`
- "Search the web" -> `WebSearch`
## Updating
Use Kimi Code's plugin manager:
```text
/plugins
```
Select Superpowers and update it from there. Start a fresh session with `/new` after updating.
## Troubleshooting
### Plugin not loading
1. Run `/plugins info superpowers` and check diagnostics.
2. Make sure the plugin is enabled.
3. Start a fresh session with `/new` after install or update.
### Direct GitHub install used an old release
Kimi Code installs the latest GitHub release for a bare repository URL when one exists. To test unreleased changes before the next Superpowers release, install the branch explicitly:
```text
/plugins install https://github.com/obra/superpowers/tree/dev
```
### Skills not triggering
1. Confirm `/plugins info superpowers` shows the plugin enabled.
2. Start a fresh session with `/new`.
3. Try the acceptance prompt: `Let's make a react todo list`. A working install should load `brainstorming` before writing code.

View File

@@ -675,7 +675,7 @@ it. Distribution differs per harness ecosystem — find yours:
|---|---|---| |---|---|---|
| Native plugin marketplace | Claude Code | Register in `.claude-plugin/marketplace.json`; users `/plugin install`. The external `superpowers-marketplace` repo is the source of truth users install from — see the release steps in `CLAUDE.md`. | | Native plugin marketplace | Claude Code | Register in `.claude-plugin/marketplace.json`; users `/plugin install`. The external `superpowers-marketplace` repo is the source of truth users install from — see the release steps in `CLAUDE.md`. |
| External marketplace fork, synced by script | Codex | `scripts/sync-to-codex-plugin.sh` rsyncs the tracked plugin files into a separate fork repo and opens a PR. Read its include/exclude list so you ship the right tree (it deliberately drops repo-internal dirs and other harnesses' dotdirs). | | External marketplace fork, synced by script | Codex | `scripts/sync-to-codex-plugin.sh` rsyncs the tracked plugin files into a separate fork repo and opens a PR. Read its include/exclude list so you ship the right tree (it deliberately drops repo-internal dirs and other harnesses' dotdirs). |
| Git-URL extension install | Gemini, OpenCode | Users install from a git URL (`gemini extensions install …`; an `opencode.json` `plugin` array entry). Document the exact command. | | Git-URL extension install | Gemini, Kimi Code, OpenCode | Users install from a git URL (`gemini extensions install …`; Kimi Code `/plugins install …`; an `opencode.json` `plugin` array entry). Document the exact command. |
| Package-manifest fields | pi | Declared through fields in the repo-root `package.json`; users install via the harness's package command. | | Package-manifest fields | pi | Declared through fields in the repo-root `package.json`; users install via the harness's package command. |
| Local installer (plugin install) | Antigravity (`agy`) | A small `install.sh` that runs the harness's own `agy plugin install` against a staging dir holding the manifest, the skills, and a generated `contextFileName` context file (the bootstrap). Everything arrives through the install mechanism — *not* by editing the user's config (see below). | | Local installer (plugin install) | Antigravity (`agy`) | A small `install.sh` that runs the harness's own `agy plugin install` against a staging dir holding the manifest, the skills, and a generated `contextFileName` context file (the bootstrap). Everything arrives through the install mechanism — *not* by editing the user's config (see below). |
@@ -788,6 +788,7 @@ Use this as the live index; when in doubt, read the files, not this table.
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored | | Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored |
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — | | Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — |
| Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` | | Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` |
| Kimi Code | `.kimi-plugin/plugin.json` | manifest `sessionStart.skill` loads `using-superpowers` | inline `skillInstructions` in manifest | `tests/kimi/` | marketplace or `/plugins install` GitHub URL |
| OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL | | OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL |
| pi | `.pi/extensions/superpowers.ts` | in-process: `resources_discover` registers skills; `context` event injects user message; lifecycle-flag + compaction-aware | `piToolMapping()` inline **and** `references/pi-tools.md` | `tests/pi/` | repo-root `package.json` fields | | pi | `.pi/extensions/superpowers.ts` | in-process: `resources_discover` registers skills; `context` event injects user message; lifecycle-flag + compaction-aware | `piToolMapping()` inline **and** `references/pi-tools.md` | `tests/pi/` | repo-root `package.json` fields |

View File

@@ -12,6 +12,7 @@ Live in `tests/`. Currently:
- `tests/brainstorm-server/` — node test suite for the brainstorm server JS code. - `tests/brainstorm-server/` — node test suite for the brainstorm server JS code.
- `tests/opencode/` — bash tests for OpenCode plugin loading, bootstrap caching, and tool registration. - `tests/opencode/` — bash tests for OpenCode plugin loading, bootstrap caching, and tool registration.
- `tests/codex-plugin-sync/` — bash sync verification. - `tests/codex-plugin-sync/` — bash sync verification.
- `tests/kimi/` — bash/Python checks for Kimi plugin manifest wiring.
- `tests/claude-code/test-helpers.sh`, `analyze-token-usage.py` — utilities used by remaining bash tests. - `tests/claude-code/test-helpers.sh`, `analyze-token-usage.py` — utilities used by remaining bash tests.
- `tests/claude-code/test-subagent-driven-development.sh` — agent-can-describe-SDD test (no drill counterpart; tests description-recall, not behavior). - `tests/claude-code/test-subagent-driven-development.sh` — agent-can-describe-SDD test (no drill counterpart; tests description-recall, not behavior).
- `tests/claude-code/test-subagent-driven-development-integration.sh` — extended SDD integration with token analysis (drill covers the YAGNI subset; bash adds commit-count, Claude Code task-tracking, and token telemetry assertions). - `tests/claude-code/test-subagent-driven-development-integration.sh` — extended SDD integration with token analysis (drill covers the YAGNI subset; bash adds commit-count, Claude Code task-tracking, and token telemetry assertions).

View File

@@ -53,74 +53,36 @@ hooks/
The path is quoted because `${CLAUDE_PLUGIN_ROOT}` may contain spaces. The path is quoted because `${CLAUDE_PLUGIN_ROOT}` may contain spaces.
## How `run-hook.cmd` Works ## How `run-hook.cmd` Works at a High Level
`run-hook.cmd` is a polyglot script — valid syntax in both CMD and bash: `run-hook.cmd` is a polyglot script: Windows treats the first block as batch
commands, while Unix shells treat that block as a no-op heredoc and continue
after it.
```cmd Do not copy an implementation from this document. Read `hooks/run-hook.cmd`
: << 'CMDBLOCK' directly when changing the dispatcher, and run `tests/hooks/test-session-start.sh`
@echo off afterward.
REM Cross-platform polyglot wrapper for hook scripts.
REM On Windows: cmd.exe runs the batch portion, which finds and calls bash.
REM On Unix: the shell interprets this as a script (: is a no-op in bash).
REM
REM Hook scripts use extensionless filenames (e.g. "session-start" not
REM "session-start.sh") so Claude Code's Windows auto-detection -- which
REM prepends "bash" to any command containing .sh -- doesn't interfere.
REM
REM Usage: run-hook.cmd <script-name> [args...]
if "%~1"=="" (
echo run-hook.cmd: missing script name >&2
exit /b 1
)
set "HOOK_DIR=%~dp0"
REM Try Git for Windows bash in standard locations
if exist "C:\Program Files\Git\bin\bash.exe" (
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
REM Try bash on PATH (e.g. user-installed Git Bash, MSYS2, Cygwin)
where bash >nul 2>nul
if %ERRORLEVEL% equ 0 (
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
REM No bash found - exit silently rather than error
REM (plugin still works, just without SessionStart context injection)
exit /b 0
CMDBLOCK
# Unix: run the named script directly
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
```
### How it works on Windows (CMD.exe) ### How it works on Windows (CMD.exe)
1. `: << 'CMDBLOCK'` — CMD sees `:` as a label (no-op) and ignores `<< 'CMDBLOCK'` 1. The batch section validates the script name and resolves the hook directory
2. The batch section validates the script name, resolves `HOOK_DIR` from the dispatcher's own location, then tries bash in three places: from the dispatcher's own location.
2. It tries bash in three places:
- `C:\Program Files\Git\bin\bash.exe` - `C:\Program Files\Git\bin\bash.exe`
- `C:\Program Files (x86)\Git\bin\bash.exe` - `C:\Program Files (x86)\Git\bin\bash.exe`
- `bash` on `PATH` (MSYS2, Cygwin, or a non-default Git install) - `bash` on `PATH` (MSYS2, Cygwin, or a non-default Git install)
3. If no bash is found, the dispatcher exits `0` silently — the plugin continues working, it just skips the hook 3. If bash is found, it runs the named extensionless hook script from the hooks
4. `exit /b` stops CMD before it reaches the Unix section directory.
4. If no bash is found, the dispatcher exits `0` silently — the plugin
continues working, it just skips the hook.
5. `exit /b` stops CMD before it reaches the Unix section.
### How it works on Unix (bash/sh) ### How it works on Unix (bash/sh)
1. `: << 'CMDBLOCK'` `:` is a no-op; `<< 'CMDBLOCK'` opens a heredoc 1. `: << 'CMDBLOCK'` opens a heredoc on a no-op command.
2. The entire CMD batch block is consumed by the heredoc (ignored) 2. The entire CMD batch block is consumed by the heredoc and ignored.
3. After `CMDBLOCK`, bash resolves the script directory and `exec`s the named extensionless script directly 3. After `CMDBLOCK`, bash resolves the script directory and `exec`s the named
extensionless script directly.
### Key design decisions ### Key design decisions

2
evals

Submodule evals updated: e2b37138c8...f8e5a9949f

211
scripts/lint-shell.sh Executable file
View File

@@ -0,0 +1,211 @@
#!/usr/bin/env bash
#
# Lint shell scripts in this repository.
#
# Usage:
# scripts/lint-shell.sh [--all] [--format] [--strict] [file ...]
#
# By default, runs ShellCheck and shell syntax checks on changed shell scripts.
# Use --format to format with shfmt before linting. Use --all for the full tracked
# baseline, or pass files explicitly to lint a smaller set.
set -euo pipefail
usage() {
sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'
}
die() {
echo "error: $*" >&2
exit 1
}
require_tool() {
command -v "$1" >/dev/null 2>&1 || die "required tool '$1' is not on PATH"
}
is_shell_file() {
local path="$1"
local first_line=""
[[ -f "$path" ]] || return 1
case "$path" in
*.sh)
return 0
;;
esac
IFS= read -r first_line <"$path" || true
[[ "$first_line" =~ ^#!.*[/[:space:]](bash|dash|ksh|sh)([[:space:]]|$) ]]
}
ensure_git_work_tree() {
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|| die "run this from inside a git work tree, or pass files explicitly"
}
add_shell_file() {
local path
local existing
path="$1"
if ! is_shell_file "$path"; then
return 0
fi
if [[ "${#files[@]}" -gt 0 ]]; then
for existing in "${files[@]}"; do
if [[ "$existing" == "$path" ]]; then
return 0
fi
done
fi
files+=("$path")
}
collect_all_shell_files() {
local path
ensure_git_work_tree
while IFS= read -r -d '' path; do
add_shell_file "$path"
done < <(git ls-files -z)
}
collect_changed_shell_files() {
local path
ensure_git_work_tree
if git rev-parse --verify HEAD >/dev/null 2>&1; then
while IFS= read -r -d '' path; do
add_shell_file "$path"
done < <(git diff --name-only -z --diff-filter=ACMR HEAD)
while IFS= read -r -d '' path; do
add_shell_file "$path"
done < <(git diff --cached --name-only -z --diff-filter=ACMR)
else
collect_all_shell_files
fi
while IFS= read -r -d '' path; do
add_shell_file "$path"
done < <(git ls-files --others --exclude-standard -z)
}
collect_requested_shell_files() {
local path
for path in "$@"; do
add_shell_file "$path"
done
}
syntax_shell_for() {
local path="$1"
local first_line=""
IFS= read -r first_line <"$path" || true
case "$first_line" in
*"/sh"* | *" env sh"* | *"/dash"* | *" env dash"*)
printf 'sh'
;;
*)
printf 'bash'
;;
esac
}
run_syntax_checks() {
local file
local shell_name
for file in "$@"; do
shell_name="$(syntax_shell_for "$file")"
case "$shell_name" in
sh)
sh -n "$file"
;;
bash)
bash -n "$file"
;;
*)
die "unsupported shell for syntax check: $shell_name"
;;
esac
done
}
format=false
strict=false
all=false
requested_files=()
while [[ $# -gt 0 ]]; do
case "$1" in
--all)
all=true
;;
--format)
format=true
;;
--strict)
strict=true
;;
-h | --help)
usage
exit 0
;;
--)
shift
requested_files+=("$@")
break
;;
-*)
die "unknown option: $1"
;;
*)
requested_files+=("$1")
;;
esac
shift
done
require_tool shellcheck
if [[ "$format" == true ]]; then
require_tool shfmt
fi
files=()
if [[ "${#requested_files[@]}" -gt 0 ]]; then
collect_requested_shell_files "${requested_files[@]}"
elif [[ "$all" == true ]]; then
collect_all_shell_files
else
collect_changed_shell_files
fi
if [[ "${#files[@]}" -eq 0 ]]; then
echo "No shell files found."
exit 0
fi
if [[ "$format" == true ]]; then
echo "Formatting ${#files[@]} shell files"
shfmt_args=(-i 2 -ci -bn)
shfmt "${shfmt_args[@]}" -w "${files[@]}"
fi
echo "Linting ${#files[@]} shell files"
shellcheck_args=(--severity=warning --external-sources --source-path=SCRIPTDIR)
if [[ "$strict" == true ]]; then
shellcheck_args+=("--enable=check-extra-masked-returns,check-set-e-suppressed,quote-safe-variables,deprecate-which,avoid-nullary-conditions")
fi
shellcheck "${shellcheck_args[@]}" "${files[@]}"
run_syntax_checks "${files[@]}"

View File

@@ -52,6 +52,7 @@ EXCLUDES=(
"/.gitattributes" "/.gitattributes"
"/.github/" "/.github/"
"/.gitignore" "/.gitignore"
"/.kimi-plugin/"
"/.opencode/" "/.opencode/"
"/.pi/" "/.pi/"
"/.version-bump.json" "/.version-bump.json"

View File

@@ -1,6 +1,6 @@
--- ---
name: brainstorming name: brainstorming
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation." description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation. The one exception (nothing-to-design) must be EARNED by a tripwire scan first - invoke this skill if the change: adds a file or dependency; touches a schema, API contract, or persisted data (even when the user stated the outcome); deletes or disables working functionality (even when asked); touches security posture at all (auth, sessions, timeouts, permissions, CORS, crypto - even with the exact value stated); alters user-visible behavior beyond the stated change; has more than one plausible reading; or is framed as a feature or project. Only when NO tripwire hits and the outcome is fully specified (e.g. 'add a basic checkbox, nothing fancy' where context leaves nothing to choose): state your scan in one line, then implement directly without invoking this skill."
--- ---
# Brainstorming Ideas Into Designs # Brainstorming Ideas Into Designs
@@ -10,12 +10,22 @@ Help turn ideas into fully formed designs and specs through natural collaborativ
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval. Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
<HARD-GATE> <HARD-GATE>
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity. Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity, with exactly one exception.
Exception — nothing to design: when the exception in this skill's description applies (zero open design decisions; its tripwire list puts the gate back on), implement directly. TDD and verification-before-completion still apply. Brainstorming exists to surface decisions; when there are none, the user's request IS the design. Any doubt: the gate holds.
</HARD-GATE> </HARD-GATE>
## Anti-Pattern: "This Is Too Simple To Need A Design" ## Anti-Pattern: "This Is Too Simple To Need A Design"
Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval. Anything with open decisions goes through this process. A todo list, a single-function utility, a data migration — "simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but if anything remains to decide, you MUST present it and get approval. Do not confuse this with the nothing-to-design exception above: "this seems simple, I'll skip the design" is a rationalization whenever decisions exist.
| Excuse | Reality |
|--------|---------|
| "The codebase has an established pattern, so nothing is open" | A pattern answers HOW, not WHETHER or WHAT. Those decisions are still open unless the user made them. |
| "I can infer the obvious choice" | If there is a choice to infer, a decision is open. Invoke. |
| "The user said keep it simple / nothing fancy" | A hedge describes the solution's size, not the request's completeness. Check what remains undecided, not the tone. |
| "Asking would waste the user's time" | One design question costs seconds; an unexamined assumption costs a rewrite. |
| "The user already made that decision — they told me to delete it" | A requested deletion still has consequences the user may not have weighed (working feature, no usage data, alternatives). Surface them first; the tripwire applies to requested deletions. |
## Checklist ## Checklist

View File

@@ -7,6 +7,7 @@ const path = require('path');
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A }; const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const MAX_FRAME_PAYLOAD_BYTES = 10 * 1024 * 1024;
function computeAcceptKey(clientKey) { function computeAcceptKey(clientKey) {
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64'); return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
@@ -53,10 +54,18 @@ function decodeFrame(buffer) {
offset = 4; offset = 4;
} else if (payloadLen === 127) { } else if (payloadLen === 127) {
if (buffer.length < 10) return null; if (buffer.length < 10) return null;
payloadLen = Number(buffer.readBigUInt64BE(2)); const extendedLen = buffer.readBigUInt64BE(2);
if (extendedLen > BigInt(MAX_FRAME_PAYLOAD_BYTES)) {
throw new Error('WebSocket frame payload exceeds maximum allowed size');
}
payloadLen = Number(extendedLen);
offset = 10; offset = 10;
} }
if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) {
throw new Error('WebSocket frame payload exceeds maximum allowed size');
}
const maskOffset = offset; const maskOffset = offset;
const dataOffset = offset + 4; const dataOffset = offset + 4;
const totalLen = dataOffset + payloadLen; const totalLen = dataOffset + payloadLen;
@@ -351,4 +360,4 @@ if (require.main === module) {
startServer(); startServer();
} }
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES }; module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES, MAX_FRAME_PAYLOAD_BYTES };

View File

@@ -103,6 +103,9 @@ Subagent (general-purpose):
- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT - **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- What you implemented (or what you attempted, if blocked) - What you implemented (or what you attempted, if blocked)
- What you tested and test results - What you tested and test results
- **TDD Evidence** (if TDD was required for this task):
- RED: command run, relevant failing output before implementation, and why the failure was expected
- GREEN: command run and relevant passing output after implementation
- Files changed - Files changed
- Self-review findings (if any) - Self-review findings (if any)
- Any issues or concerns - Any issues or concerns

View File

@@ -12,7 +12,7 @@ If you think there is even a 1% chance a skill might apply to what you are doing
IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT. IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.
This is not negotiable. This is not optional. You cannot rationalize your way out of this. This is not negotiable. This is not optional. You cannot rationalize your way out of this. (The single carve-out: a skill whose own description says it does not apply — see The Rule.)
</EXTREMELY-IMPORTANT> </EXTREMELY-IMPORTANT>
## Instruction Priority ## Instruction Priority
@@ -49,6 +49,10 @@ Skills speak in actions ("dispatch a subagent", "create a todo", "read a file")
**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it. **Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.
**Documented exceptions in a skill's own description are authoritative.** When a description itself says the skill does not apply to a request (e.g. brainstorming's nothing-to-design exception), not invoking it is compliance, not rationalization. Any doubt about whether the exception's conditions hold means invoke. Only the skill's description can define such an exception; you cannot infer one.
**An exception skip must be stated, never silent.** Before your first action, write one line naming the exception and the tripwire scan that came up empty — e.g. "Skipping brainstorming per its exception: no security/deletion/schema/new-file tripwires; outcome fully specified." If you did not write the scan line, you did not scan — invoke the skill instead.
```dot ```dot
digraph skill_flow { digraph skill_flow {
"User message received" [shape=doublecircle]; "User message received" [shape=doublecircle];
@@ -69,7 +73,12 @@ digraph skill_flow {
"Invoke brainstorming skill" -> "Might any skill apply?"; "Invoke brainstorming skill" -> "Might any skill apply?";
"User message received" -> "Might any skill apply?"; "User message received" -> "Might any skill apply?";
"Might any skill apply?" -> "Invoke the skill" [label="yes, even 1%"]; "Might any skill apply?" -> "Skill's own description exempts this request?" [label="yes, even 1%"];
"Skill's own description exempts this request?" [shape=diamond];
"Skill's own description exempts this request?" -> "Invoke the skill" [label="no / any doubt"];
"Skill's own description exempts this request?" -> "State the one-line tripwire scan, then proceed" [label="yes, clearly"];
"State the one-line tripwire scan, then proceed" [shape=box];
"State the one-line tripwire scan, then proceed" -> "Respond (including clarifications)";
"Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"]; "Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
"Invoke the skill" -> "Announce: 'Using [skill] to [purpose]'"; "Invoke the skill" -> "Announce: 'Using [skill] to [purpose]'";
"Announce: 'Using [skill] to [purpose]'" -> "Has checklist?"; "Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
@@ -94,6 +103,7 @@ These thoughts mean STOP—you're rationalizing:
| "I remember this skill" | Skills evolve. Read current version. | | "I remember this skill" | Skills evolve. Read current version. |
| "This doesn't count as a task" | Action = task. Check for skills. | | "This doesn't count as a task" | Action = task. Check for skills. |
| "The skill is overkill" | Simple things become complex. Use it. | | "The skill is overkill" | Simple things become complex. Use it. |
| "Too trivial to scan the tripwire list" | The scan is one sentence. Write it or invoke the skill. |
| "I'll just do this one thing first" | Check BEFORE doing anything. | | "I'll just do this one thing first" | Check BEFORE doing anything. |
| "This feels productive" | Undisciplined action wastes time. Skills prevent this. | | "This feels productive" | Undisciplined action wastes time. Skills prevent this. |
| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. | | "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. |
@@ -118,4 +128,6 @@ The skill itself tells you which.
## User Instructions ## User Instructions
Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows. Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows — unless a skill's own description exempts the request (see The Rule above).
Pressure phrasing — "don't ask questions", "make assumptions", "just build it" — changes how you interact (state assumptions instead of asking), not which skills you invoke. Only an instruction that names what to skip ("don't write a plan", "skip TDD") or a description exception skips a workflow step. "Your instruction wins per the priority rules" applied to an unnamed workflow step is a rationalization.

View File

@@ -151,6 +151,8 @@ Concrete results
The description should ONLY describe triggering conditions. Do NOT summarize the skill's process or workflow in the description. The description should ONLY describe triggering conditions. Do NOT summarize the skill's process or workflow in the description.
(Negative triggering conditions are still triggering conditions: a description MAY state when the skill does NOT apply — including its tripwires — and per using-superpowers' Rule such description-level exceptions are authoritative, so they must live here, not only in the body. That is scope, not workflow.)
**Why this matters:** Testing revealed that when a description summarizes the skill's workflow, an agent may follow the description instead of reading the full skill content. A description saying "code review between tasks" caused an agent to do ONE review, even though the skill's flowchart clearly showed TWO reviews (spec compliance then code quality). **Why this matters:** Testing revealed that when a description summarizes the skill's workflow, an agent may follow the description instead of reading the full skill content. A description saying "code review between tasks" caused an agent to do ONE review, even though the skill's flowchart clearly showed TWO reviews (spec compliance then code quality).
When the description was changed to just "Use when executing implementation plans with independent tasks" (no workflow summary), the agent correctly read the flowchart and followed the two-stage review process. When the description was changed to just "Use when executing implementation plans with independent tasks" (no workflow summary), the agent correctly read the flowchart and followed the two-stage review process.

View File

@@ -329,6 +329,21 @@ function runTests() {
assert.strictEqual(result.payload.length, 65536); assert.strictEqual(result.payload.length, 65536);
}); });
test('rejects oversized 64-bit frames before payload allocation', () => {
const mask = Buffer.from([0x00, 0x00, 0x00, 0x00]);
const header = Buffer.alloc(14);
header[0] = 0x81; // FIN + TEXT
header[1] = 0x80 | 127; // masked, 64-bit length
header.writeBigUInt64BE(BigInt(ws.MAX_FRAME_PAYLOAD_BYTES) + 1n, 2);
mask.copy(header, 10);
assert.throws(
() => ws.decodeFrame(header),
/exceeds maximum allowed size/i,
'oversized advertised payload must be rejected from header alone'
);
});
// ========== Close Frame with Status Code ========== // ========== Close Frame with Status Code ==========
console.log('\n--- Close Frame Details ---'); console.log('\n--- Close Frame Details ---');

View File

@@ -175,6 +175,7 @@ write_upstream_fixture() {
mkdir -p \ mkdir -p \
"$repo/.codex-plugin" \ "$repo/.codex-plugin" \
"$repo/.kimi-plugin" \
"$repo/.private-journal" \ "$repo/.private-journal" \
"$repo/assets" \ "$repo/assets" \
"$repo/evals/drill" \ "$repo/evals/drill" \
@@ -210,6 +211,13 @@ EOF
"name": "superpowers", "name": "superpowers",
"version": "$MANIFEST_VERSION" "version": "$MANIFEST_VERSION"
} }
EOF
cat > "$repo/.kimi-plugin/plugin.json" <<EOF
{
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF EOF
cat > "$repo/assets/superpowers-small.svg" <<'EOF' cat > "$repo/assets/superpowers-small.svg" <<'EOF'
@@ -267,6 +275,7 @@ EOF
git -C "$repo" add \ git -C "$repo" add \
.codex-plugin/plugin.json \ .codex-plugin/plugin.json \
.kimi-plugin/plugin.json \
.gitignore \ .gitignore \
assets/app-icon.png \ assets/app-icon.png \
assets/superpowers-small.svg \ assets/superpowers-small.svg \
@@ -415,10 +424,15 @@ EOF
write_stale_ignored_destination_fixture() { write_stale_ignored_destination_fixture() {
local repo="$1" local repo="$1"
mkdir -p "$repo/plugins/superpowers/.private-journal" mkdir -p \
"$repo/plugins/superpowers/.kimi-plugin" \
"$repo/plugins/superpowers/.private-journal"
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep" printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
printf '{"name":"stale-kimi"}\n' > "$repo/plugins/superpowers/.kimi-plugin/plugin.json"
printf 'stale ignored leak\n' > "$repo/plugins/superpowers/.private-journal/leak.txt" printf 'stale ignored leak\n' > "$repo/plugins/superpowers/.private-journal/leak.txt"
git -C "$repo" add plugins/superpowers/.fixture-keep git -C "$repo" add \
plugins/superpowers/.fixture-keep \
plugins/superpowers/.kimi-plugin/plugin.json
commit_fixture "$repo" "Initial stale ignored destination fixture" commit_fixture "$repo" "Initial stale ignored destination fixture"
} }
@@ -618,6 +632,7 @@ main() {
assert_contains "$preview_output" "Version: $MANIFEST_VERSION" "Preview uses manifest version" assert_contains "$preview_output" "Version: $MANIFEST_VERSION" "Preview uses manifest version"
assert_not_contains "$preview_output" "Version: $PACKAGE_VERSION" "Preview does not use package.json version" assert_not_contains "$preview_output" "Version: $PACKAGE_VERSION" "Preview does not use package.json version"
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path" assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
assert_not_contains "$preview_section" ".kimi-plugin/plugin.json" "Preview excludes Kimi manifest from Codex sync"
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset" assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset" assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
assert_contains "$preview_section" "hooks/hooks-codex.json" "Preview includes Codex hook manifest" assert_contains "$preview_section" "hooks/hooks-codex.json" "Preview includes Codex hook manifest"
@@ -644,6 +659,7 @@ main() {
echo "" echo ""
echo "Convergence assertions..." echo "Convergence assertions..."
assert_equals "$stale_preview_status" "0" "Stale ignored destination preview exits successfully" assert_equals "$stale_preview_status" "0" "Stale ignored destination preview exits successfully"
assert_matches "$stale_preview_section" "\\*deleting +\\.kimi-plugin/plugin\\.json" "Preview deletes stale Kimi manifest from Codex plugin"
assert_matches "$stale_preview_section" "\\*deleting +\\.private-journal/leak\\.txt" "Preview deletes stale ignored destination file" assert_matches "$stale_preview_section" "\\*deleting +\\.private-journal/leak\\.txt" "Preview deletes stale ignored destination file"
echo "" echo ""

6
tests/kimi/run-tests.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
bash "$SCRIPT_DIR/test-plugin-manifest.sh"

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
MANIFEST="$REPO_ROOT/.kimi-plugin/plugin.json"
python3 - "$MANIFEST" <<'PY'
import json
import sys
from pathlib import Path
manifest_path = Path(sys.argv[1])
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
def assert_equal(actual, expected, label):
if actual != expected:
raise AssertionError(f"{label}: expected {expected!r}, got {actual!r}")
def assert_present(text, needle, label):
if needle not in text:
raise AssertionError(f"{label}: missing {needle!r}")
assert_equal(manifest.get("name"), "superpowers", "plugin name")
assert_equal(manifest.get("skills"), "./skills/", "skills path")
assert_equal(
manifest.get("sessionStart", {}).get("skill"),
"using-superpowers",
"sessionStart.skill",
)
instructions = manifest.get("skillInstructions")
if not isinstance(instructions, str) or not instructions.strip():
raise AssertionError("skillInstructions must be a non-empty string")
for token in [
"AskUserQuestion",
"TodoList",
"Agent",
"Skill",
"Read",
"Write",
"Edit",
"Bash",
"Grep",
"Glob",
"FetchURL",
"WebSearch",
]:
assert_present(instructions, token, "skillInstructions")
version_config = json.loads(
(manifest_path.parents[1] / ".version-bump.json").read_text(encoding="utf-8")
)
version_entries = version_config.get("files")
if not isinstance(version_entries, list):
raise AssertionError(".version-bump.json must contain files list")
if not any(
entry.get("path") == ".kimi-plugin/plugin.json" and entry.get("field") == "version"
for entry in version_entries
if isinstance(entry, dict)
):
raise AssertionError(
".version-bump.json must update .kimi-plugin/plugin.json version"
)
unsupported_fields = [
"tools",
"commands",
"hooks",
"apps",
"inject",
"configFile",
"config_file",
"bootstrap",
]
present_unsupported = sorted(field for field in unsupported_fields if field in manifest)
if present_unsupported:
raise AssertionError(
"unsupported Kimi runtime fields present: "
+ ", ".join(present_unsupported)
)
print("Kimi plugin manifest looks good")
PY

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SCRIPT_UNDER_TEST="$REPO_ROOT/scripts/lint-shell.sh"
FAILURES=0
TEST_ROOT="$(mktemp -d)"
cleanup() {
rm -rf "$TEST_ROOT"
}
trap cleanup EXIT
pass() {
echo " [PASS] $1"
}
fail() {
echo " [FAIL] $1"
FAILURES=$((FAILURES + 1))
}
assert_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
pass "$description"
else
fail "$description"
echo " expected to find: $needle"
echo " in:"
printf '%s\n' "$haystack" | sed 's/^/ /'
fi
}
assert_not_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
fail "$description"
echo " did not expect to find: $needle"
echo " in:"
printf '%s\n' "$haystack" | sed 's/^/ /'
else
pass "$description"
fi
}
configure_git_identity() {
local repo="$1"
git -C "$repo" config user.name "Test Bot"
git -C "$repo" config user.email "test@example.com"
}
write_stub_tool() {
local path="$1"
local name="$2"
cat >"$path" <<EOF
#!/usr/bin/env bash
{
printf '${name}:'
for arg in "\$@"; do
printf ' <%s>' "\$arg"
done
printf '\n'
} >> "\$SUPERPOWERS_SHELL_LINT_TEST_LOG"
exit 0
EOF
chmod +x "$path"
}
make_fixture_repo() {
local repo="$1"
git init -q -b main "$repo"
configure_git_identity "$repo"
mkdir -p "$repo/hooks"
cat >"$repo/tracked.sh" <<'EOF'
#!/usr/bin/env bash
echo "tracked"
EOF
cat >"$repo/hooks/session-start" <<'EOF'
#!/bin/sh
echo "extensionless"
EOF
cat >"$repo/README.md" <<'EOF'
# Fixture
```bash
echo "not a shell script"
```
EOF
cat >"$repo/untracked.sh" <<'EOF'
#!/usr/bin/env bash
echo "untracked"
EOF
git -C "$repo" add tracked.sh hooks/session-start README.md
git -C "$repo" commit -q -m "fixture"
printf '\necho "changed"\n' >>"$repo/tracked.sh"
printf '\necho "changed extensionless"\n' >>"$repo/hooks/session-start"
}
run_lint_shell() {
local repo="$1"
local fakebin="$2"
local log="$3"
shift 3
(
cd "$repo"
PATH="$fakebin:$PATH" \
SUPERPOWERS_SHELL_LINT_TEST_LOG="$log" \
bash "$SCRIPT_UNDER_TEST" "$@"
)
}
echo "Shell lint script tests"
fixture="$TEST_ROOT/repo"
fakebin="$TEST_ROOT/bin"
log="$TEST_ROOT/tool.log"
mkdir -p "$fixture" "$fakebin"
: >"$log"
write_stub_tool "$fakebin/shellcheck" "shellcheck"
write_stub_tool "$fakebin/shfmt" "shfmt"
make_fixture_repo "$fixture"
if output="$(run_lint_shell "$fixture" "$fakebin" "$log" 2>&1)"; then
pass "lint-shell check mode exits successfully with stub tools"
else
fail "lint-shell check mode exits successfully with stub tools"
printf '%s\n' "$output" | sed 's/^/ /'
fi
tool_log="$(cat "$log")"
assert_contains "$output" "Linting 3 shell files" "reports changed shell file count"
assert_not_contains "$tool_log" "shfmt:" "does not run shfmt in lint mode"
assert_contains "$tool_log" "shellcheck:" "runs ShellCheck"
assert_contains "$tool_log" "<--severity=warning>" "uses warning severity as the baseline"
assert_contains "$tool_log" "<--external-sources>" "allows ShellCheck to follow sourced files"
assert_contains "$tool_log" "<--source-path=SCRIPTDIR>" "resolves ShellCheck sources relative to each script"
assert_contains "$tool_log" "<hooks/session-start>" "includes changed extensionless shell shebang file"
assert_contains "$tool_log" "<tracked.sh>" "includes changed tracked .sh file"
assert_contains "$tool_log" "<untracked.sh>" "includes untracked shell files by default"
assert_not_contains "$tool_log" "README.md" "ignores Markdown with shell snippets"
: >"$log"
if output="$(run_lint_shell "$fixture" "$fakebin" "$log" --all --format 2>&1)"; then
pass "lint-shell --format exits successfully with stub tools"
else
fail "lint-shell --format exits successfully with stub tools"
printf '%s\n' "$output" | sed 's/^/ /'
fi
tool_log="$(cat "$log")"
assert_contains "$tool_log" "<-w>" "uses shfmt write mode with --format"
assert_contains "$tool_log" "shellcheck:" "runs ShellCheck after --format"
assert_contains "$tool_log" "<--severity=warning>" "keeps warning severity after --format"
assert_contains "$tool_log" "<hooks/session-start>" "--all includes tracked extensionless shell shebang file"
assert_contains "$tool_log" "<tracked.sh>" "--all includes tracked .sh file"
assert_not_contains "$tool_log" "untracked.sh" "--all ignores untracked shell files"
if [[ "$FAILURES" -eq 0 ]]; then
echo "All shell lint script tests passed"
else
echo "$FAILURES shell lint script test(s) failed"
exit 1
fi