@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.
- package/.cursor-plugin/plugin.json +38 -0
- package/README.md +56 -43
- package/assets/logo.svg +4 -0
- package/assets/templates/workit-contract.md +13 -0
- package/commands/wk-architecture.md +5 -0
- package/commands/wk-bdd.md +5 -0
- package/commands/wk-continue.md +5 -0
- package/commands/wk-debug.md +5 -0
- package/commands/wk-deslop.md +5 -0
- package/commands/wk-fanout.md +5 -0
- package/commands/wk-implement.md +5 -0
- package/commands/wk-retro.md +5 -0
- package/commands/wk-review.md +5 -0
- package/commands/wk-shape.md +5 -0
- package/commands/wk-ship.md +5 -0
- package/commands/wk-test-audit.md +5 -0
- package/commands/wk-verify-app.md +5 -0
- package/dist/cursor-session-start.js +27182 -0
- package/dist/mcp-server.js +30655 -0
- package/dist/workit-hook.js +24510 -0
- package/hooks/hooks-cursor.json +48 -0
- package/hooks/launch-runtime.mjs +276 -0
- package/hooks/launch.mjs +137 -0
- package/hooks/node-resolve.mjs +249 -0
- package/hooks/workit-paths.mjs +242 -0
- package/mcp.json +8 -0
- package/package.json +54 -6
- package/rules/workit-contract.mdc +38 -0
- package/skills/workit-architecture/SKILL.md +72 -0
- package/skills/workit-architecture/agents/openai.yaml +3 -0
- package/skills/workit-architecture/references/instruction-files.md +52 -0
- package/skills/workit-architecture/references/vocabulary.md +53 -0
- package/skills/workit-bdd/SKILL.md +53 -0
- package/skills/workit-continue/SKILL.md +54 -0
- package/skills/workit-debug/SKILL.md +45 -0
- package/skills/workit-deslop/SKILL.md +58 -0
- package/skills/workit-fanout/SKILL.md +71 -0
- package/skills/workit-fanout/references/brief.md +141 -0
- package/skills/workit-fanout/references/orchestration.md +169 -0
- package/skills/workit-implement/SKILL.md +71 -0
- package/skills/workit-retro/SKILL.md +72 -0
- package/skills/workit-retro/agents/openai.yaml +3 -0
- package/skills/workit-retro/references/sources.md +35 -0
- package/skills/workit-retro/references/steering.md +39 -0
- package/skills/workit-review/SKILL.md +66 -0
- package/skills/workit-review/references/impact.md +24 -0
- package/skills/workit-shape/SKILL.md +71 -0
- package/skills/workit-shape/references/diagrams.md +17 -0
- package/skills/workit-shape/references/knowledge.md +61 -0
- package/skills/workit-shape/references/mockups.md +15 -0
- package/skills/workit-shape/references/slicing.md +44 -0
- package/skills/workit-ship/SKILL.md +71 -0
- package/skills/workit-ship/references/changelog.md +44 -0
- package/skills/workit-ship/references/pr-body.md +32 -0
- package/skills/workit-ship/references/release.md +39 -0
- package/skills/workit-test-audit/SKILL.md +54 -0
- package/skills/workit-verify-app/SKILL.md +68 -0
- 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
package/package.json
CHANGED
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workit-dev/cursor",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"
|
|
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
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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
|
+
```
|