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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/hooks.json +18 -1
- package/package.json +15 -16
- package/scripts/install-runtime.mjs +208 -0
- package/scripts/session-context.mjs +37 -0
- package/shared/runtimeResolver.mjs +85 -12
- package/skills/plan-review-triggers/SKILL.md +4 -4
- package/CLAUDE.md +0 -105
- package/bin/open-plan-annotator-binary +0 -0
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Interactive plan annotation plugin for Claude Code",
|
|
8
|
-
"version": "1.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
70
|
-
"@types/node": "^25.6.
|
|
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.
|
|
73
|
-
"typebox": "^1.1.
|
|
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.
|
|
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.
|
|
88
|
-
"@open-plan-annotator/runtime-darwin-x64": "1.
|
|
89
|
-
"@open-plan-annotator/runtime-linux-arm64": "1.
|
|
90
|
-
"@open-plan-annotator/runtime-linux-x64": "1.
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|