open-plan-annotator 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Interactive plan annotation plugin for Claude Code",
8
- "version": "1.7.0"
8
+ "version": "1.8.1"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "open-plan-annotator",
13
13
  "source": "./",
14
14
  "description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
15
- "version": "1.7.0",
15
+ "version": "1.8.1",
16
16
  "author": {
17
17
  "name": "ndom91"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
3
  "description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
4
- "version": "1.7.0",
4
+ "version": "1.8.1",
5
5
  "author": {
6
6
  "name": "ndom91"
7
7
  },
package/hooks/hooks.json CHANGED
@@ -1,12 +1,29 @@
1
1
  {
2
2
  "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/install-runtime.mjs\"",
10
+ "timeout": 60
11
+ },
12
+ {
13
+ "type": "command",
14
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-context.mjs\"",
15
+ "timeout": 5
16
+ }
17
+ ]
18
+ }
19
+ ],
3
20
  "PermissionRequest": [
4
21
  {
5
22
  "matcher": "ExitPlanMode",
6
23
  "hooks": [
7
24
  {
8
25
  "type": "command",
9
- "command": "open-plan-annotator-binary",
26
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/open-plan-annotator.mjs\"",
10
27
  "timeout": 345600
11
28
  }
12
29
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "type": "module",
5
5
  "description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
6
6
  "author": "ndom91",
@@ -27,7 +27,8 @@
27
27
  },
28
28
  "files": [
29
29
  "bin/open-plan-annotator.mjs",
30
- "bin/open-plan-annotator-binary",
30
+ "scripts/install-runtime.mjs",
31
+ "scripts/session-context.mjs",
31
32
  "shared/cliHelp.mjs",
32
33
  "shared/cliMode.mjs",
33
34
  "shared/packageManager.mjs",
@@ -44,7 +45,6 @@
44
45
  "hooks/",
45
46
  "commands/",
46
47
  "skills/",
47
- "CLAUDE.md",
48
48
  "README.md"
49
49
  ],
50
50
  "scripts": {
@@ -61,19 +61,17 @@
61
61
  "lint": "biome check .",
62
62
  "lint:fix": "biome check --write .",
63
63
  "format": "biome format --write .",
64
- "do-release": "./scripts/release.sh",
65
- "prepack": "bun scripts/claude-pack-docs.mjs prepack",
66
- "postpack": "bun scripts/claude-pack-docs.mjs postpack"
64
+ "do-release": "./scripts/release.sh"
67
65
  },
68
66
  "devDependencies": {
69
- "@biomejs/biome": "^2.4.13",
70
- "@types/node": "^25.6.0",
67
+ "@biomejs/biome": "^2.4.15",
68
+ "@types/node": "^25.6.2",
71
69
  "@types/bun": "^1.3.13",
72
- "@typescript/native-preview": "^7.0.0-dev.20260430.1",
73
- "typebox": "^1.1.37"
70
+ "@typescript/native-preview": "^7.0.0-dev.20260511.1",
71
+ "typebox": "^1.1.38"
74
72
  },
75
73
  "dependencies": {
76
- "@opencode-ai/plugin": "^1.14.30"
74
+ "@opencode-ai/plugin": "^1.14.48"
77
75
  },
78
76
  "peerDependencies": {
79
77
  "typebox": "*"
@@ -84,12 +82,13 @@
84
82
  }
85
83
  },
86
84
  "optionalDependencies": {
87
- "@open-plan-annotator/runtime-darwin-arm64": "1.7.0",
88
- "@open-plan-annotator/runtime-darwin-x64": "1.7.0",
89
- "@open-plan-annotator/runtime-linux-arm64": "1.7.0",
90
- "@open-plan-annotator/runtime-linux-x64": "1.7.0"
85
+ "@open-plan-annotator/runtime-darwin-arm64": "1.8.1",
86
+ "@open-plan-annotator/runtime-darwin-x64": "1.8.1",
87
+ "@open-plan-annotator/runtime-linux-arm64": "1.8.1",
88
+ "@open-plan-annotator/runtime-linux-x64": "1.8.1"
91
89
  },
92
90
  "overrides": {
93
91
  "uuid": "^14.0.0"
94
- }
92
+ },
93
+ "packageManager": "bun@1.3.9"
95
94
  }
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ // Installs the per-platform Bun-compiled runtime binary for open-plan-annotator
3
+ // from the npm registry. Intended to run as a Claude Code SessionStart hook.
4
+ //
5
+ // Behavior:
6
+ // 1. Determines plugin version from the sibling package.json.
7
+ // 2. Picks the runtime package matching the host platform/arch.
8
+ // 3. If the target binary already exists at the resolver's fallback path
9
+ // (`packages/runtime-<platform>-<arch>/bin/open-plan-annotator`), exits 0.
10
+ // 4. Otherwise fetches the package manifest from the npm registry,
11
+ // downloads the tarball, verifies its integrity against `dist.integrity`,
12
+ // extracts it via system `tar`, and atomically moves the binary into place.
13
+ //
14
+ // Logs go to stderr only — stdout is reserved for hook protocol output.
15
+
16
+ import { execFileSync } from "node:child_process";
17
+ import { createHash } from "node:crypto";
18
+ import {
19
+ chmodSync,
20
+ existsSync,
21
+ mkdirSync,
22
+ mkdtempSync,
23
+ readFileSync,
24
+ renameSync,
25
+ rmSync,
26
+ writeFileSync,
27
+ } from "node:fs";
28
+ import { get as httpsGet } from "node:https";
29
+ import { tmpdir } from "node:os";
30
+ import { dirname, join, resolve } from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+
33
+ const SUPPORTED_PLATFORMS = new Set([
34
+ "darwin-arm64",
35
+ "darwin-x64",
36
+ "linux-arm64",
37
+ "linux-x64",
38
+ ]);
39
+
40
+ const here = dirname(fileURLToPath(import.meta.url));
41
+ const pluginRoot = resolve(here, "..");
42
+
43
+ const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8"));
44
+ const version = pkg.version;
45
+
46
+ const platformKey = `${process.platform}-${process.arch}`;
47
+ if (!SUPPORTED_PLATFORMS.has(platformKey)) {
48
+ process.stderr.write(
49
+ `open-plan-annotator: unsupported platform ${platformKey}. Supported: ${[...SUPPORTED_PLATFORMS].join(", ")}\n`,
50
+ );
51
+ process.exit(1);
52
+ }
53
+
54
+ const runtimePackage = `@open-plan-annotator/runtime-${platformKey}`;
55
+ const targetDir = join(pluginRoot, "packages", `runtime-${platformKey}`, "bin");
56
+ const targetBinary = join(targetDir, "open-plan-annotator");
57
+
58
+ if (existsSync(targetBinary)) {
59
+ // Fast path: nothing to do.
60
+ process.exit(0);
61
+ }
62
+
63
+ process.stderr.write(
64
+ `open-plan-annotator: installing runtime ${runtimePackage}@${version} for ${platformKey}…\n`,
65
+ );
66
+
67
+ const start = Date.now();
68
+
69
+ try {
70
+ const manifest = await fetchJson(
71
+ `https://registry.npmjs.org/${encodeURIComponent(runtimePackage)}/${encodeURIComponent(version)}`,
72
+ );
73
+
74
+ const tarballUrl = manifest?.dist?.tarball;
75
+ const integrity = manifest?.dist?.integrity;
76
+ if (typeof tarballUrl !== "string" || typeof integrity !== "string") {
77
+ throw new Error(
78
+ `npm manifest for ${runtimePackage}@${version} missing dist.tarball or dist.integrity`,
79
+ );
80
+ }
81
+
82
+ const tarballBuf = await fetchBuffer(tarballUrl);
83
+ verifyIntegrity(tarballBuf, integrity, runtimePackage, version);
84
+
85
+ const tmp = mkdtempSync(join(tmpdir(), "opa-runtime-"));
86
+ try {
87
+ const tarPath = join(tmp, "runtime.tgz");
88
+ writeFileSync(tarPath, tarballBuf);
89
+ execFileSync("tar", ["-xzf", tarPath, "-C", tmp], { stdio: "ignore" });
90
+
91
+ // npm tarballs always extract to a `package/` directory.
92
+ const extractedBinary = join(tmp, "package", "bin", "open-plan-annotator");
93
+ if (!existsSync(extractedBinary)) {
94
+ throw new Error(
95
+ `expected ${extractedBinary} after extracting ${runtimePackage}@${version}, not found`,
96
+ );
97
+ }
98
+
99
+ mkdirSync(targetDir, { recursive: true });
100
+ // renameSync may fail across filesystems; tmpdir is usually on the same
101
+ // device as the plugin cache, but if not, fall back to copy+unlink.
102
+ try {
103
+ renameSync(extractedBinary, targetBinary);
104
+ } catch (err) {
105
+ if (err?.code === "EXDEV") {
106
+ const buf = readFileSync(extractedBinary);
107
+ writeFileSync(targetBinary, buf);
108
+ } else {
109
+ throw err;
110
+ }
111
+ }
112
+ chmodSync(targetBinary, 0o755);
113
+ } finally {
114
+ rmSync(tmp, { recursive: true, force: true });
115
+ }
116
+
117
+ const elapsedMs = Date.now() - start;
118
+ process.stderr.write(
119
+ `open-plan-annotator: installed ${targetBinary} (${elapsedMs}ms)\n`,
120
+ );
121
+ process.exit(0);
122
+ } catch (err) {
123
+ const message = err instanceof Error ? err.message : String(err);
124
+ process.stderr.write(
125
+ `open-plan-annotator: failed to install runtime ${runtimePackage}@${version}: ${message}\n`,
126
+ );
127
+ process.stderr.write(
128
+ `open-plan-annotator: rerun a Claude Code session to retry; ExitPlanMode will not work until install succeeds.\n`,
129
+ );
130
+ process.exit(1);
131
+ }
132
+
133
+ /**
134
+ * Fetch a URL with redirect handling and return the response buffer.
135
+ *
136
+ * @param {string} url
137
+ * @param {number} [maxRedirects]
138
+ * @returns {Promise<Buffer>}
139
+ */
140
+ function fetchBuffer(url, maxRedirects = 5) {
141
+ return new Promise((resolveFetch, rejectFetch) => {
142
+ httpsGet(
143
+ url,
144
+ {
145
+ headers: {
146
+ "user-agent": `open-plan-annotator-installer/${version}`,
147
+ accept: "*/*",
148
+ },
149
+ },
150
+ (res) => {
151
+ const status = res.statusCode ?? 0;
152
+ if ((status === 301 || status === 302 || status === 307 || status === 308) && res.headers.location) {
153
+ if (maxRedirects <= 0) {
154
+ rejectFetch(new Error(`too many redirects for ${url}`));
155
+ res.resume();
156
+ return;
157
+ }
158
+ const next = new URL(res.headers.location, url).toString();
159
+ res.resume();
160
+ fetchBuffer(next, maxRedirects - 1).then(resolveFetch, rejectFetch);
161
+ return;
162
+ }
163
+ if (status !== 200) {
164
+ rejectFetch(new Error(`GET ${url} -> HTTP ${status}`));
165
+ res.resume();
166
+ return;
167
+ }
168
+ const chunks = [];
169
+ res.on("data", (chunk) => chunks.push(chunk));
170
+ res.on("end", () => resolveFetch(Buffer.concat(chunks)));
171
+ res.on("error", rejectFetch);
172
+ },
173
+ ).on("error", rejectFetch);
174
+ });
175
+ }
176
+
177
+ /**
178
+ * @param {string} url
179
+ * @returns {Promise<unknown>}
180
+ */
181
+ async function fetchJson(url) {
182
+ const buf = await fetchBuffer(url);
183
+ return JSON.parse(buf.toString("utf8"));
184
+ }
185
+
186
+ /**
187
+ * Verify a buffer against an npm Subresource Integrity string (e.g. "sha512-…").
188
+ *
189
+ * @param {Buffer} buf
190
+ * @param {string} integrity
191
+ * @param {string} pkgName
192
+ * @param {string} pkgVersion
193
+ */
194
+ function verifyIntegrity(buf, integrity, pkgName, pkgVersion) {
195
+ const match = integrity.match(/^(sha\d+)-([A-Za-z0-9+/=]+)$/);
196
+ if (!match) {
197
+ throw new Error(
198
+ `unrecognized integrity format "${integrity}" for ${pkgName}@${pkgVersion}`,
199
+ );
200
+ }
201
+ const [, algo, expectedB64] = match;
202
+ const actualB64 = createHash(algo).update(buf).digest("base64");
203
+ if (actualB64 !== expectedB64) {
204
+ throw new Error(
205
+ `integrity mismatch for ${pkgName}@${pkgVersion}\n expected ${algo}: ${expectedB64}\n actual ${algo}: ${actualB64}`,
206
+ );
207
+ }
208
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook: inject non-negotiable plan-routing instructions into
3
+ // Claude's session context. Returns a JSON `hookSpecificOutput.additionalContext`
4
+ // payload that Claude Code reads and injects into the session prompt.
5
+ //
6
+ // Why this is a hook and not a skill: skills are advisory and load lazily —
7
+ // Claude judges relevance from frontmatter and can rationalize past it,
8
+ // resulting in inline plans that bypass the annotator. Hook-injected context
9
+ // is unconditional and always present, matching the binary nature of the
10
+ // "always route plans through annotator" requirement.
11
+
12
+ const additionalContext = `[open-plan-annotator] This session has the open-plan-annotator plugin installed.
13
+
14
+ HARD RULE — any response that would contain ANY of the following MUST be routed through EnterPlanMode → ExitPlanMode (which fires the annotator UI):
15
+ • a multi-step proposal
16
+ • 3 or more file paths to create or modify
17
+ • 2 or more option/tradeoff comparisons for the user to choose between
18
+ • a section header like "Plan:" / "Steps:" / "Approach:" / "Implementation:" / "Concrete changes:"
19
+ • a numbered or bulleted list of action items the user is expected to react to
20
+ • a closing question like "OK to proceed?" / "sound good?" / "shall I…?"
21
+
22
+ Never paste plans inline. The annotator IS the discussion surface — tradeoffs, options, and open questions belong inside the plan body where the user can comment on each one. Mixed-signal user messages (a directive plus "what do you think?") still route through the annotator; the exploratory clause means "include alternatives in the plan", not "skip the plan".
23
+
24
+ Workflow:
25
+ • Assistant-initiated (you decided a plan is needed): call EnterPlanMode → draft → ExitPlanMode.
26
+ • User-initiated ("draft a plan", "/plan", etc.): invoke /annotate-plan <task>.
27
+
28
+ Full trigger list and self-check rules in skill plan-review-triggers.`;
29
+
30
+ process.stdout.write(
31
+ JSON.stringify({
32
+ hookSpecificOutput: {
33
+ hookEventName: "SessionStart",
34
+ additionalContext,
35
+ },
36
+ }),
37
+ );
@@ -1,6 +1,7 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import fs from "node:fs";
2
- import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
+ import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
6
7
  export const RUNTIME_PACKAGE_MAP = {
@@ -19,12 +20,45 @@ export function getRuntimePackageName(platform = process.platform, arch = proces
19
20
  }
20
21
 
21
22
  export function resolveRuntimeBinary(options = {}) {
23
+ const attempt = () => tryResolveRuntimeBinary(options);
24
+
25
+ const first = attempt();
26
+ if (first.ok) return first.value;
27
+
28
+ // Skip installer for hard-failure conditions (unsupported platform): no
29
+ // amount of installing will produce a binary, and we'd waste a network
30
+ // round trip downloading the host's binary unnecessarily.
31
+ const platform = options.platform ?? process.platform;
32
+ const arch = options.arch ?? process.arch;
33
+ if (!getRuntimePackageName(platform, arch)) {
34
+ throw first.error;
35
+ }
36
+
37
+ // Last-resort recovery: invoke the SessionStart installer synchronously and
38
+ // retry resolution once. Useful for hosts that fetch this package without
39
+ // running `npm install` (and therefore skip the optionalDependencies that
40
+ // would otherwise pull the matching runtime binary). The Claude Code plugin
41
+ // install path is the canonical example, but it triggers the same script
42
+ // earlier via its SessionStart hook; this branch covers everyone else.
43
+ if (runInstaller(options)) {
44
+ const second = attempt();
45
+ if (second.ok) return second.value;
46
+ throw second.error;
47
+ }
48
+
49
+ throw first.error;
50
+ }
51
+
52
+ function tryResolveRuntimeBinary(options = {}) {
22
53
  const platform = options.platform ?? process.platform;
23
54
  const arch = options.arch ?? process.arch;
24
55
  const packageName = getRuntimePackageName(platform, arch);
25
56
 
26
57
  if (!packageName) {
27
- throw new Error(`Unsupported platform ${getRuntimePlatformKey(platform, arch)}`);
58
+ return {
59
+ ok: false,
60
+ error: new Error(`Unsupported platform ${getRuntimePlatformKey(platform, arch)}`),
61
+ };
28
62
  }
29
63
 
30
64
  const requireFrom = createRequire(options.parentUrl ?? import.meta.url);
@@ -37,27 +71,66 @@ export function resolveRuntimeBinary(options = {}) {
37
71
  const workspaceBinaryPath = path.join(workspaceRoot, "packages", packageName.split("/").at(-1) ?? "", "bin", "open-plan-annotator");
38
72
  if (fs.existsSync(workspaceBinaryPath)) {
39
73
  return {
40
- packageName,
41
- packageRoot: path.dirname(path.dirname(workspaceBinaryPath)),
42
- binaryPath: workspaceBinaryPath,
74
+ ok: true,
75
+ value: {
76
+ packageName,
77
+ packageRoot: path.dirname(path.dirname(workspaceBinaryPath)),
78
+ binaryPath: workspaceBinaryPath,
79
+ },
43
80
  };
44
81
  }
45
82
 
46
- throw new Error(
47
- `Missing runtime package ${packageName}. Reinstall open-plan-annotator for ${getRuntimePlatformKey(platform, arch)}.`,
48
- );
83
+ return {
84
+ ok: false,
85
+ error: new Error(
86
+ `Missing runtime package ${packageName}. Reinstall open-plan-annotator for ${getRuntimePlatformKey(platform, arch)}.`,
87
+ ),
88
+ };
49
89
  }
50
90
 
51
91
  const packageRoot = path.dirname(packageJsonPath);
52
92
  const binaryPath = path.join(packageRoot, "bin", "open-plan-annotator");
53
93
 
54
94
  if (!fs.existsSync(binaryPath)) {
55
- throw new Error(`Runtime package ${packageName} is installed but ${binaryPath} is missing. Rebuild or reinstall it.`);
95
+ return {
96
+ ok: false,
97
+ error: new Error(`Runtime package ${packageName} is installed but ${binaryPath} is missing. Rebuild or reinstall it.`),
98
+ };
56
99
  }
57
100
 
58
101
  return {
59
- packageName,
60
- packageRoot,
61
- binaryPath,
102
+ ok: true,
103
+ value: {
104
+ packageName,
105
+ packageRoot,
106
+ binaryPath,
107
+ },
62
108
  };
63
109
  }
110
+
111
+ /**
112
+ * Run `scripts/install-runtime.mjs` synchronously to lay down the per-platform
113
+ * runtime binary. Returns true if the installer ran successfully, false if it
114
+ * was unavailable or failed. Diagnostics go to stderr; this function never
115
+ * throws so a failure here falls through to the original missing-runtime
116
+ * error from the caller.
117
+ */
118
+ function runInstaller(_options = {}) {
119
+ // Opt-out for callers that want to keep the old behavior (e.g. tests that
120
+ // assert the error path) or environments where re-entry would be harmful.
121
+ if (process.env.OPEN_PLAN_ANNOTATOR_SKIP_INSTALL === "1") return false;
122
+
123
+ const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
124
+ const installerPath = path.join(workspaceRoot, "scripts", "install-runtime.mjs");
125
+ if (!fs.existsSync(installerPath)) return false;
126
+
127
+ try {
128
+ execFileSync(process.execPath, [installerPath], {
129
+ stdio: ["ignore", "ignore", "inherit"],
130
+ timeout: 60_000,
131
+ });
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
@@ -6,10 +6,10 @@ description: Required reading whenever drafting any multi-step proposal, impleme
6
6
  # Plan Review Triggers
7
7
 
8
8
  <!--
9
- Maintainer note: trigger rules below are mirrored in `CLAUDE.plugin.md` at the
10
- repo root. Skills are how the rules reach users who installed the Claude Code
11
- plugin via the marketplace (repo-root markdown does not load through that
12
- path). Keep both files in sync when editing trigger rules.
9
+ This skill is the long-form trigger reference for end users who have the
10
+ open-plan-annotator plugin installed. The terse always-on reminder lives in
11
+ `scripts/session-context.mjs` (injected via the `SessionStart` hook); keep
12
+ the two in rough sync when editing trigger rules.
13
13
  -->
14
14
 
15
15
  This plugin (open-plan-annotator) installs a hook on `ExitPlanMode` that opens a browser UI where the user marks up plans directly — deletions, replacements, insertions, comments — and returns structured feedback. Inline markdown plans bypass that workflow.
package/CLAUDE.md DELETED
@@ -1,105 +0,0 @@
1
- # open-plan-annotator: Plan Review Instructions
2
-
3
- <!--
4
- Maintainer note: this file's content is mirrored in
5
- `skills/plan-review-triggers/SKILL.md` because Claude Code plugin installs do
6
- not load repo-root markdown — only skills/, commands/, hooks/. Keep both files
7
- in sync when editing trigger rules. Sections that diverge intentionally
8
- (audience, framing) are still close enough that a side-by-side diff catches
9
- drift quickly.
10
- -->
11
-
12
- ## Why This Plugin Exists
13
-
14
- Markdown plans rendered in chat are read-only for the user. To give feedback they have to retype sections inline, which is awkward in a terminal. open-plan-annotator opens a browser UI where the user can mark up your plan directly — delete lines, replace text, insert steps, leave comments. Their annotations come back to you as structured feedback.
15
-
16
- **This is the user's required review surface for any multi-section proposal.** Inline plans bypass the tool the user installed and silently lose their preferred feedback workflow.
17
-
18
- This is distinct from `plannotator-review` (post-implementation code review). open-plan-annotator runs *before* code is written.
19
-
20
- ## Hard Rule: Annotator Required When Triggers Match
21
-
22
- When any trigger below fires, you MUST route through the annotator. "Prefer" / "consider" do not apply. Inline plans are a bug.
23
-
24
- - Assistant-initiated (you decided a plan is needed): call `EnterPlanMode`, draft, then `ExitPlanMode` — this fires the annotator hook.
25
- - User-initiated ("draft a plan", "/plan", etc.): invoke the `/annotate-plan <task>` slash command. It runs the same plan-mode flow.
26
-
27
- ## Trigger: Mechanical Heuristic
28
-
29
- Annotator REQUIRED when the response would contain ANY of:
30
-
31
- - 3 or more file paths to be created or modified
32
- - 2 or more option/tradeoff comparisons the user is asked to choose between
33
- - A `Plan:`, `Steps:`, `Concrete changes:`, `Implementation:`, `Approach:`, or similar section header
34
- - A numbered or bulleted list of action items the user is expected to react to
35
- - A multi-section proposal with decision points
36
-
37
- This is a hard gate. Count file paths. Count options. If the count crosses the line, you do not get to write the inline response.
38
-
39
- ## Trigger: Task Shape
40
-
41
- Enter plan mode (or invoke the slash command) before any of:
42
-
43
- - Creating or modifying more than 2 files
44
- - Architectural or structural changes
45
- - Refactoring, migration, or feature additions
46
- - Bug fixes that require investigation
47
- - Anything the user has not explicitly described step-by-step
48
-
49
- ## Trigger: Phrase Match
50
-
51
- Treat these as plan triggers regardless of length:
52
-
53
- - User says "draft a plan", "let's plan X", "what's the approach", "give me options", "how should we tackle this", "what would it look like"
54
- - Your response would contain "recommended approach", "implementation plan", "proposed fix", "rollout plan", "here's what I'd do", or "concrete file changes"
55
- - Any moment you would otherwise ask "want me to proceed?" / "shall I draft this?" / "OK to proceed?" / "sound good?"
56
-
57
- ## Mixed Signals: Still Trigger
58
-
59
- When a user message combines directives ("let's add X", "can we Y") with an exploratory question ("what do you think?"), the annotator still applies. Do NOT collapse into a 2-3 sentence inline response just because one clause was exploratory.
60
-
61
- The annotator IS the discussion surface. Tradeoffs, options, and open questions belong inside the plan body where the user can comment on each one — not in flat chat where they would have to retype your bullets to push back. Treat exploratory clauses as "include alternatives in the plan", not "skip the plan".
62
-
63
- ## Self-Check Before Sending
64
-
65
- Before emitting any response, scan it for these strings:
66
-
67
- - "Concrete file changes", "Concrete changes", "Plan:", "Steps:", "Implementation:", "Approach:"
68
- - "OK to proceed?", "Want me to proceed?", "Shall I…?", "Sound good?", "Confirm…?"
69
- - 3+ lines that look like file paths (`foo/bar.tsx`, `path/to/file.ts`)
70
- - Numbered list of more than 2 items each describing a code change
71
- - "**N.**" / "**Fix N —**" headers introducing proposed changes
72
-
73
- If ANY match: stop, discard the inline response, route through the annotator instead.
74
-
75
- ## Do NOT Trigger For
76
-
77
- - Single-line fixes, typos, renames
78
- - Direct factual answers
79
- - Status updates or progress reports
80
- - Plans the user has already approved (do not re-prompt)
81
- - Pure research or exploration with no proposed actions
82
- - Trivial questions where a plan would be overhead
83
- - Replies to the user's questions ABOUT an already-submitted plan (answer the question, do not re-submit)
84
-
85
- ## Plan Quality Standards
86
-
87
- When writing a plan, include:
88
-
89
- - Brief summary of what you understood the task to require
90
- - Specific files you intend to create or modify and why
91
- - Any assumptions you are making
92
- - Explicit question if anything is ambiguous
93
- - Tradeoffs / option comparisons inline in the plan (since mixed-signal user messages route here)
94
-
95
- ## Workflow
96
-
97
- **Assistant-initiated (you decided a plan is needed):**
98
- draft mentally → call `EnterPlanMode` → draft plan → call `ExitPlanMode` → annotator opens → user annotates → revise based on feedback → re-exit when aligned.
99
-
100
- **User-initiated (user asked for a plan):**
101
- invoke `/annotate-plan <task>`. The command enters plan mode, drafts a plan, and exits to fire the annotator. Do not paste a multi-section plan inline and ask "sound good?" — that bypasses the annotator.
102
-
103
- ## Slash Command
104
-
105
- `/annotate-plan <task>` is the canonical user-invoked entry point. When the user invokes it, follow the command body exactly. When you (the assistant) decide a plan is needed without the user invoking the command, prefer `EnterPlanMode` directly — the slash command is for user invocation, the tool is for assistant initiation. Both paths fire the same annotator hook.
Binary file