Compare commits

..

11 Commits

Author SHA1 Message Date
Drew Ritter
cb6bdf9dd3 Fix shell lint baseline warnings 2026-06-02 15:35:26 -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
20 changed files with 716 additions and 22 deletions

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

@@ -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).

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

@@ -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

@@ -97,7 +97,7 @@ if [[ -f "$PID_FILE" ]]; then
rm -f "$PID_FILE" rm -f "$PID_FILE"
fi fi
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR" || exit
# Resolve the harness PID (grandparent of this script). # Resolve the harness PID (grandparent of this script).
# $PPID is the ephemeral shell the harness spawned to run us — it dies # $PPID is the ephemeral shell the harness spawned to run us — it dies
@@ -135,7 +135,7 @@ disown "$SERVER_PID" 2>/dev/null
echo "$SERVER_PID" > "$PID_FILE" echo "$SERVER_PID" > "$PID_FILE"
# Wait for server-started message (check log file) # Wait for server-started message (check log file)
for i in {1..50}; do for _ in {1..50}; do
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
# Verify server is still alive after a short window (catches process reapers) # Verify server is still alive after a short window (catches process reapers)
alive="true" alive="true"

View File

@@ -23,7 +23,7 @@ if [[ -f "$PID_FILE" ]]; then
kill "$pid" 2>/dev/null || true kill "$pid" 2>/dev/null || true
# Wait for graceful shutdown (up to ~2s) # Wait for graceful shutdown (up to ~2s)
for i in {1..20}; do for _ in {1..20}; do
if ! kill -0 "$pid" 2>/dev/null; then if ! kill -0 "$pid" 2>/dev/null; then
break break
fi fi

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

@@ -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

