@workit-dev/cursor 0.0.1 → 1.0.0

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.
Files changed (58) hide show
  1. package/.cursor-plugin/plugin.json +38 -0
  2. package/README.md +56 -43
  3. package/assets/logo.svg +4 -0
  4. package/assets/templates/workit-contract.md +13 -0
  5. package/commands/wk-architecture.md +5 -0
  6. package/commands/wk-bdd.md +5 -0
  7. package/commands/wk-continue.md +5 -0
  8. package/commands/wk-debug.md +5 -0
  9. package/commands/wk-deslop.md +5 -0
  10. package/commands/wk-fanout.md +5 -0
  11. package/commands/wk-implement.md +5 -0
  12. package/commands/wk-retro.md +5 -0
  13. package/commands/wk-review.md +5 -0
  14. package/commands/wk-shape.md +5 -0
  15. package/commands/wk-ship.md +5 -0
  16. package/commands/wk-test-audit.md +5 -0
  17. package/commands/wk-verify-app.md +5 -0
  18. package/dist/cursor-session-start.js +27182 -0
  19. package/dist/mcp-server.js +30655 -0
  20. package/dist/workit-hook.js +24510 -0
  21. package/hooks/hooks-cursor.json +48 -0
  22. package/hooks/launch-runtime.mjs +276 -0
  23. package/hooks/launch.mjs +137 -0
  24. package/hooks/node-resolve.mjs +249 -0
  25. package/hooks/workit-paths.mjs +242 -0
  26. package/mcp.json +8 -0
  27. package/package.json +54 -6
  28. package/rules/workit-contract.mdc +38 -0
  29. package/skills/workit-architecture/SKILL.md +72 -0
  30. package/skills/workit-architecture/agents/openai.yaml +3 -0
  31. package/skills/workit-architecture/references/instruction-files.md +52 -0
  32. package/skills/workit-architecture/references/vocabulary.md +53 -0
  33. package/skills/workit-bdd/SKILL.md +53 -0
  34. package/skills/workit-continue/SKILL.md +54 -0
  35. package/skills/workit-debug/SKILL.md +45 -0
  36. package/skills/workit-deslop/SKILL.md +58 -0
  37. package/skills/workit-fanout/SKILL.md +71 -0
  38. package/skills/workit-fanout/references/brief.md +141 -0
  39. package/skills/workit-fanout/references/orchestration.md +169 -0
  40. package/skills/workit-implement/SKILL.md +71 -0
  41. package/skills/workit-retro/SKILL.md +72 -0
  42. package/skills/workit-retro/agents/openai.yaml +3 -0
  43. package/skills/workit-retro/references/sources.md +35 -0
  44. package/skills/workit-retro/references/steering.md +39 -0
  45. package/skills/workit-review/SKILL.md +66 -0
  46. package/skills/workit-review/references/impact.md +24 -0
  47. package/skills/workit-shape/SKILL.md +71 -0
  48. package/skills/workit-shape/references/diagrams.md +17 -0
  49. package/skills/workit-shape/references/knowledge.md +61 -0
  50. package/skills/workit-shape/references/mockups.md +15 -0
  51. package/skills/workit-shape/references/slicing.md +44 -0
  52. package/skills/workit-ship/SKILL.md +71 -0
  53. package/skills/workit-ship/references/changelog.md +44 -0
  54. package/skills/workit-ship/references/pr-body.md +32 -0
  55. package/skills/workit-ship/references/release.md +39 -0
  56. package/skills/workit-test-audit/SKILL.md +54 -0
  57. package/skills/workit-verify-app/SKILL.md +68 -0
  58. package/skills/workit-verify-app/references/template.md +51 -0
