@workit-dev/claude-code 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 (54) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/README.md +35 -31
  3. package/agents/implementer.md +56 -0
  4. package/agents/reviewer.md +33 -0
  5. package/agents/verifier.md +40 -0
  6. package/assets/templates/headers.md +3 -0
  7. package/assets/templates/hygiene/.editorconfig +8 -0
  8. package/assets/templates/hygiene/.gitattributes +3 -0
  9. package/assets/templates/hygiene/CHANGELOG.md +14 -0
  10. package/assets/templates/hygiene/CONTRIBUTING.md +3 -0
  11. package/assets/templates/hygiene/LICENSE +21 -0
  12. package/assets/templates/hygiene/README.md +3 -0
  13. package/assets/templates/issue-update.md +5 -0
  14. package/assets/templates/plan-template.md +26 -0
  15. package/assets/templates/spec-template.md +64 -0
  16. package/assets/templates/workit-contract.md +13 -0
  17. package/bin/node-resolve.mjs +249 -0
  18. package/bin/workit +82 -0
  19. package/bin/workit-hook.mjs +552 -0
  20. package/bin/workit-paths.mjs +242 -0
  21. package/dist/workit-hook.js +248 -0
  22. package/dist/workit.js +122606 -0
  23. package/hooks/hooks.json +147 -0
  24. package/package.json +45 -6
  25. package/skills/architecture/SKILL.md +72 -0
  26. package/skills/architecture/agents/openai.yaml +3 -0
  27. package/skills/architecture/references/instruction-files.md +52 -0
  28. package/skills/architecture/references/vocabulary.md +53 -0
  29. package/skills/bdd/SKILL.md +53 -0
  30. package/skills/continue/SKILL.md +54 -0
  31. package/skills/debug/SKILL.md +45 -0
  32. package/skills/deslop/SKILL.md +58 -0
  33. package/skills/fanout/SKILL.md +71 -0
  34. package/skills/fanout/references/brief.md +141 -0
  35. package/skills/fanout/references/orchestration.md +169 -0
  36. package/skills/implement/SKILL.md +71 -0
  37. package/skills/retro/SKILL.md +72 -0
  38. package/skills/retro/agents/openai.yaml +3 -0
  39. package/skills/retro/references/sources.md +35 -0
  40. package/skills/retro/references/steering.md +39 -0
  41. package/skills/review/SKILL.md +66 -0
  42. package/skills/review/references/impact.md +24 -0
  43. package/skills/shape/SKILL.md +71 -0
  44. package/skills/shape/references/diagrams.md +17 -0
  45. package/skills/shape/references/knowledge.md +61 -0
  46. package/skills/shape/references/mockups.md +15 -0
  47. package/skills/shape/references/slicing.md +44 -0
  48. package/skills/ship/SKILL.md +71 -0
  49. package/skills/ship/references/changelog.md +44 -0
  50. package/skills/ship/references/pr-body.md +32 -0
  51. package/skills/ship/references/release.md +39 -0
  52. package/skills/test-audit/SKILL.md +54 -0
  53. package/skills/verify-app/SKILL.md +68 -0
  54. package/skills/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