@@ -7,7 +7,8 @@ run_claude() {
local prompt="$1" local prompt="$1"
local timeout="${2:-60}" local timeout="${2:-60}"
local allowed_tools="${3:-}" local allowed_tools="${3:-}"
local output_file=$(mktemp) local output_file
output_file="$(mktemp)"
# Build command as an argv array so timeout wraps claude directly. # Build command as an argv array so timeout wraps claude directly.
local cmd=(claude -p "$prompt") local cmd=(claude -p "$prompt")
@@ -74,7 +75,8 @@ assert_count() {
local expected="$3" local expected="$3"
local test_name="${4:-test}" local test_name="${4:-test}"
local actual=$(echo "$output" | grep -c "$pattern" || echo "0") local actual
actual="$(echo "$output" | grep -c "$pattern" || true)"
if [ "$actual" -eq "$expected" ]; then if [ "$actual" -eq "$expected" ]; then
echo " [PASS] $test_name (found $actual instances)" echo " [PASS] $test_name (found $actual instances)"
@@ -98,8 +100,10 @@ assert_order() {
local test_name="${4:-test}" local test_name="${4:-test}"
# Get line numbers where patterns appear # Get line numbers where patterns appear
local line_a=$(echo "$output" | grep -n "$pattern_a" | head -1 | cut -d: -f1) local line_a
local line_b=$(echo "$output" | grep -n "$pattern_b" | head -1 | cut -d: -f1) local line_b
line_a="$(echo "$output" | grep -n "$pattern_a" | head -1 | cut -d: -f1 || true)"
line_b="$(echo "$output" | grep -n "$pattern_b" | head -1 | cut -d: -f1 || true)"
if [ -z "$line_a" ]; then if [ -z "$line_a" ]; then
echo " [FAIL] $test_name: pattern A not found: $pattern_a" echo " [FAIL] $test_name: pattern A not found: $pattern_a"
@@ -125,7 +129,8 @@ assert_order() {
# Create a temporary test project directory # Create a temporary test project directory
# Usage: test_project=$(create_test_project) # Usage: test_project=$(create_test_project)
create_test_project() { create_test_project() {
local test_dir=$(mktemp -d) local test_dir
test_dir="$(mktemp -d)"
echo "$test_dir" echo "$test_dir"
} }

View File

@@ -37,7 +37,10 @@ TEST_PROJECT=$(create_test_project)
echo "Test project: $TEST_PROJECT" echo "Test project: $TEST_PROJECT"
# Trap to cleanup # Trap to cleanup
trap "cleanup_test_project $TEST_PROJECT" EXIT cleanup_integration_test_project() {
cleanup_test_project "$TEST_PROJECT"
}
trap cleanup_integration_test_project EXIT
# Set up minimal Node.js project # Set up minimal Node.js project
cd "$TEST_PROJECT" cd "$TEST_PROJECT"
@@ -164,12 +167,19 @@ PLUGIN_DIR=$(cd "$SCRIPT_DIR/../.." && pwd)
# other concurrent claude sessions. # other concurrent claude sessions.
echo "Running Claude (plugin-dir: $PLUGIN_DIR, cwd: $TEST_PROJECT)..." echo "Running Claude (plugin-dir: $PLUGIN_DIR, cwd: $TEST_PROJECT)..."
echo "================================================================================" echo "================================================================================"
cd "$TEST_PROJECT" && timeout 1800 claude -p "$PROMPT" --plugin-dir "$PLUGIN_DIR" --allowed-tools=all --permission-mode bypassPermissions 2>&1 | tee "$OUTPUT_FILE" || { set +e
(
cd "$TEST_PROJECT" &&
timeout 1800 claude -p "$PROMPT" --plugin-dir "$PLUGIN_DIR" --allowed-tools=all --permission-mode bypassPermissions
) 2>&1 | tee "$OUTPUT_FILE"
execution_status=$?
set -e
if [[ "$execution_status" -ne 0 ]]; then
echo "" echo ""
echo "================================================================================" echo "================================================================================"
echo "EXECUTION FAILED (exit code: $?)" echo "EXECUTION FAILED (exit code: $execution_status)"
exit 1 exit 1
} fi
echo "================================================================================" echo "================================================================================"
echo "" echo ""

View File

@@ -47,16 +47,20 @@ assert_not_contains() {
echo "=== Worktree Path Policy Test ===" echo "=== Worktree Path Policy Test ==="
echo "" echo ""
assert_not_contains "$USING_SKILL" "~/.config/superpowers/worktrees" "using-git-worktrees does not mention old global path" # Intentionally search for the literal legacy path, not the current user's home.
# shellcheck disable=SC2088
legacy_global_worktree_path="~/.config/superpowers/worktrees"
assert_not_contains "$USING_SKILL" "$legacy_global_worktree_path" "using-git-worktrees does not mention old global path"
assert_not_contains "$USING_SKILL" "global legacy" "using-git-worktrees does not use unclear global legacy shorthand" assert_not_contains "$USING_SKILL" "global legacy" "using-git-worktrees does not use unclear global legacy shorthand"
assert_not_contains "$USING_SKILL" "Global path" "using-git-worktrees has no global path quick-reference row" assert_not_contains "$USING_SKILL" "Global path" "using-git-worktrees has no global path quick-reference row"
assert_contains "$USING_SKILL" 'default to `.worktrees/` at the project root' "using-git-worktrees defaults new manual worktrees to .worktrees/" assert_contains "$USING_SKILL" 'default to `.worktrees/` at the project root' "using-git-worktrees defaults new manual worktrees to .worktrees/"
assert_not_contains "$FINISHING_SKILL" "~/.config/superpowers/worktrees" "finishing-a-development-branch does not treat old global path as owned" assert_not_contains "$FINISHING_SKILL" "$legacy_global_worktree_path" "finishing-a-development-branch does not treat old global path as owned"
assert_contains "$FINISHING_SKILL" '`.worktrees/` or `worktrees/`' "finishing-a-development-branch keeps project-local cleanup ownership" assert_contains "$FINISHING_SKILL" '`.worktrees/` or `worktrees/`' "finishing-a-development-branch keeps project-local cleanup ownership"
assert_not_contains "$ROTOTILL_SPEC" "~/.config/superpowers/worktrees" "rototill spec does not preserve old global path policy" assert_not_contains "$ROTOTILL_SPEC" "$legacy_global_worktree_path" "rototill spec does not preserve old global path policy"
assert_not_contains "$ROTOTILL_PLAN" "~/.config/superpowers/worktrees" "rototill plan does not preserve old global path policy" assert_not_contains "$ROTOTILL_PLAN" "$legacy_global_worktree_path" "rototill plan does not preserve old global path policy"
assert_not_contains "$ROTOTILL_PLAN" "legacy path compat" "rototill plan does not advertise legacy path compatibility" assert_not_contains "$ROTOTILL_PLAN" "legacy path compat" "rototill plan does not advertise legacy path compatibility"
echo "" echo ""

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