@@ -0,0 +1,249 @@
1
+ // Finds a Node that can run a Workit hook when the PATH node is below the
2
+ // package's engines floor (a repo pinned to Node 16/18/22 by a version
3
+ // manager). Plain JavaScript with node: builtins only and no syntax newer than
4
+ // Node 16 supports: the host launchers import it (or carry a byte-equal copy,
5
+ // a test holds them equal) and must start on the very Node they are replacing.
6
+ // Side-effect free: importing it runs nothing; no process is spawned here.
7
+ //
8
+ // Order: (1) $WORKIT_NODE; (2) the node `workit setup`/`upgrade` recorded in
9
+ // <state dir>/node.json (workit-paths.mjs); (3) a scan of version-manager installs (fnm, nvm,
10
+ // volta, mise, asdf and the Windows layouts), highest version >= the floor.
11
+ import { spawnSync } from "node:child_process";
12
+ import { readdirSync, readFileSync, statSync } from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import { stateDir } from "./workit-paths.mjs";
16
+
17
+ /** The Node major the hooks need; a test holds it equal to every package's engines.node. */
18
+ export const HOOK_NODE_FLOOR = [24, 0, 0];
19
+
20
+ /** Set on the re-executed child so a launcher never re-executes twice. */
21
+ export const HOOK_NODE_REEXEC_ENV = "WORKIT_HOOK_REEXEC";
22
+
23
+ const MAX_ENTRIES_PER_ROOT = 200;
24
+
25
+ /** @param {unknown} text @returns {number[] | null} */
26
+ export const parseNodeVersion = (text) => {
27
+ const found = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(String(text).trim());
28
+ return found ? [Number(found[1]), Number(found[2] || 0), Number(found[3] || 0)] : null;
29
+ };
30
+
31
+ /** @param {number[]} a @param {number[]} b negative when a < b */
32
+ export const compareVersions = (a, b) => {
33
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i];
34
+ return 0;
35
+ };
36
+
37
+ /** The `>=X[.Y[.Z]]` minimum of an engines.node range, or null. @param {unknown} range */
38
+ export const floorOfRange = (range) => {
39
+ const min = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(String(range === undefined ? "" : range));
40
+ return min ? [Number(min[1]), Number(min[2] || 0), Number(min[3] || 0)] : null;
41
+ };
42
+
43
+ /** Workit's state dir (workit-paths.mjs, the one resolver). @param {NodeJS.ProcessEnv} env */
44
+ export const hookStateDir = (env) => stateDir(env);
45
+
46
+ /** @param {string} file */
47
+ const isFile = (file) => {
48
+ try {
49
+ return statSync(file).isFile();
50
+ } catch {
51
+ return false;
52
+ }
53
+ };
54
+
55
+ /** @param {string} file */
56
+ const readJson = (file) => {
57
+ try {
58
+ return JSON.parse(readFileSync(file, "utf8"));
59
+ } catch {
60
+ return undefined;
61
+ }
62
+ };
63
+
64
+ /**
65
+ * The version-manager directories that hold one node per child directory:
66
+ * `{ dir, bin }` where the node is `<dir>/<child>/<bin>`.
67
+ * @param {NodeJS.ProcessEnv} env @param {string} platform
68
+ */
69
+ const scanRoots = (env, platform) => {
70
+ const home = env.HOME || env.USERPROFILE || os.homedir();
71
+ const win = platform === "win32";
72
+ const exe = win ? "node.exe" : "node";
73
+ const bin = win ? exe : path.join("bin", exe);
74
+ /** @type {Array<{ dir: string, bin: string }>} */
75
+ const roots = [];
76
+ const add = (dir, rel) => {
77
+ if (dir) roots.push({ dir, bin: rel });
78
+ };
79
+ // fnm: <dir>/node-versions/<v>/installation/bin/node
80
+ const fnmDirs = [
81
+ env.FNM_DIR,
82
+ win ? path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "fnm") : "",
83
+ win ? path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "fnm") : "",
84
+ env.XDG_DATA_HOME ? path.join(env.XDG_DATA_HOME, "fnm") : "",
85
+ path.join(home, ".local", "share", "fnm"),
86
+ path.join(home, "Library", "Application Support", "fnm"),
87
+ path.join(home, ".fnm"),
88
+ ];
89
+ for (const dir of fnmDirs)
90
+ if (dir) add(path.join(dir, "node-versions"), path.join("installation", bin));
91
+ // nvm: <dir>/versions/node/<v>/bin/node; nvm-windows: <NVM_HOME>/<v>/node.exe
92
+ for (const dir of [env.NVM_DIR, path.join(home, ".nvm")])
93
+ if (dir) add(path.join(dir, "versions", "node"), bin);
94
+ if (env.NVM_HOME) add(env.NVM_HOME, exe);
95
+ // volta: <dir>/tools/image/node/<v>/bin/node
96
+ const volta = env.VOLTA_HOME || (win ? "" : path.join(home, ".volta"));
97
+ if (volta) add(path.join(volta, "tools", "image", "node"), bin);
98
+ if (win)
99
+ add(
100
+ path.join(
101
+ env.LOCALAPPDATA || path.join(home, "AppData", "Local"),
102
+ "Volta",
103
+ "tools",
104
+ "image",
105
+ "node",
106
+ ),
107
+ bin,
108
+ );
109
+ // mise and asdf: <dir>/installs/node[js]/<v>/bin/node
110
+ const mise = env.MISE_DATA_DIR || path.join(home, ".local", "share", "mise");
111
+ add(path.join(mise, "installs", "node"), bin);
112
+ const asdf = env.ASDF_DATA_DIR || path.join(home, ".asdf");
113
+ add(path.join(asdf, "installs", "nodejs"), bin);
114
+ return roots;
115
+ };
116
+
117
+ /**
118
+ * Every installed node a version manager lists, with its version: directory
119
+ * listings only, at most MAX_ENTRIES_PER_ROOT per root.
120
+ * @param {NodeJS.ProcessEnv} env @param {string} [platform]
121
+ * @returns {Array<{ path: string, version: number[], source: string }>}
122
+ */
123
+ export const scanInstalledNodes = (env, platform = process.platform) => {
124
+ /** @type {Array<{ path: string, version: number[], source: string }>} */
125
+ const found = [];
126
+ for (const root of scanRoots(env, platform)) {
127
+ let names;
128
+ try {
129
+ names = readdirSync(root.dir).slice(0, MAX_ENTRIES_PER_ROOT);
130
+ } catch {
131
+ continue;
132
+ }
133
+ for (const name of names) {
134
+ const version = parseNodeVersion(name);
135
+ if (!version || !/^v?\d/.test(name)) continue;
136
+ // volta's layout nests nothing; fnm's, nvm's, mise's and asdf's differ only in `bin`.
137
+ const candidate = path.join(root.dir, name, root.bin);
138
+ if (isFile(candidate)) found.push({ path: candidate, version, source: root.dir });
139
+ }
140
+ }
141
+ return found;
142
+ };
143
+
144
+ /**
145
+ * The node the hooks should run on, or null. `floor` is [major, minor, patch].
146
+ * @param {{ env: NodeJS.ProcessEnv, floor: number[], platform?: string }} options
147
+ * @returns {{ path: string, version: string | null, source: "env" | "recorded" | "scan" } | null}
148
+ */
149
+ export const resolveHookNode = ({ env, floor, platform = process.platform }) => {
150
+ if (env.WORKIT_NODE && isFile(env.WORKIT_NODE))
151
+ return { path: env.WORKIT_NODE, version: null, source: "env" };
152
+ const recorded = readJson(path.join(hookStateDir(env), "node.json"));
153
+ if (recorded && typeof recorded.path === "string" && isFile(recorded.path)) {
154
+ const version = parseNodeVersion(recorded.version);
155
+ if (version && compareVersions(version, floor) >= 0)
156
+ return { path: recorded.path, version: String(recorded.version), source: "recorded" };
157
+ }
158
+ let best = null;
159
+ for (const entry of scanInstalledNodes(env, platform))
160
+ if (
161
+ compareVersions(entry.version, floor) >= 0 &&
162
+ (!best || compareVersions(entry.version, best.version) > 0)
163
+ )
164
+ best = entry;
165
+ return best ? { path: best.path, version: best.version.join("."), source: "scan" } : null;
166
+ };
167
+
168
+ /**
169
+ * The fail-open line for a node below the floor: null when `found` is at or
170
+ * above `floor`, or unparseable (an unreadable version never blocks).
171
+ * @param {string} found @param {number[] | null} floor @param {string} range
172
+ */
173
+ export const floorMessage = (found, floor, range) => {
174
+ if (!floor) return null;
175
+ const have = parseNodeVersion(found);
176
+ if (!have || compareVersions(have, floor) >= 0) return null;
177
+ return `needs Node ${range}, found ${found}`;
178
+ };
179
+
180
+ /** What the agent is told once, at SessionStart, when no Node >= the floor resolves. */
181
+ export const nodeFloorNotice = (found, range) =>
182
+ `Workit hooks are off: the Node on PATH is ${String(found).replace(/^v/, "")}, below the required ${range}, and no Node 24+ was found. ` +
183
+ "Install Node 24 (fnm install 24, nvm install 24) or run `workit setup` from a Node 24 shell so Workit can record it; " +
184
+ "`workit doctor` (check hook_node) shows what the hooks run on.";
185
+
186
+ /**
187
+ * Where setup and upgrade record the Node they run on. A no-op for a non-node
188
+ * binary (bun) or a Node below the floor.
189
+ * @param {{ env: NodeJS.ProcessEnv, execPath: string, version: string, floor: number[] }} options
190
+ * @returns {string | null} the file written
191
+ */
192
+ export const nodeRecord = ({ env, execPath, version, floor }) => {
193
+ const parsed = parseNodeVersion(version);
194
+ if (!parsed || compareVersions(parsed, floor) < 0) return null;
195
+ if (!/^node(?:\.exe)?$/i.test(path.basename(execPath))) return null;
196
+ return path.join(hookStateDir(env), "node.json");
197
+ };
198
+
199
+ /**
200
+ * Run `script args…` on `nodePath` with this process's stdin, stdout and
201
+ * stderr passed through. The child carries the re-exec guard and a real
202
+ * version (WORKIT_HOOK_NODE_VERSION, a test seam, is dropped).
203
+ * @returns {number | null} the child's exit code, or null when it could not start
204
+ */
205
+ export const reexecOnNode = (nodePath, script, args, env) => {
206
+ const child = Object.assign({}, env, { [HOOK_NODE_REEXEC_ENV]: "1" });
207
+ delete child.WORKIT_HOOK_NODE_VERSION;
208
+ const run = spawnSync(nodePath, [script].concat(args), {
209
+ stdio: "inherit",
210
+ env: child,
211
+ windowsHide: true,
212
+ });
213
+ if (run.error) return null;
214
+ return run.status === null ? 1 : run.status;
215
+ };
216
+
217
+ /**
218
+ * What a launcher does on a Node below its floor: re-run itself on a resolved
219
+ * Node (the child's exit code is returned), else fail open: one stderr line,
220
+ * and `{}` on stdout, except that a start event answers with `notice(text)` so
221
+ * the agent is told the hooks are off. Resolves to the exit code.
222
+ * @param {{ host: string, range: string, floor: number[], found: string, script: string,
223
+ * argv: string[], env: NodeJS.ProcessEnv, isStart: (event: unknown) => boolean,
224
+ * notice: (text: string) => unknown }} options
225
+ */
226
+ export const handleBelowFloor = async (options) => {
227
+ if (!options.env[HOOK_NODE_REEXEC_ENV]) {
228
+ const hit = resolveHookNode({ env: options.env, floor: options.floor });
229
+ const code = hit ? reexecOnNode(hit.path, options.script, options.argv, options.env) : null;
230
+ if (code !== null) return code;
231
+ }
232
+ process.stderr.write(
233
+ `[workit] ${options.host} hook unavailable: needs Node ${options.range}, found ${options.found}\n`,
234
+ );
235
+ let early = "";
236
+ for await (const chunk of process.stdin) early += String(chunk);
237
+ let event;
238
+ try {
239
+ event = JSON.parse(early).hook_event_name;
240
+ } catch {
241
+ event = undefined;
242
+ }
243
+ process.stdout.write(
244
+ options.isStart(event)
245
+ ? `${JSON.stringify(options.notice(nodeFloorNotice(options.found, options.range)))}\n`
246
+ : "{}\n",
247
+ );
248
+ return 0;
249
+ };
@@ -0,0 +1,242 @@
1
+ // The one resolver for every Workit file location (docs/agents/file-locations.md).
2
+ // Kinds: config, state, data, cache and runtime. Linux and macOS follow the
3
+ // XDG Base Directory spec; Windows uses %APPDATA% (config, roaming) and
4
+ // %LOCALAPPDATA% (state, data, cache). An absolute XDG_*_HOME wins on every
5
+ // platform; a relative one is ignored, as the spec says.
6
+ //
7
+ // Plain JavaScript with node: builtins only and no syntax newer than Node 16:
8
+ // TypeScript imports it, and the hook launchers carry a byte-equal copy
9
+ // (claude-code bin/, codex bin/, cursor hooks/; a test holds them equal). The
10
+ // POSIX shell launchers embed the *_SH functions below verbatim, and a test
11
+ // runs them against the JavaScript for the same environment.
12
+ // Side-effect free on import; only migrateLegacyDirs touches the disk.
13
+ import { existsSync, lstatSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+
17
+ /** Every location kind the resolver knows. */
18
+ export const WORKIT_DIR_KINDS = ["config", "state", "data", "cache", "runtime"];
19
+
20
+ /** Environment overrides per kind, first non-empty wins; each names the directory itself. */
21
+ export const WORKIT_DIR_OVERRIDES = {
22
+ config: ["WORKIT_CONFIG", "WORKIT_CONFIG_DIR"],
23
+ state: ["WORKIT_STATE_DIR"],
24
+ data: [],
25
+ cache: [],
26
+ runtime: [],
27
+ };
28
+
29
+ /** The XDG base variable each kind follows (`<base>/workit`). */
30
+ export const WORKIT_DIR_XDG = {
31
+ config: "XDG_CONFIG_HOME",
32
+ state: "XDG_STATE_HOME",
33
+ data: "XDG_DATA_HOME",
34
+ cache: "XDG_CACHE_HOME",
35
+ runtime: "XDG_RUNTIME_DIR",
36
+ };
37
+
38
+ /** The XDG defaults under $HOME when the base variable is unset. */
39
+ const XDG_DEFAULTS = {
40
+ config: [".config"],
41
+ state: [".local", "state"],
42
+ data: [".local", "share"],
43
+ cache: [".cache"],
44
+ };
45
+
46
+ /**
47
+ * The user's home: $HOME, else %USERPROFILE%, else the OS record.
48
+ * @param {NodeJS.ProcessEnv} env
49
+ */
50
+ export const userHome = (env) => env.HOME || env.USERPROFILE || os.homedir();
51
+
52
+ /**
53
+ * The kind's XDG base when set to an absolute path (the spec ignores a relative one).
54
+ * @param {string} kind @param {NodeJS.ProcessEnv} env
55
+ */
56
+ const xdgBase = (kind, env) => {
57
+ const base = env[WORKIT_DIR_XDG[kind]];
58
+ return base && path.isAbsolute(base) ? base : "";
59
+ };
60
+
61
+ /**
62
+ * Where one kind of Workit file lives.
63
+ * @param {"config" | "state" | "data" | "cache" | "runtime"} kind
64
+ * @param {NodeJS.ProcessEnv} [env]
65
+ * @param {string} [platform]
66
+ * @returns {string}
67
+ */
68
+ export const workitDir = (kind, env = process.env, platform = process.platform) => {
69
+ for (const name of WORKIT_DIR_OVERRIDES[kind]) if (env[name]) return String(env[name]);
70
+ const base = xdgBase(kind, env);
71
+ if (base) return path.join(base, "workit");
72
+ if (kind === "runtime") {
73
+ if (platform === "win32") return path.join(env.TEMP || env.TMP || os.tmpdir(), "workit");
74
+ const uid = typeof process.getuid === "function" ? process.getuid() : os.userInfo().username;
75
+ return path.join(env.TMPDIR || os.tmpdir(), `workit-${uid}`);
76
+ }
77
+ const home = userHome(env);
78
+ if (platform !== "win32") return path.join(home, ...XDG_DEFAULTS[kind], "workit");
79
+ if (kind === "config")
80
+ return path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "workit");
81
+ const local = path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "workit");
82
+ return kind === "cache" ? path.join(local, "cache") : local;
83
+ };
84
+
85
+ /** @param {NodeJS.ProcessEnv} [env] @param {string} [platform] */
86
+ export const configDir = (env, platform) => workitDir("config", env, platform);
87
+ /** @param {NodeJS.ProcessEnv} [env] @param {string} [platform] */
88
+ export const stateDir = (env, platform) => workitDir("state", env, platform);
89
+ /** @param {NodeJS.ProcessEnv} [env] @param {string} [platform] */
90
+ export const dataDir = (env, platform) => workitDir("data", env, platform);
91
+ /** @param {NodeJS.ProcessEnv} [env] @param {string} [platform] */
92
+ export const cacheDir = (env, platform) => workitDir("cache", env, platform);
93
+ /** @param {NodeJS.ProcessEnv} [env] @param {string} [platform] */
94
+ export const runtimeDir = (env, platform) => workitDir("runtime", env, platform);
95
+
96
+ /**
97
+ * Where Workit keeps the worktrees it manages: `<state>/worktrees/<repo>/<slug>`.
98
+ * State, not cache: a worktree holds uncommitted work.
99
+ * @param {NodeJS.ProcessEnv} [env] @param {string} [platform]
100
+ */
101
+ export const worktreesDir = (env, platform) => path.join(stateDir(env, platform), "worktrees");
102
+
103
+ /**
104
+ * The managed worktree root before it moved to worktreesDir:
105
+ * `$XDG_CACHE_HOME/workit-wt`, else `~/.cache/workit-wt` (on every platform).
106
+ * Never moved (a session may be using a worktree there); cleanup still sweeps it.
107
+ * @param {NodeJS.ProcessEnv} [env]
108
+ */
109
+ export const legacyWorktreesDir = (env = process.env) =>
110
+ path.join(xdgBase("cache", env) || path.join(userHome(env), ".cache"), "workit-wt");
111
+
112
+ /**
113
+ * The POSIX shell form of stateDir, embedded verbatim by the shell launchers
114
+ * (claude-code bin/workit, the owned launcher `workit upgrade` writes).
115
+ */
116
+ export const STATE_DIR_SH = `workit_state_dir() {
117
+ if [ -n "\${WORKIT_STATE_DIR:-}" ]; then printf '%s\\n' "$WORKIT_STATE_DIR"; return; fi
118
+ case "\${XDG_STATE_HOME:-}" in /*) printf '%s\\n' "$XDG_STATE_HOME/workit"; return ;; esac
119
+ case "$(uname -s 2>/dev/null)" in
120
+ MINGW*|MSYS*|CYGWIN*) printf '%s\\n' "\${LOCALAPPDATA:-$HOME/AppData/Local}/workit" ;;
121
+ *) printf '%s\\n' "$HOME/.local/state/workit" ;;
122
+ esac
123
+ }`;
124
+
125
+ /** The POSIX shell form of configDir, embedded verbatim by scripts/lib/config-dir.sh. */
126
+ export const CONFIG_DIR_SH = `workit_config_dir() {
127
+ if [ -n "\${WORKIT_CONFIG:-}" ]; then printf '%s\\n' "$WORKIT_CONFIG"; return; fi
128
+ if [ -n "\${WORKIT_CONFIG_DIR:-}" ]; then printf '%s\\n' "$WORKIT_CONFIG_DIR"; return; fi
129
+ case "\${XDG_CONFIG_HOME:-}" in /*) printf '%s\\n' "$XDG_CONFIG_HOME/workit"; return ;; esac
130
+ case "$(uname -s 2>/dev/null)" in
131
+ MINGW*|MSYS*|CYGWIN*) printf '%s\\n' "\${APPDATA:-$HOME/AppData/Roaming}/workit" ;;
132
+ *) printf '%s\\n' "$HOME/.config/workit" ;;
133
+ esac
134
+ }`;
135
+
136
+ /** The POSIX shell form of dataDir, embedded verbatim by scripts/lib/config-dir.sh. */
137
+ export const DATA_DIR_SH = `workit_data_dir() {
138
+ case "\${XDG_DATA_HOME:-}" in /*) printf '%s\\n' "$XDG_DATA_HOME/workit"; return ;; esac
139
+ case "$(uname -s 2>/dev/null)" in
140
+ MINGW*|MSYS*|CYGWIN*) printf '%s\\n' "\${LOCALAPPDATA:-$HOME/AppData/Local}/workit" ;;
141
+ *) printf '%s\\n' "$HOME/.local/share/workit" ;;
142
+ esac
143
+ }`;
144
+
145
+ /**
146
+ * The POSIX shell form of runtimeDir, embedded verbatim by scripts/lib/config-dir.sh.
147
+ * `os.tmpdir()` drops a trailing slash from TMPDIR; so does this.
148
+ */
149
+ export const RUNTIME_DIR_SH = `workit_runtime_dir() {
150
+ case "\${XDG_RUNTIME_DIR:-}" in /*) printf '%s\\n' "$XDG_RUNTIME_DIR/workit"; return ;; esac
151
+ case "$(uname -s 2>/dev/null)" in
152
+ MINGW*|MSYS*|CYGWIN*) printf '%s\\n' "\${TEMP:-\${TMP:-/tmp}}/workit" ;;
153
+ *) set -- "\${TMPDIR:-/tmp}"; printf '%s\\n' "\${1%/}/workit-$(id -u)" ;;
154
+ esac
155
+ }`;
156
+
157
+ /**
158
+ * Locations older Workit versions used, per kind, that migrateLegacyDirs moves
159
+ * to the resolver's default: macOS state lived in ~/Library/Application
160
+ * Support/workit, Windows config in ~\.config\workit. Only while the kind has
161
+ * no override or XDG variable (then the default is in use).
162
+ * @param {NodeJS.ProcessEnv} [env] @param {string} [platform]
163
+ * @returns {Array<{ kind: "config" | "state", legacy: string, target: string }>}
164
+ */
165
+ export const legacyDirs = (env = process.env, platform = process.platform) => {
166
+ const home = userHome(env);
167
+ /** @type {Array<{ kind: "config" | "state", legacy: string }>} */
168
+ const old = [];
169
+ if (platform === "darwin")
170
+ old.push({
171
+ kind: "state",
172
+ legacy: path.join(home, "Library", "Application Support", "workit"),
173
+ });
174
+ if (platform === "win32")
175
+ old.push({ kind: "config", legacy: path.join(home, ".config", "workit") });
176
+ return old
177
+ .filter(
178
+ (entry) =>
179
+ !WORKIT_DIR_OVERRIDES[entry.kind].some((name) => env[name]) && !xdgBase(entry.kind, env),
180
+ )
181
+ .map((entry) => ({
182
+ kind: entry.kind,
183
+ legacy: entry.legacy,
184
+ target: workitDir(entry.kind, env, platform),
185
+ }));
186
+ };
187
+
188
+ /** The marker a move leaves beside the old location: `<legacy>.moved-<stamp>`. */
189
+ export const LEGACY_MARKER_INFIX = ".moved-";
190
+
191
+ /**
192
+ * A real directory, never a symlink to one: a migration must not move a link
193
+ * the user made (the data behind it would stay where the link pointed).
194
+ * @param {string} file
195
+ */
196
+ export const isRealDir = (file) => {
197
+ try {
198
+ return lstatSync(file).isDirectory();
199
+ } catch {
200
+ return false;
201
+ }
202
+ };
203
+
204
+ /**
205
+ * One-time move of each legacy location whose new location does not exist yet.
206
+ * Leaves `<legacy>.moved-<UTC stamp>` naming the new location; a second run
207
+ * finds no legacy directory and does nothing. Never throws: a failed move
208
+ * leaves both places as they were, and `workit doctor` (legacy_dirs) names the
209
+ * command that finishes it.
210
+ * @param {{ env?: NodeJS.ProcessEnv, platform?: string, now?: Date }} [options]
211
+ * @returns {Array<{ kind: string, legacy: string, target: string, marker: string }>}
212
+ */
213
+ export const migrateLegacyDirs = (options = {}) => {
214
+ const env = options.env || process.env;
215
+ const platform = options.platform || process.platform;
216
+ const stamp = (options.now || new Date())
217
+ .toISOString()
218
+ .replace(/[-:]/g, "")
219
+ .replace(/\..*$/, "Z");
220
+ /** @type {Array<{ kind: string, legacy: string, target: string, marker: string }>} */
221
+ const moved = [];
222
+ for (const entry of legacyDirs(env, platform)) {
223
+ if (!isRealDir(entry.legacy) || existsSync(entry.target)) continue;
224
+ const marker = `${entry.legacy}${LEGACY_MARKER_INFIX}${stamp}`;
225
+ try {
226
+ mkdirSync(path.dirname(entry.target), { recursive: true });
227
+ renameSync(entry.legacy, entry.target);
228
+ } catch {
229
+ continue;
230
+ }
231
+ try {
232
+ writeFileSync(
233
+ marker,
234
+ `Workit moved this ${entry.kind} directory to ${entry.target} at ${stamp}.\n`,
235
+ );
236
+ } catch {
237
+ // the move is done; a missing marker only loses the note
238
+ }
239
+ moved.push({ kind: entry.kind, legacy: entry.legacy, target: entry.target, marker });
240
+ }
241
+ return moved;
242
+ };
package/mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "workit": {
4
+ "command": "npx",
5
+ "args": ["-y", "--prefer-online", "--min-release-age=0", "--package=@workit-dev/cursor@latest", "workit-cursor-mcp", "${workspaceFolder}"]
6
+ }
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,10 +1,58 @@
1
1
  {
2
2
  "name": "@workit-dev/cursor",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @workit-dev/cursor",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Workit Cursor plugin — shared MCP transport, native hooks, and sixteen method skills",
5
6
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
7
+ "agentic-coding",
8
+ "ai-agents",
9
+ "cursor",
10
+ "mcp",
11
+ "model-context-protocol",
12
+ "plugin",
13
+ "workflow"
14
+ ],
15
+ "homepage": "https://github.com/BrainerVirus/workit#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/BrainerVirus/workit/issues"
18
+ },
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/BrainerVirus/workit.git",
23
+ "directory": "packages/workit-cursor"
24
+ },
25
+ "bin": {
26
+ "workit-cursor-hook": "./dist/workit-hook.js",
27
+ "workit-cursor-mcp": "./dist/mcp-server.js",
28
+ "workit-cursor-session-start": "./dist/cursor-session-start.js"
29
+ },
30
+ "files": [
31
+ "dist/",
32
+ "assets/",
33
+ "mcp.json",
34
+ ".cursor-plugin/",
35
+ "rules/workit-contract.mdc",
36
+ "skills/",
37
+ "commands/",
38
+ "hooks/hooks-cursor.json",
39
+ "hooks/launch.mjs",
40
+ "hooks/launch-runtime.mjs",
41
+ "hooks/node-resolve.mjs",
42
+ "hooks/workit-paths.mjs"
43
+ ],
44
+ "type": "module",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "bun scripts/build.ts"
50
+ },
51
+ "devDependencies": {
52
+ "@workit-dev/core": "^1.0.0",
53
+ "@workit-dev/mcp": "^1.0.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=24"
57
+ }
10
58
  }
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: Workit coordination boundaries and truthful Cursor capabilities
3
+ alwaysApply: true
4
+ ---
5
+
6
+ Workit is an optional coordinator, not a permission system. Ordinary questions,
7
+ investigation, and reversible edits need no Workit task, assessment,
8
+ reviewer, or close call; follow Cursor's native permissions. When a step has one
9
+ right answer, run the `workit` CLI verb for it (`workit check`, `workit git …`,
10
+ `workit pr …`, `workit ledger …`). Use one compact
11
+ Workit record when handoff, dependent steps, concurrent actors, or important
12
+ decisions make durable continuity useful. Do not duplicate a shared task per
13
+ host or merge distinct intents because their titles look similar.
14
+
15
+ Workit policy may narrow an action, but it never grants host access or overrides
16
+ a host deny. Check the actual target checkout, configured account, branch and
17
+ commit conventions, protected refs, and expected remote state before a delivery
18
+ verb; merge and release need a workspace grant (`workit grant show`). Report unsupported shell coverage honestly.
19
+
20
+ Cursor hooks provide evidence only where their documented input and blocking
21
+ behavior support it. AskQuestion answers are policy-only; `sessionStart` and
22
+ `preCompact` cannot block; `subagentStop` has no stable child identity; arbitrary
23
+ shell writes and Tab edits are unavailable. Never invent a caller
24
+ identity or cross-process caller channel. For native subagents in tracked
25
+ Workit work, begin the task payload with exactly one assigned role marker:
26
+ `[workit-role: implementer]`, `[workit-role: reviewer]`, or
27
+ `[workit-role: investigator]`.
28
+
29
+ Cursor cannot attribute subagent writes, so Workit never enforces a
30
+ subagent's write scope here. Fanout writers (workit-fanout) are agent-guided:
31
+ each runs in its own git worktree with a fixed brief, and its file scope is
32
+ checked at fan-in with `git diff --name-only <base>...<branch>`, not enforced
33
+ while it works. Verifiers and reviewers stay read-only and record
34
+ `workit ledger verdict` under a session the lead assigns
35
+ (`WORKIT_SESSION_ID=<lead>:verifier-<random hex>` in the brief, a fresh
36
+ suffix per verifier: Cursor's shell carries no session id, so this declared
37
+ child id is the verifier's session), never the
38
+ author's.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: workit-architecture
3
+ description: Rank cited deepening refactors from churn and ledger friction; never applied unapproved. Use for architecture.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Architecture: deepen the modules that cause friction
8
+
9
+ User-invoked: offer it, never start it yourself. It proposes and grills; it
10
+ never refactors without the user's approval. Use the words in
11
+ `references/vocabulary.md` exactly and the domain words in `GLOSSARY.md`.
12
+
13
+ ## Deepening
14
+
15
+ 1. **Scope by YAGNI.** A direction the user names wins. Otherwise find hot
16
+ spots: churn (`git log --since=6.months --name-only --format=`, counted per
17
+ file, skipping lockfiles, version-bump manifests, generated files, docs and
18
+ `chore(release)` commits), friction in `workit ledger list --last 200`
19
+ (failed checks and verdicts, rulings, `retro:` decisions) and
20
+ `workit test-audit <paths> --json`. State the window and paths in one line.
21
+ 2. **Settled decisions stand.** Read `GLOSSARY.md`, `CODING_STANDARDS.md` and
22
+ ADRs when present. Never re-litigate an ADR without new evidence.
23
+ 3. **Explore organically.** A read-only explorer (Claude Code: Explore; or
24
+ you) walks the hot spots under the limits in `references/vocabulary.md`
25
+ and returns cited friction. Run the deletion test on every suspect.
26
+ 4. **Report 3-5 ranked candidates, then stop and wait for approval.** Each:
27
+ - files and modules;
28
+ - the friction, with **2 or more cited occurrences** (commit, ledger row,
29
+ PR, test-audit finding); a one-off is not a candidate;
30
+ - an interface sketch: what callers see, what moves behind the seam;
31
+ - what gets simpler and what becomes testable through that interface;
32
+ - **merge danger**: one-way or two-way door, plus blast radius (callers,
33
+ packages, public surface).
34
+ The text list is primary; offer an HTML report only if the host renders one.
35
+ 5. **Grill the chosen one**: numbered questions, each with a recommended
36
+ answer; challenge a weak premise with evidence first. Settle the interface,
37
+ the seam, the tests to replace and the migration order; a module named for
38
+ a new concept gets its glossary entry (read the `workit-shape` skill's SKILL.md and follow it).
39
+ 6. **Hand off on approval.** Several slices go into a plan for
40
+ `workit fanout plan <plan.json>`, with scopes and dependencies
41
+ (read the `workit-fanout` skill's SKILL.md and follow it). One slice goes to a build (read the `workit-implement` skill's SKILL.md and follow it). Record the
42
+ choice with `workit ledger decision "architecture: <id>" --why "<why>"`;
43
+ offer an ADR for a lasting rejection so the next run does not repeat it.
44
+
45
+ ## Instruction files
46
+
47
+ Restructure AGENTS.md or CLAUDE.md in three escalating passes, **each its own
48
+ commit in one PR**, so the user can drop the later ones: remove no-ops;
49
+ progressive disclosure into topic files; coding rules to `CODING_STANDARDS.md`
50
+ and mechanical rules to checks. No commit adds a `workit knowledge lint`
51
+ finding. Never scaffold an empty file (`references/instruction-files.md`).
52
+
53
+ ## Test sweep
54
+
55
+ Run `workit test-audit <paths> --json` over the suite or one hot spot and
56
+ report findings by module, worst first. On approval, the bad tests are
57
+ replaced (read the `workit-test-audit` skill's SKILL.md and follow it).
58
+
59
+ ## Example
60
+
61
+ Bad: "The CLI is messy, so I split router.ts in five." Uncited, unapproved.
62
+
63
+ Good: "1. Check runner (check.ts, evidence.ts), 3 occurrences: commits a1b2c3
64
+ and d4e5f6 edit both for one fix; ledger row 41 is a `check` red on a hidden
65
+ timeout. Sketch: `runCheck(spec)` returns evidence, the timeout behind it, so
66
+ timeouts test without a shell. Two-way door, 2 callers in one package."
67
+
68
+ ## Check
69
+
70
+ ```sh
71
+ workit knowledge lint # no new finding after each instruction-files commit
72
+ ```