+ };
package/bin/workit ADDED
@@ -0,0 +1,82 @@
1
+ #!/bin/sh
2
+ # `workit` on the Claude Code Bash tool's PATH (Claude adds a plugin's bin/).
3
+ # Local pin: run the CLI from the monorepo sources with bun. Installed
4
+ # package: run the bundled dist/workit.js with node.
5
+ # WORKIT_CLAUDE_RUNTIME=source|dist forces one; WORKIT_SHIM_TRACE=1 prints
6
+ # the resolved entry on stderr.
7
+ # When the PATH node is below the floor (24), the dist runs on a Node that is:
8
+ # $WORKIT_NODE, the node `workit setup` recorded in <state dir>/node.json, then
9
+ # the highest fnm/nvm/volta/mise/asdf install (same order as hooks, see
10
+ # bin/node-resolve.mjs; a test holds the floor equal).
11
+ set -e
12
+ # The state dir as core/workit-paths.mjs resolves it (STATE_DIR_SH; a test holds it verbatim).
13
+ workit_state_dir() {
14
+ if [ -n "${WORKIT_STATE_DIR:-}" ]; then printf '%s\n' "$WORKIT_STATE_DIR"; return; fi
15
+ case "${XDG_STATE_HOME:-}" in /*) printf '%s\n' "$XDG_STATE_HOME/workit"; return ;; esac
16
+ case "$(uname -s 2>/dev/null)" in
17
+ MINGW*|MSYS*|CYGWIN*) printf '%s\n' "${LOCALAPPDATA:-$HOME/AppData/Local}/workit" ;;
18
+ *) printf '%s\n' "$HOME/.local/state/workit" ;;
19
+ esac
20
+ }
21
+ here=$(dirname "$0")
22
+ root=$(cd "$here/.." && pwd)
23
+ main="$root/../workit-cli/src/main.ts"
24
+ entry=""
25
+ if [ "${WORKIT_CLAUDE_RUNTIME:-}" != "dist" ] && [ -f "$root/../workit-core/src/core.ts" ] && [ -f "$main" ]; then
26
+ entry="$main"
27
+ fi
28
+ if [ "${WORKIT_CLAUDE_RUNTIME:-}" = "source" ] && [ -z "$entry" ]; then
29
+ echo "workit: WORKIT_CLAUDE_RUNTIME=source but no monorepo CLI source next to $root" >&2
30
+ exit 1
31
+ fi
32
+ if [ -n "$entry" ]; then
33
+ [ "${WORKIT_SHIM_TRACE:-}" = "1" ] && echo "workit-shim: source $entry" >&2
34
+ exec bun "$entry" "$@"
35
+ fi
36
+ node_floor=24
37
+ node_bin=node
38
+ node_major=$(node -v 2>/dev/null | sed 's/^v//; s/\..*$//')
39
+ case "$node_major" in
40
+ ''|*[!0-9]*) node_major=0 ;;
41
+ esac
42
+ if [ "$node_major" -lt "$node_floor" ]; then
43
+ found=""
44
+ if [ -n "${WORKIT_NODE:-}" ] && [ -x "$WORKIT_NODE" ]; then
45
+ found="$WORKIT_NODE"
46
+ fi
47
+ if [ -z "$found" ]; then
48
+ state=$(workit_state_dir)
49
+ recorded=$(sed -n 's/.*"path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$state/node.json" 2>/dev/null | head -n 1)
50
+ rv=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"v\{0,1\}\([0-9]*\).*/\1/p' "$state/node.json" 2>/dev/null | head -n 1)
51
+ if [ -n "$recorded" ] && [ -x "$recorded" ] && [ "${rv:-0}" -ge "$node_floor" ]; then
52
+ found="$recorded"
53
+ fi
54
+ fi
55
+ if [ -z "$found" ]; then
56
+ candidates=""
57
+ for spec in \
58
+ "${FNM_DIR:-$HOME/.local/share/fnm}/node-versions|installation/bin/node" \
59
+ "$HOME/Library/Application Support/fnm/node-versions|installation/bin/node" \
60
+ "${NVM_DIR:-$HOME/.nvm}/versions/node|bin/node" \
61
+ "${VOLTA_HOME:-$HOME/.volta}/tools/image/node|bin/node" \
62
+ "${MISE_DATA_DIR:-$HOME/.local/share/mise}/installs/node|bin/node" \
63
+ "${ASDF_DATA_DIR:-$HOME/.asdf}/installs/nodejs|bin/node"; do
64
+ base=${spec%%|*}
65
+ rel=${spec#*|}
66
+ for dir in "$base"/*; do
67
+ name=${dir##*/}
68
+ v=${name#v}
69
+ major=${v%%.*}
70
+ case "$major" in ''|*[!0-9]*) continue ;; esac
71
+ if [ "$major" -ge "$node_floor" ] && [ -x "$dir/$rel" ]; then
72
+ candidates="$candidates
73
+ $v $dir/$rel"
74
+ fi
75
+ done
76
+ done
77
+ found=$(printf '%s\n' "$candidates" | sed '/^$/d' | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1 | sed 's/^[^ ]* //')
78
+ fi
79
+ if [ -n "$found" ]; then node_bin="$found"; fi
80
+ fi
81
+ [ "${WORKIT_SHIM_TRACE:-}" = "1" ] && echo "workit-shim: dist $root/dist/workit.js (node $node_bin)" >&2
82
+ exec "$node_bin" "$root/dist/workit.js" "$@"