diff --git a/.clinerules/memory-bank.md b/.clinerules/memory-bank.md new file mode 100644 index 0000000..40732bd --- /dev/null +++ b/.clinerules/memory-bank.md @@ -0,0 +1,60 @@ +# Cline's Memory Bank + +I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. + +## Memory Bank Structure + +The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy: + +### Core Files (Required) + +1. `projectbrief.md` - Foundation document that shapes all other files + - Created at project start if it doesn't exist + - Defines core requirements and goals + - Source of truth for project scope +2. `productContext.md` - Why this project exists + - Problems it solves + - How it should work + - User experience goals +3. `activeContext.md` - Current work focus + - Recent changes + - Next steps + - Active decisions and considerations + - Important patterns and preferences + - Learnings and project insights +4. `systemPatterns.md` - System architecture + - Key technical decisions + - Design patterns in use + - Component relationships + - Critical implementation paths +5. `techContext.md` - Technologies used + - Development setup + - Technical constraints + - Dependencies + - Tool usage patterns +6. `progress.md` - What works + - What's left to build + - Current status + - Known issues + - Evolution of project decisions + +### Additional Context + +Create additional files/folders within memory-bank/ when they help organize: + +- Complex feature documentation +- Integration specifications +- API documentation +- Testing strategies +- Deployment procedures + +## Documentation Updates + +Memory Bank updates occur when: + +1. Discovering new project patterns +2. After implementing significant changes +3. When user requests with **update memory bank** (MUST review ALL files) +4. When context needs clarification + +REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. diff --git a/.gitignore b/.gitignore index 59eba62..ce255d9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # Ignore local Claude Code state, but track the shared, committed settings. .claude/* !.claude/settings.json +# Cline plugin install artifacts (e.g. from `cline plugin install ./ --cwd .`). +.cline/plugins/ .DS_Store node_modules/ .eval-magic diff --git a/AGENTS.md b/AGENTS.md index 75c36be..1c83bce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,12 +4,20 @@ Slow-powers is a set of software development methodology skills and meta skill-w ## What lives here -This repo ships Slow-powers across three harnesses: +This repo ships Slow-powers across four harnesses: - `skills/` — Skills, assets, and cross-cutting tests - `.claude-plugin/` — Claude Code plugin - `.codex-plugin/` — OpenAI Codex plugin - `opencode/` — OpenCode plugin (`@slowdini/slow-powers-opencode`) +- `cline/` — Cline plugin (CLI/SDK/Kanban only; declared via the `cline` + field in `package.json`, with `skills/` auto-discovered from the package + root) + +Cline-specific setup for working on this repo also lives at root: + +- `.clinerules/` — Cline rules (the Memory Bank custom instructions) +- `memory-bank/` — Cline Memory Bank files recording ongoing work ## Editing the right files @@ -69,3 +77,7 @@ bun run check (pre-commit runs typecheck + lint-staged; pre-push runs the test suite). `bun scripts/bump-version.ts ` updates every manifest in lockstep. + +To test the Cline integration live: `cline plugin install ./ --cwd `, +run a Cline session in the scratch dir, then +`cline plugin uninstall slow-powers --cwd ` when done. diff --git a/README.md b/README.md index af8db19..763bc39 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Slow-powers is a fork of [obra/superpowers](https://github.com/obra/superpowers) ## Quickstart -[Claude Code](#claude-code) · [Codex CLI](#codex-cli) · [OpenCode](#opencode) +[Claude Code](#claude-code) · [Codex CLI](#codex-cli) · [OpenCode](#opencode) · [Cline](#cline) ## How it works @@ -99,6 +99,17 @@ You can also browse and install it interactively: run `codex`, open opencode plugin @slowdini/slow-powers-opencode -g ``` +### Cline + +```bash +cline plugin install https://github.com/slowdini/slow-powers.git +``` + +Cline plugins load in the Cline CLI, SDK, and Kanban — **not** the VSCode or +JetBrains extensions. On those, you can still use the skills by copying or +symlinking `skills/` into `.cline/skills/` (project) or `~/.cline/skills/` +(global); the bootstrap injection and plan gate are CLI/SDK/Kanban-only. + ## The skills Slow-powers provides a set of highly focused skills that ensure your agent operates with maximum discipline: @@ -151,8 +162,9 @@ Flat layout — skills and assets live at root, harness-specific integration liv - `.claude-plugin/` — Claude Code plugin manifest and hooks - `.codex-plugin/` — OpenAI Codex plugin manifest - `opencode/` — OpenCode plugin +- `cline/` — Cline plugin entry point - `.claude-plugin/marketplace.json` — Claude Code marketplace registry -- `package.json` — OpenCode plugin manifest + dev tooling +- `package.json` — OpenCode + Cline plugin manifests + dev tooling ## Releasing diff --git a/cline/plugins/slow-powers.js b/cline/plugins/slow-powers.js new file mode 100644 index 0000000..196751c --- /dev/null +++ b/cline/plugins/slow-powers.js @@ -0,0 +1,127 @@ +/** + * Slow-powers plugin for Cline (CLI / SDK / Kanban). + * + * Two jobs, mirroring what the bash hooks do on Claude Code and Codex: + * + * 1. BOOTSTRAP INJECTION — registers the contents of bootstrap.md as a session + * rule, so the skill-enforcement block is part of every session's system + * prompt. This replaces the SessionStart-hook injection used on Claude/Codex + * and the system-prompt transform used on OpenCode. + * + * 2. PLAN GATE — the FIRST switch_to_act_mode call of a conversation is skipped + * with an instruction to run the hardening-plans skill on the plan first. + * switch_to_act_mode is how Cline presents a finished plan and leaves plan + * mode; skipping it keeps the session in plan mode, so the agent can load + * the skill, fix findings inline, and re-submit a hardened plan. The + * re-submission finds the per-conversation marker and is allowed through. + * + * WHY DENY-ONCE (and not deny-until-proven-hardened): keying the marker per + * conversation and allowing the second attempt guarantees we can never + * hard-lock a user inside plan mode. Worst case (agent re-submits without + * hardening) degrades to no-gate behavior — never worse. Same argument as + * hooks/exit-plan-mode. + * + * Skills need no wiring here: when this package is installed as a Cline + * plugin, the top-level skills/ directory is discovered automatically. + * + * Single-file plugin constraint: only Node builtins may be imported at + * runtime; @cline/* packages are host-provided and referenced in JSDoc only. + */ + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url)); +const BOOTSTRAP_PATH = path.resolve(PLUGIN_DIR, "../../bootstrap.md"); + +// Module-level cache: bootstrap.md does not change during a session, so read +// it once. undefined = not yet loaded, null = missing/unreadable. +let _bootstrapCache; + +function getBootstrapContent() { + if (_bootstrapCache !== undefined) return _bootstrapCache; + try { + _bootstrapCache = fs.readFileSync(BOOTSTRAP_PATH, "utf8"); + } catch { + _bootstrapCache = null; + } + return _bootstrapCache; +} + +// Pick a key that is stable across the skip and the re-submit. conversationId +// is the natural choice; fall back to agentId, then a fixed key. Any +// consistent key preserves deny-once safety. +function conversationKey(context) { + const snapshot = context?.snapshot; + const raw = snapshot?.conversationId ?? snapshot?.agentId ?? "fallback"; + // Sanitize to a safe, bounded filename component (mirrors the bash hooks). + return String(raw) + .replace(/[^A-Za-z0-9._-]/g, "_") + .slice(0, 128); +} + +function markerPath(context) { + return path.join( + process.env.SLOW_POWERS_PLAN_GATE_DIR ?? os.tmpdir(), + `slow-powers-plan-gate-${conversationKey(context)}`, + ); +} + +const SKIP_REASON = + "A plan is about to be presented. Before it leaves your hands, use the " + + "hardening-plans skill to review the plan file as a skeptical executor, " + + "then call switch_to_act_mode again to present the hardened plan."; + +/** @type {import("@cline/sdk").AgentPlugin} */ +const SlowPowersPlugin = { + name: "slow-powers", + manifest: { + capabilities: ["hooks", "rules"], + }, + + setup(api) { + const bootstrap = getBootstrapContent(); + if (!bootstrap) return; + api.registerRule({ + id: "slow-powers/bootstrap", + source: "slow-powers", + content: bootstrap, + }); + }, + + hooks: { + beforeTool(context) { + try { + // The runtime passes both `tool` (the AgentTool definition) and + // `toolCall` (the pending call). First-party guards read `tool.name`; + // the plugin docs read `toolCall.name`. Accept either shape. + const toolName = context?.tool?.name ?? context?.toolCall?.name; + if (toolName !== "switch_to_act_mode") return undefined; + + const marker = markerPath(context); + if (fs.existsSync(marker)) { + // Re-submission after hardening — let the plan be presented. + return undefined; + } + + // First switch_to_act_mode this conversation: record it, then skip + // once to insert the hardening-plans beat. Best-effort marker write — + // a failed write must not break the gate (fail-open below covers it). + try { + fs.writeFileSync(marker, ""); + } catch { + // Ignore: worst case the gate fires again on the next attempt. + } + + return { skip: true, reason: SKIP_REASON }; + } catch { + // Fail open: a plugin error must never block the user's workflow. + return undefined; + } + }, + }, +}; + +export default SlowPowersPlugin; diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md new file mode 100644 index 0000000..b3c2086 --- /dev/null +++ b/memory-bank/activeContext.md @@ -0,0 +1,77 @@ +# Active Context + +## Current focus + +Cline support was just added (August 2026). Two halves: + +1. **Cline plugin** (`cline/plugins/slow-powers.js`, declared via the `cline` + field in `package.json`): registers `bootstrap.md` as a session rule and + gates the first `switch_to_act_mode` of each conversation on + hardening-plans (skip-once + tmp marker, mirroring `hooks/exit-plan-mode`). + Skills are auto-discovered from the package root — no wiring needed. +2. **Repo-local Cline setup**: `.clinerules/memory-bank.md` (canonical Memory + Bank instructions) and this `memory-bank/` directory, both committed. + +## Recent changes + +- `cline/plugins/slow-powers.js` (new), `package.json` `cline` field + `files` +- `tests/harness/spec.ts` Cline entry; Cline assertions in `manifests.test.ts`; + new `tests/harness/cline-plugin.test.ts` +- README Cline install section; AGENTS.md four-harness update; + `.gitignore` covers `.cline/plugins/` install artifacts + +## Verification results (Cline CLI 3.0.51, headless) + +- `cline plugin install --cwd ` works; installer copies the + repo and registers the plugin entry. +- Live session: all 8 skills discovered; `` bootstrap + block present in instructions; bootstrap behavior observed (agent invoked a + skill on a ~1% match, per the bootstrap rule). +- Plan gate: unit-tested against the documented `AgentBeforeToolResult` + contract. The runtime's `skip` handling (tool doesn't run, `reason` goes to + the model) and the hook context shape were confirmed in the shipped CLI + source — the first-party `core.plan-mode-command-guard` extension uses the + same pattern. `switch_to_act_mode` is NOT exposed in headless one-shot + sessions, so an interactive (TUI) confirmation of the gate firing is the one + remaining manual check. + +## Next steps + +- PR opened: https://github.com/slowdini/slow-powers/pull/266 (base `dev`). +- Manually confirm the plan gate in an interactive `cline -i` plan-mode + session (present plan → approve → first `switch_to_act_mode` gets skipped + with the hardening instruction) — easiest via a test release, per the + maintainer. +- After merge to `dev`: trigger the Release PR workflow with the next version + to ship the Cline plugin (that release doubles as the test release). + +## Active decisions + +- Distribution reuses the root `package.json` (git install); no separate npm + package or release-workflow change. +- The Cline gate is skip-once only. The already-hardened short-circuit + (upstream #153 refinement) is deferred — it needs reliable detection that + hardening-plans already ran (the skill-invocation tool is `skills` in the + Cline runtime). +- No `.cline/skills/` dogfooding symlinks: Cline's skill registry is + last-wins with plugin dirs scanned *after* workspace dirs, so an installed + slow-powers plugin would silently shadow the repo's skills. The + installed-vs-repo precedence question is deferred to a separate + cross-harness exploration (it affects all harnesses). + +## Learnings + +- Cline plugins load only in CLI/SDK/Kanban — not VSCode/JetBrains. IDE users + get skills via manual copy into `.cline/skills/` or `~/.cline/skills/`. +- Cline reads `AGENTS.md` natively; no memory-file symlink needed for it. +- Cline's plan-exit tool is `switch_to_act_mode`; `AgentBeforeToolResult.skip` + + `reason` is the deny mechanism; `registerRule` puts content in the system + prompt every session. +- Hook contexts pass the tool name on BOTH `tool.name` (first-party shape) and + `toolCall.name` (docs shape) — read `tool?.name ?? toolCall?.name`. +- Headless one-shot sessions (`cline -p "..."`) don't expose + `switch_to_act_mode` and can't drive TTY-only commands (`cline config`); use + interactive sessions for plan-gate verification. +- Cline's local plugin install copies dotfile-free repo content — everything + the plugin needs (`cline/`, `skills/`, `bootstrap.md`) is a normal path, so + this is fine. diff --git a/memory-bank/productContext.md b/memory-bank/productContext.md new file mode 100644 index 0000000..83bfa2e --- /dev/null +++ b/memory-bank/productContext.md @@ -0,0 +1,26 @@ +# Product Context + +## Why this exists + +Coding agents under pressure skip discipline: they present unreviewed plans, +claim success without running tests, thrash on bugs with guess-and-check, and +let new work collide with in-progress branches. Slow-powers exists to put that +discipline back — not by replacing harness features, but by hardening them +(plan-mode gates, skill-enforcement bootstrap, verification loops). + +## How it should work + +- A bootstrap block (`bootstrap.md`) is injected into every session, making + skill use non-negotiable when a skill applies. +- Skills declare prerequisite / next-step gates so the agent follows an + intended sequence (plan → harden → isolate → TDD → verify). +- Harness hooks/plugins supply the deterministic beats a skill can't enforce + on its own (e.g. gating plan presentation on hardening-plans). + +## User experience goals + +- Install once per harness, then forget it — the value shows up as plans that + don't hallucinate files, tests that exist before code, and success claims + backed by command output. +- "The plugin for people who don't install plugins": minimal surface, no + config, no lock-in; users can extend with their own evaluated skills. diff --git a/memory-bank/progress.md b/memory-bank/progress.md new file mode 100644 index 0000000..0b10832 --- /dev/null +++ b/memory-bank/progress.md @@ -0,0 +1,37 @@ +# Progress + +## What works + +- Eight skills with eval coverage; bootstrap injection and plan gates on + Claude Code, Codex CLI, and OpenCode. +- Full test suite green (`bun test`), typecheck and biome clean. +- **Cline support (new)**: plugin entry, manifest field, unit + manifest + tests, README/AGENTS.md docs, memory bank initialized. Verified live on + Cline CLI 3.0.51: install, skills discovery, and bootstrap rule injection + all confirmed in headless sessions. + +## What's left + +- Manual interactive check of the Cline plan gate (`switch_to_act_mode` is + only exposed in interactive sessions), then PR. +- Release: next version bump will carry the Cline plugin via the normal flow. + +## Known issues / deferred + +- Cline plugins don't load on VSCode/JetBrains extensions — IDE users get a + documented skills-only manual install (no bootstrap, no plan gate). +- Cline skill collisions are last-wins with plugin directories scanned after + workspace ones, so an installed slow-powers plugin shadows same-named + workspace skills (the reverse of what this repo wants for development). + Deferred: cross-harness installed-vs-repo precedence exploration. +- Cline plan gate has no already-hardened short-circuit yet (deferred; needs + skill-invocation detection). + +## Decision log + +- 2026-08: Cline distribution via root `package.json` + git install (no new + npm package). +- 2026-08: Memory bank committed to git (`.clinerules/memory-bank.md` + + `memory-bank/`). +- 2026-08: No `.cline/skills/` symlinks (option (c)) pending the precedence + exploration. diff --git a/memory-bank/projectbrief.md b/memory-bank/projectbrief.md new file mode 100644 index 0000000..a84d3e8 --- /dev/null +++ b/memory-bank/projectbrief.md @@ -0,0 +1,25 @@ +# Project Brief + +Slow-powers is an agent skill set for professional software development. It +enhances plan mode and debugging work, enforces best practices (TDD, +verification, workspace isolation), and works *with* the features of modern +agent harnesses instead of replacing them. It is a fork of +[obra/superpowers](https://github.com/obra/superpowers), with rewrites focused +on clarity, token efficiency, and a lighter touch. + +## Core goals + +- Ship discipline-enforcing skills (plan hardening, TDD, scientific debugging, + verification, isolated workspaces) that measurably improve agent behavior — + every skill ships with a documented eval or it doesn't ship. +- Support multiple agent harnesses from one repo: Claude Code, OpenAI Codex, + OpenCode, and Cline. +- Keep skill content cross-harness compatible (no harness-specific vocabulary + in skill prose). + +## Scope + +- `skills/` holds the shared skills and their evals. +- Harness-specific integration (manifests, hooks, runtime plugins) lives in + top-level directories; skill content itself stays harness-agnostic. +- This repo is the source of truth; installed plugins are downstream copies. diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md new file mode 100644 index 0000000..b788965 --- /dev/null +++ b/memory-bank/systemPatterns.md @@ -0,0 +1,48 @@ +# System Patterns + +## Flat layout, one source of truth + +Skills and shared assets live at the repo root; each harness's integration is +a thin top-level layer that points back at them. Nothing is duplicated per +harness. + +## Per-harness delivery of the same two behaviors + +Every harness delivers (1) the `bootstrap.md` skill-enforcement block and +(2) a deterministic plan-presentation gate, using that harness's native +mechanism: + +| Harness | Bootstrap delivery | Plan gate | +| -------- | -------------------------------------- | ------------------------------------------------ | +| Claude | `hooks/session-start` (SessionStart) | `hooks/exit-plan-mode` (PreToolUse, deny-once) | +| Codex | shared `hooks/hooks.json` SessionStart | `hooks/codex-stop-plan-mode` (Stop hook) | +| OpenCode | `opencode/plugins/slow-powers.js` system-prompt transform | same plugin, `file.edited` event on plan files | +| Cline | `cline/plugins/slow-powers.js` `registerRule` | same plugin, `beforeTool` skip-once on `switch_to_act_mode` | + +Claude/Codex hooks are extensionless bash scripts dispatched by the +`hooks/run-hook.cmd` polyglot (Windows-safe). OpenCode/Cline integrations are +dependency-free JS runtime plugins. + +## Manifest and version lockstep + +`scripts/manifest-files.ts` lists every versioned manifest; +`scripts/bump-version.ts` rewrites them in lockstep (then biome-formats); +`tests/harness/manifests.test.ts` asserts parity. The Cline and OpenCode +integrations both declare themselves inside the root `package.json`, which is +already locked. + +## Parameterized parity tests + +`tests/harness/spec.ts` holds one `HarnessSpec` per harness; the suite in +`manifests.test.ts` applies the same contract to all of them. Adding a +harness = adding a spec entry (+ custom assertions when the manifest shape +doesn't fit the dotted-string `pathFields` machinery, as with Cline's nested +`cline.plugins[].paths[]`). + +## Skill integrity tests + +The shared-assets block in `manifests.test.ts` pins: SKILL.md frontmatter +(name + description), top-level-only skill folders, documented peer +directories (`assets`/`evals`/`references`/`scripts`), resolvable markdown +links, reachable reference files, mermaid-not-graphviz, and the bootstrap +marker. diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md new file mode 100644 index 0000000..ad3b0c4 --- /dev/null +++ b/memory-bank/techContext.md @@ -0,0 +1,30 @@ +# Tech Context + +## Stack + +- **bun** — test runner and script runtime (`bun test`, `bun scripts/*.ts`) +- **biome** — lint + format (`bun run check`, `check:ci`); JSON included +- **typescript** — `tsc --noEmit` over `scripts/**/*.ts` and `tests/**/*.ts` + only (runtime plugins and hooks are plain JS/bash, deliberately) +- **husky + lint-staged** — pre-commit typecheck/lint, pre-push test suite + (installed by `bun install` via the `prepare` script) +- **eval-magic** — skill evaluation harness (`bun run evals*` scripts); + eval fixtures live under `skills//evals/` + +## Release flow + +Releases cut from `dev`, tagged from `main`. The Release PR workflow bumps +every manifest via `scripts/bump-version.ts`; merging to `main` tags, creates +the GitHub release, and publishes `@slowdini/slow-powers-opencode` to npm. + +## Constraints + +- Hook scripts: pure bash, no jq/python/bun at hook time; printf-based JSON + (heredocs hang on bash 5.3+); extensionless filenames (Windows). +- Cline single-file plugins may import only Node builtins; `@cline/*` + packages are host-provided. +- Skill prose must use cross-harness vocabulary (see `writing-skills`). + +## Local environment + +- Cline CLI 3.0.51 (homebrew) used for live verification of the Cline plugin. diff --git a/package.json b/package.json index 4325f5d..ef45487 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,21 @@ "description": "Slow-powers — structured development workflows for coding agents (TDD, debugging, verification, git hygiene)", "type": "module", "main": "./opencode/plugins/slow-powers.js", + "cline": { + "plugins": [ + { + "paths": [ + "./cline/plugins/slow-powers.js" + ], + "capabilities": [ + "hooks", + "rules" + ] + } + ] + }, "files": [ + "cline/", "opencode/", "skills/", "bootstrap.md", diff --git a/tests/harness/cline-plugin.test.ts b/tests/harness/cline-plugin.test.ts new file mode 100644 index 0000000..15ac288 --- /dev/null +++ b/tests/harness/cline-plugin.test.ts @@ -0,0 +1,158 @@ +// Behavioral tests for the cline/plugins/slow-powers.js Cline plugin. +// The plugin registers bootstrap.md as a session rule and gates the first +// switch_to_act_mode call of each conversation on hardening-plans. We import +// the real plugin module and drive it with fake api/hook contexts, with an +// isolated marker dir so its marker files never touch the developer's real +// temp dir (mirrors the TMPDIR isolation in exit-plan-mode-hook.test.ts). +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { BOOTSTRAP_MARKER, REPO_ROOT } from "./spec"; + +const PLUGIN_PATH = path.join(REPO_ROOT, "cline/plugins/slow-powers.js"); + +// The subset of the AgentPlugin surface this plugin implements. Typed +// locally so the test stays honest about the contract it exercises without +// pulling @cline/sdk into the typecheck graph. +interface RuleContribution { + id: string; + source?: string; + content: string; +} + +interface FakeApi { + registerRule: (rule: RuleContribution) => void; +} + +interface HookContext { + tool?: { name?: string }; + toolCall?: { name?: string; input?: unknown }; + snapshot?: { conversationId?: string; agentId?: string }; +} + +interface BeforeToolResult { + skip?: boolean; + reason?: string; +} + +interface ClinePlugin { + name: string; + manifest: { capabilities: string[] }; + setup?: (api: FakeApi) => void; + hooks?: { + beforeTool?: (context: HookContext) => BeforeToolResult | undefined; + }; +} + +// Computed (non-literal) specifier: bun resolves it at runtime, tsc treats +// the module as `any` and doesn't try to type-resolve the .js file. +const plugin = (await import(PLUGIN_PATH)).default as ClinePlugin; + +function beforeTool(context: HookContext): BeforeToolResult | undefined { + return plugin.hooks?.beforeTool?.(context); +} + +// Mirrors the runtime's hook invocation shape: {snapshot, tool, toolCall, +// input} — with the tool definition carrying the canonical name. +const planContext = (conversationId?: string): HookContext => ({ + tool: { name: "switch_to_act_mode" }, + toolCall: { name: "switch_to_act_mode", input: {} }, + snapshot: { conversationId }, +}); + +let markerDir: string; +let prevGateDir: string | undefined; + +beforeEach(() => { + markerDir = fs.mkdtempSync(path.join(os.tmpdir(), "cline-plugin-test-")); + prevGateDir = process.env.SLOW_POWERS_PLAN_GATE_DIR; + process.env.SLOW_POWERS_PLAN_GATE_DIR = markerDir; +}); + +afterEach(() => { + if (prevGateDir === undefined) delete process.env.SLOW_POWERS_PLAN_GATE_DIR; + else process.env.SLOW_POWERS_PLAN_GATE_DIR = prevGateDir; + fs.rmSync(markerDir, { recursive: true, force: true }); +}); + +function markers(): string[] { + return fs + .readdirSync(markerDir) + .filter((f) => f.startsWith("slow-powers-plan-gate-")); +} + +describe("cline plugin module", () => { + test("exports an AgentPlugin-shaped object", () => { + expect(plugin.name).toBe("slow-powers"); + expect(plugin.manifest.capabilities).toContain("hooks"); + expect(plugin.manifest.capabilities).toContain("rules"); + expect(typeof plugin.setup).toBe("function"); + expect(typeof plugin.hooks?.beforeTool).toBe("function"); + }); + + test("setup registers bootstrap.md as a session rule", () => { + const registered: RuleContribution[] = []; + plugin.setup?.({ registerRule: (rule) => registered.push(rule) }); + + expect(registered.length).toBe(1); + expect(registered[0].id).toBe("slow-powers/bootstrap"); + expect(registered[0].source).toBe("slow-powers"); + expect(registered[0].content).toContain(BOOTSTRAP_MARKER); + }); +}); + +describe("cline plugin plan gate", () => { + test("skips the first switch_to_act_mode and points at hardening-plans", () => { + const result = beforeTool(planContext("conv-A")); + + expect(result?.skip).toBe(true); + expect(result?.reason).toContain("hardening-plans"); + expect(markers()).toEqual(["slow-powers-plan-gate-conv-A"]); + }); + + test("allows the re-submitted call (marker present, same conversation)", () => { + expect(beforeTool(planContext("conv-B"))?.skip).toBe(true); + expect(beforeTool(planContext("conv-B"))).toBeUndefined(); + }); + + test("treats distinct conversations independently", () => { + beforeTool(planContext("conv-C")); // first call for C -> skip + marker + // D has never been seen, even though C's marker exists in the same dir. + expect(beforeTool(planContext("conv-D"))?.skip).toBe(true); + }); + + test("ignores unrelated tools entirely", () => { + const result = beforeTool({ + tool: { name: "read_files" }, + toolCall: { name: "read_files", input: {} }, + snapshot: { conversationId: "conv-E" }, + }); + + expect(result).toBeUndefined(); + expect(markers()).toEqual([]); + }); + + test("fires when only the toolCall carries the name (docs shape)", () => { + const result = beforeTool({ + toolCall: { name: "switch_to_act_mode", input: {} }, + snapshot: { conversationId: "conv-toolCall-only" }, + }); + + expect(result?.skip).toBe(true); + expect(markers()).toEqual(["slow-powers-plan-gate-conv-toolCall-only"]); + }); + + test("still denies-once when conversationId is absent (no crash)", () => { + expect(beforeTool(planContext())?.skip).toBe(true); + expect(beforeTool(planContext())).toBeUndefined(); + }); + + test("fails open on malformed contexts", () => { + expect(beforeTool({})).toBeUndefined(); + expect( + beforeTool({ snapshot: { conversationId: "conv-F" } }), + ).toBeUndefined(); + expect(beforeTool({ toolCall: {} })).toBeUndefined(); + }); +}); diff --git a/tests/harness/manifests.test.ts b/tests/harness/manifests.test.ts index 492cebc..82605ed 100644 --- a/tests/harness/manifests.test.ts +++ b/tests/harness/manifests.test.ts @@ -249,6 +249,28 @@ describe("shared assets (delivered by every harness)", () => { }); }); +describe("Cline plugin manifest (package.json cline field)", () => { + test("cline.plugins declares resolvable entry files with hooks+rules capabilities", () => { + const manifest = readJson("package.json") as { + cline?: { plugins?: { paths?: unknown; capabilities?: unknown }[] }; + }; + const plugins = manifest.cline?.plugins ?? []; + expect(plugins.length).toBeGreaterThan(0); + for (const plugin of plugins) { + expect(Array.isArray(plugin.paths)).toBe(true); + for (const entry of plugin.paths as string[]) { + expect(typeof entry).toBe("string"); + const resolved = resolveWithinRoot(REPO_ROOT, entry); + expect(fs.existsSync(resolved)).toBe(true); + expect(fs.statSync(resolved).isFile()).toBe(true); + } + const capabilities = plugin.capabilities as string[]; + expect(capabilities).toContain("hooks"); + expect(capabilities).toContain("rules"); + } + }); +}); + describe("version lockstep", () => { test.each([ ...VERSION_LOCKED_MANIFESTS, diff --git a/tests/harness/spec.ts b/tests/harness/spec.ts index dabf31c..a901688 100644 --- a/tests/harness/spec.ts +++ b/tests/harness/spec.ts @@ -108,6 +108,18 @@ export const HARNESSES: HarnessSpec[] = [ pathFields: [{ field: "main", kind: "file" }], hooks: null, }, + { + name: "Cline", + // Cline has no standalone manifest either; the package.json `cline` field + // declares plugin entry points, and skills/ is auto-discovered from the + // package root. The nested `cline.plugins[].paths[]` shape doesn't fit + // the dotted-string pathFields machinery, so entry-point resolution is + // asserted in a dedicated describe block in manifests.test.ts. + manifest: "package.json", + requiredFields: ["name", "version", "cline"], + pathFields: [], + hooks: null, + }, ]; export const BOOTSTRAP_MARKER = "";