@workit-dev/core 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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -43
  3. package/package.json +56 -6
  4. package/schemas/config.json +74 -0
  5. package/schemas/vcs.json +79 -0
  6. package/schemas/workspaces.json +1029 -0
  7. package/schemas/youtrack.json +84 -0
  8. package/scripts/analyze-release-scope.ts +215 -0
  9. package/scripts/bundled-deps.json +131 -0
  10. package/scripts/fixtures/sample-plan.md +15 -0
  11. package/scripts/fixtures/sample-sdd/progress.md +2 -0
  12. package/scripts/install-codex-plugin.sh +28 -0
  13. package/scripts/install-cursor-plugin.sh +146 -0
  14. package/scripts/install-opencode-plugin.sh +79 -0
  15. package/scripts/install-pi-package.sh +56 -0
  16. package/scripts/lib/config-dir.sh +28 -0
  17. package/scripts/publish-changed-packages.ts +395 -0
  18. package/scripts/rewrite-workspace-deps.ts +53 -0
  19. package/scripts/run-cursor-mcp.sh +6 -0
  20. package/scripts/sync-release-manifests.ts +82 -0
  21. package/scripts/sync-runtime.sh +142 -0
  22. package/scripts/validate-cursor-marketplace.ts +171 -0
  23. package/scripts/verify-bundle-sources.ts +134 -0
  24. package/scripts/write-schemas.ts +16 -0
  25. package/skills/workit-architecture/SKILL.md +72 -0
  26. package/skills/workit-architecture/agents/openai.yaml +3 -0
  27. package/skills/workit-architecture/references/instruction-files.md +52 -0
  28. package/skills/workit-architecture/references/vocabulary.md +53 -0
  29. package/skills/workit-bdd/SKILL.md +53 -0
  30. package/skills/workit-continue/SKILL.md +54 -0
  31. package/skills/workit-debug/SKILL.md +45 -0
  32. package/skills/workit-deslop/SKILL.md +58 -0
  33. package/skills/workit-fanout/SKILL.md +71 -0
  34. package/skills/workit-fanout/references/brief.md +141 -0
  35. package/skills/workit-fanout/references/orchestration.md +169 -0
  36. package/skills/workit-implement/SKILL.md +71 -0
  37. package/skills/workit-retro/SKILL.md +72 -0
  38. package/skills/workit-retro/agents/openai.yaml +3 -0
  39. package/skills/workit-retro/references/sources.md +35 -0
  40. package/skills/workit-retro/references/steering.md +39 -0
  41. package/skills/workit-review/SKILL.md +66 -0
  42. package/skills/workit-review/references/impact.md +24 -0
  43. package/skills/workit-shape/SKILL.md +71 -0
  44. package/skills/workit-shape/references/diagrams.md +17 -0
  45. package/skills/workit-shape/references/knowledge.md +61 -0
  46. package/skills/workit-shape/references/mockups.md +15 -0
  47. package/skills/workit-shape/references/slicing.md +44 -0
  48. package/skills/workit-ship/SKILL.md +71 -0
  49. package/skills/workit-ship/references/changelog.md +44 -0
  50. package/skills/workit-ship/references/pr-body.md +32 -0
  51. package/skills/workit-ship/references/release.md +39 -0
  52. package/skills/workit-test-audit/SKILL.md +54 -0
  53. package/skills/workit-verify-app/SKILL.md +68 -0
  54. package/skills/workit-verify-app/references/template.md +51 -0
  55. package/src/autonomy.ts +910 -0
  56. package/src/check-config.ts +613 -0
  57. package/src/checks.ts +534 -0
  58. package/src/ci-waiter.ts +159 -0
  59. package/src/core/boundary.ts +55 -0
  60. package/src/core/branch-policy.ts +112 -0
  61. package/src/core/branch.ts +1192 -0
  62. package/src/core/changelog-gate.ts +349 -0
  63. package/src/core/changelog-merge.ts +214 -0
  64. package/src/core/changelog.ts +392 -0
  65. package/src/core/cleanup.ts +638 -0
  66. package/src/core/cli-locate.ts +173 -0
  67. package/src/core/commit-flavors.ts +65 -0
  68. package/src/core/config-schemas.ts +653 -0
  69. package/src/core/config.ts +351 -0
  70. package/src/core/context-read.ts +144 -0
  71. package/src/core/docs-gate.ts +69 -0
  72. package/src/core/finding-evidence.ts +89 -0
  73. package/src/core/finding-queue.ts +36 -0
  74. package/src/core/git.ts +73 -0
  75. package/src/core/gitignore.ts +52 -0
  76. package/src/core/heavy-eta.ts +170 -0
  77. package/src/core/heavy-lease.ts +290 -0
  78. package/src/core/heavy-rules.ts +373 -0
  79. package/src/core/heavy-scope.ts +131 -0
  80. package/src/core/heavy-words.ts +83 -0
  81. package/src/core/heavy.ts +535 -0
  82. package/src/core/hygiene.ts +149 -0
  83. package/src/core/init.ts +218 -0
  84. package/src/core/live-session.ts +333 -0
  85. package/src/core/logger.ts +310 -0
  86. package/src/core/methods.ts +185 -0
  87. package/src/core/model-fallback.ts +208 -0
  88. package/src/core/node-resolve.mjs +249 -0
  89. package/src/core/operation-input.ts +632 -0
  90. package/src/core/package-root.ts +28 -0
  91. package/src/core/policy/attributes.ts +97 -0
  92. package/src/core/policy/bookkeeping.ts +119 -0
  93. package/src/core/policy/carry.ts +122 -0
  94. package/src/core/policy/check-evidence.ts +115 -0
  95. package/src/core/policy/current-judgment.ts +20 -0
  96. package/src/core/policy/derive.ts +387 -0
  97. package/src/core/policy/judgment.ts +246 -0
  98. package/src/core/policy/lane.ts +562 -0
  99. package/src/core/policy/pr-lane.ts +32 -0
  100. package/src/core/policy/substance.ts +365 -0
  101. package/src/core/policy-resolver.ts +94 -0
  102. package/src/core/pr-create.ts +448 -0
  103. package/src/core/pr-template.ts +162 -0
  104. package/src/core/release-notes.ts +197 -0
  105. package/src/core/release-tracks.ts +779 -0
  106. package/src/core/remotes.ts +436 -0
  107. package/src/core/repo-context.ts +542 -0
  108. package/src/core/repo-paths.ts +29 -0
  109. package/src/core/resources.ts +1299 -0
  110. package/src/core/role-models.ts +603 -0
  111. package/src/core/route-intent.ts +305 -0
  112. package/src/core/rules.ts +65 -0
  113. package/src/core/runtime-identity.ts +63 -0
  114. package/src/core/safe-write.ts +87 -0
  115. package/src/core/scope-glob.ts +56 -0
  116. package/src/core/scripts.ts +6 -0
  117. package/src/core/skill-manifests.ts +173 -0
  118. package/src/core/store-lock.ts +374 -0
  119. package/src/core/support-matrix.ts +24 -0
  120. package/src/core/task-context.ts +247 -0
  121. package/src/core/task-contract.ts +1279 -0
  122. package/src/core/task-delivery.ts +109 -0
  123. package/src/core/task-engine.ts +2248 -0
  124. package/src/core/task-evaluation.ts +1147 -0
  125. package/src/core/task-store.ts +2649 -0
  126. package/src/core/task-view.ts +180 -0
  127. package/src/core/templates.ts +22 -0
  128. package/src/core/tracker-issues.ts +174 -0
  129. package/src/core/vcs-config.ts +540 -0
  130. package/src/core/workers.ts +62 -0
  131. package/src/core/workit-paths.mjs +242 -0
  132. package/src/core/workspace-branch-policy.ts +123 -0
  133. package/src/core/workspaces.ts +1745 -0
  134. package/src/core/youtrack-backend.ts +288 -0
  135. package/src/core/youtrack-tools.ts +260 -0
  136. package/src/core/youtrack-writes.ts +73 -0
  137. package/src/core/youtrack-ytrack.ts +217 -0
  138. package/src/core/youtrack.ts +454 -0
  139. package/src/core/ytrack-skill.ts +297 -0
  140. package/src/core.ts +171 -0
  141. package/src/deslop/lex.ts +271 -0
  142. package/src/deslop/scan.ts +232 -0
  143. package/src/duration.ts +27 -0
  144. package/src/fanout-brief.ts +486 -0
  145. package/src/fanout-check.ts +455 -0
  146. package/src/fanout-landed.ts +370 -0
  147. package/src/fanout-stack.ts +108 -0
  148. package/src/fanout-status.ts +362 -0
  149. package/src/fanout-worktree.ts +893 -0
  150. package/src/fanout.ts +1473 -0
  151. package/src/forge/exec.ts +385 -0
  152. package/src/forge/github.ts +831 -0
  153. package/src/forge/gitlab.ts +906 -0
  154. package/src/forge/issue-forge.ts +209 -0
  155. package/src/forge/issue-link.ts +468 -0
  156. package/src/forge/issue-ops.ts +314 -0
  157. package/src/forge/merge-gate-base-red.ts +72 -0
  158. package/src/forge/pr-body.ts +100 -0
  159. package/src/forge/pr-edit.ts +292 -0
  160. package/src/forge/pr-ops.ts +1109 -0
  161. package/src/forge/pr-status.ts +10 -0
  162. package/src/forge/redact.ts +195 -0
  163. package/src/forge/report.ts +994 -0
  164. package/src/forge/reruns.ts +132 -0
  165. package/src/forge/resolve.ts +509 -0
  166. package/src/forge/types.ts +242 -0
  167. package/src/forge/verify.ts +398 -0
  168. package/src/git/default-base.ts +130 -0
  169. package/src/git/env-redirect.ts +28 -0
  170. package/src/git/facts.ts +122 -0
  171. package/src/git/ops.ts +1823 -0
  172. package/src/git/refs-read.ts +332 -0
  173. package/src/git/release.ts +1020 -0
  174. package/src/git/rev.ts +1175 -0
  175. package/src/git/session-hook.ts +597 -0
  176. package/src/git/stage.ts +63 -0
  177. package/src/hooks/agent-model.ts +149 -0
  178. package/src/hooks/budget-signal.ts +284 -0
  179. package/src/hooks/context.ts +461 -0
  180. package/src/hooks/cwd-normalize.ts +79 -0
  181. package/src/hooks/deadline.ts +228 -0
  182. package/src/hooks/delivery-verbs.ts +182 -0
  183. package/src/hooks/descriptor.ts +124 -0
  184. package/src/hooks/handle.ts +433 -0
  185. package/src/hooks/heavy-gate.ts +99 -0
  186. package/src/hooks/hook-runs.ts +130 -0
  187. package/src/hooks/hosts/claude-code.ts +380 -0
  188. package/src/hooks/hosts/codex.ts +372 -0
  189. package/src/hooks/hosts/cursor.ts +419 -0
  190. package/src/hooks/hosts/fields.ts +98 -0
  191. package/src/hooks/hosts/opencode.ts +79 -0
  192. package/src/hooks/hosts/pi.ts +78 -0
  193. package/src/hooks/index.ts +60 -0
  194. package/src/hooks/model-limit.ts +378 -0
  195. package/src/hooks/model-switch.ts +35 -0
  196. package/src/hooks/policy.ts +29 -0
  197. package/src/hooks/process.ts +48 -0
  198. package/src/hooks/protocol.ts +135 -0
  199. package/src/hooks/raw-git.ts +2289 -0
  200. package/src/hooks/repeat-failure.ts +115 -0
  201. package/src/hooks/run.ts +54 -0
  202. package/src/hooks/session-marker.ts +151 -0
  203. package/src/hooks/shell-words.ts +532 -0
  204. package/src/hooks/skill-nudge.ts +295 -0
  205. package/src/hooks/spawn-gate.ts +72 -0
  206. package/src/hooks/standing-signal.ts +99 -0
  207. package/src/hooks/standing.ts +225 -0
  208. package/src/hooks/steering.ts +74 -0
  209. package/src/hooks/stop.ts +408 -0
  210. package/src/hooks/worktree-confine.ts +415 -0
  211. package/src/hooks/write-gate.ts +1174 -0
  212. package/src/host-session.ts +459 -0
  213. package/src/knowledge.ts +368 -0
  214. package/src/ledger.ts +2913 -0
  215. package/src/stack.ts +2570 -0
  216. package/src/store/event-log.ts +217 -0
  217. package/src/store/notes.ts +29 -0
  218. package/src/store/patch.ts +135 -0
  219. package/src/store/paths.ts +368 -0
  220. package/src/store/reduce.ts +203 -0
  221. package/src/test-audit-mocks.ts +309 -0
  222. package/src/verdict-cover.ts +151 -0
  223. package/src/worktree-deps.ts +142 -0
  224. package/templates/headers.md +3 -0
  225. package/templates/hygiene/.editorconfig +8 -0
  226. package/templates/hygiene/.gitattributes +3 -0
  227. package/templates/hygiene/CHANGELOG.md +14 -0
  228. package/templates/hygiene/CONTRIBUTING.md +3 -0
  229. package/templates/hygiene/LICENSE +21 -0
  230. package/templates/hygiene/README.md +3 -0
  231. package/templates/issue-update.md +5 -0
  232. package/templates/plan-template.md +26 -0
  233. package/templates/spec-template.md +64 -0
  234. package/templates/workit-contract.md +13 -0
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bun
2
+ // Optional manual manifest sync. The release never runs it: since #704
3
+ // semantic-release and rewrite-workspace-deps.ts stamp the version into the
4
+ // manifests of the CI checkout at publish time, and main is not synced, so the
5
+ // committed manifests keep the version of the last manual sync. Run it in any
6
+ // PR to align every tracked manifest with the latest `workit-dev-v*` tag (or a version
7
+ // given as argv[2]); it is idempotent. plugin.test.ts reads
8
+ // SYNC_MANIFEST_PATHS to keep every plugin manifest on this list.
9
+ import { execFileSync } from "node:child_process";
10
+ import { readFileSync, writeFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { RELEASE_TAG, TAG_GLOB } from "./analyze-release-scope";
13
+
14
+ export const SYNC_MANIFEST_PATHS = [
15
+ "package.json",
16
+ "packages/workit-core/package.json",
17
+ "packages/workit-mcp/package.json",
18
+ "packages/workit-cli/package.json",
19
+ "packages/workit-opencode/package.json",
20
+ "packages/workit-cursor/package.json",
21
+ "packages/workit-codex/package.json",
22
+ "packages/workit-pi/package.json",
23
+ "packages/workit-claude-code/package.json",
24
+ // Kept in lockstep with packages/workit-core/package.json by contract test.
25
+ "packages/workit-cursor/.cursor-plugin/plugin.json",
26
+ "packages/workit-codex/.codex-plugin/plugin.json",
27
+ // Claude Code reads the plugin version from here (the marketplace entry
28
+ // carries none), so it must track the published npm version.
29
+ "packages/workit-claude-code/.claude-plugin/plugin.json",
30
+ ];
31
+
32
+ export type ManifestSyncResult = { version: string; changed: string[] };
33
+
34
+ /**
35
+ * Write `version` (a bare semver or a `workit-dev-v` release tag) into every tracked
36
+ * manifest under `root`. Idempotent: manifests already at the target version
37
+ * are left byte-untouched and omitted from `changed`. Throws on a value that
38
+ * is not a plain release version (`latest`, branches, ranges).
39
+ */
40
+ export function syncManifests(root: string, tagOrVersion: string): ManifestSyncResult {
41
+ const value = tagOrVersion.trim();
42
+ const version = RELEASE_TAG.exec(value)?.[1] ?? RELEASE_TAG.exec(`workit-dev-v${value}`)?.[1];
43
+ if (!version) throw new Error(`invalid version tag: ${JSON.stringify(tagOrVersion)}`);
44
+ const changed: string[] = [];
45
+ for (const rel of SYNC_MANIFEST_PATHS) {
46
+ const file = resolve(root, ...rel.split("/"));
47
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
48
+ if (parsed.version === version) continue;
49
+ parsed.version = version;
50
+ writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
51
+ changed.push(rel);
52
+ }
53
+ return { version, changed };
54
+ }
55
+
56
+ /** The newest release tag by descending semver refname order (none throws). */
57
+ function latestReleaseTag(cwd?: string): string {
58
+ const tag = execFileSync("git", ["tag", "--list", TAG_GLOB, "--sort=-v:refname"], {
59
+ cwd,
60
+ encoding: "utf8",
61
+ stdio: ["pipe", "pipe", "pipe"],
62
+ })
63
+ .split("\n")
64
+ .map((line) => line.trim())
65
+ .find((line) => RELEASE_TAG.test(line));
66
+ if (!tag)
67
+ throw new Error(`no ${TAG_GLOB} release tag found — run after the first semantic-release`);
68
+ return tag;
69
+ }
70
+
71
+ if (import.meta.main) {
72
+ const root = process.argv[3]
73
+ ? resolve(process.argv[3])
74
+ : resolve(import.meta.dir, "..", "..", "..");
75
+ const tag = process.argv[2] ?? latestReleaseTag(root);
76
+ const { version, changed } = syncManifests(root, tag);
77
+ console.log(
78
+ changed.length > 0
79
+ ? `synced ${changed.length} manifest(s) to ${version}: ${changed.join(", ")}`
80
+ : `manifests already at ${version}`,
81
+ );
82
+ }
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env bash
2
+ # Sync workit runtime for Cursor + OpenCode.
3
+ # Prefer local monorepo (dev) when present; otherwise git-pull the share clone.
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
7
+ . "$SCRIPT_DIR/lib/config-dir.sh"
8
+
9
+ SHARE="$(workit_data_dir)"
10
+ PLUGIN_DIR="${HOME}/.cursor/plugins/local/workit"
11
+ DEV_DEFAULT="${HOME}/Documents/projects/personal/workit"
12
+ DEV="${WORKIT_DEV:-$DEV_DEFAULT}"
13
+ REPO_SLUG="${WORKIT_REPO:-BrainerVirus/workit}"
14
+ LOCK_DIR="$(workit_runtime_dir)"
15
+ LOCK="$LOCK_DIR/sync.lock"
16
+
17
+ # RR-05: missing required tools, an unacquirable lock, or a failed clone must
18
+ # never look like a successful sync.
19
+ if ! command -v flock >/dev/null 2>&1; then
20
+ echo "FATAL: sync-runtime requires flock (util-linux) — not found in PATH" >&2
21
+ exit 1
22
+ fi
23
+ mkdir -p "$LOCK_DIR"
24
+ exec 9>"$LOCK"
25
+ if ! flock -n 9; then
26
+ echo "sync-runtime: another process holds $LOCK — state unverified, failing" >&2
27
+ exit 1
28
+ fi
29
+
30
+ SRC=""
31
+ if [ -f "${DEV}/packages/workit-opencode/src/plugin.ts" ] && [ -d "${DEV}/packages/workit-cursor/.cursor-plugin" ]; then
32
+ SRC="$DEV"
33
+ elif [ -d "${SHARE}/.git" ]; then
34
+ # RR-05: a failed update of an existing share clone must not look like a
35
+ # successful sync — propagate fetch/pull failures to a FATAL nonzero exit.
36
+ if ! git -C "$SHARE" fetch --quiet origin; then
37
+ echo "FATAL: could not fetch updates for $SHARE" >&2
38
+ exit 1
39
+ fi
40
+ if ! git -C "$SHARE" pull --ff-only --quiet origin main; then
41
+ echo "FATAL: could not update $SHARE from origin/main" >&2
42
+ exit 1
43
+ fi
44
+ SRC="$SHARE"
45
+ elif [ ! -d "${SHARE}/packages/workit-core/src" ]; then
46
+ mkdir -p "$(dirname "$SHARE")"
47
+ if ! git clone --depth 1 "https://github.com/${REPO_SLUG}.git" "$SHARE"; then
48
+ echo "FATAL: could not clone https://github.com/${REPO_SLUG}.git into $SHARE" >&2
49
+ exit 1
50
+ fi
51
+ SRC="$SHARE"
52
+ else
53
+ SRC="$SHARE"
54
+ fi
55
+
56
+ CURSOR_SRC="$SRC/packages/workit-cursor"
57
+ BUN_BIN=""
58
+ if [ -n "${BUN:-}" ]; then
59
+ BUN_BIN="$BUN"
60
+ if [ ! -x "$BUN_BIN" ]; then
61
+ echo "FATAL: BUN is set but unusable: $BUN_BIN" >&2
62
+ exit 1
63
+ fi
64
+ elif [ -x "$HOME/.bun/bin/bun" ]; then
65
+ BUN_BIN="$HOME/.bun/bin/bun"
66
+ elif command -v bun >/dev/null 2>&1; then
67
+ BUN_BIN="$(command -v bun)"
68
+ else
69
+ echo "FATAL: Bun is required to build the Cursor adapter" >&2
70
+ exit 1
71
+ fi
72
+
73
+ for DEP in @workit-dev/core @workit-dev/mcp @modelcontextprotocol/sdk zod; do
74
+ if [ ! -e "$SRC/node_modules/$DEP" ]; then
75
+ if [ ! -f "$SRC/bun.lock" ]; then
76
+ echo "FATAL: dependency install requires $SRC/bun.lock" >&2
77
+ exit 1
78
+ fi
79
+ if ! (cd "$SRC" && "$BUN_BIN" install --frozen-lockfile); then
80
+ echo "FATAL: root dependency install failed in $SRC" >&2
81
+ exit 1
82
+ fi
83
+ break
84
+ fi
85
+ done
86
+
87
+ if ! (cd "$SRC" && PATH="$(dirname "$BUN_BIN"):$PATH" "$BUN_BIN" "$SRC/packages/workit-mcp/scripts/build.ts"); then
88
+ echo "FATAL: shared MCP build failed in $SRC" >&2
89
+ exit 1
90
+ fi
91
+ if ! (cd "$SRC" && PATH="$(dirname "$BUN_BIN"):$PATH" "$BUN_BIN" "$CURSOR_SRC/scripts/build.ts"); then
92
+ echo "FATAL: Cursor adapter build failed in $CURSOR_SRC" >&2
93
+ exit 1
94
+ fi
95
+ for ENTRY in mcp-server.js cursor-session-start.js workit-hook.js; do
96
+ DIST_ENTRY="$CURSOR_SRC/dist/$ENTRY"
97
+ if [ ! -f "$DIST_ENTRY" ] || [ ! -s "$DIST_ENTRY" ] || [ "$(IFS= read -r LINE <"$DIST_ENTRY"; printf '%s' "$LINE")" != '#!/usr/bin/env node' ]; then
98
+ echo "FATAL: Cursor adapter invalid dist entry: $DIST_ENTRY" >&2
99
+ exit 1
100
+ fi
101
+ done
102
+
103
+ if [ "$SRC" != "$SHARE" ]; then
104
+ mkdir -p "$SHARE"
105
+ # Keep .git if share is a clone; never wipe it when syncing from the monorepo
106
+ # versions/, current and bin/ are the owned CLI install (workit upgrade): never delete them
107
+ rsync -a --delete \
108
+ --exclude '/versions' --exclude '/current' --exclude '/bin' \
109
+ --exclude '.git' \
110
+ --exclude 'node_modules' \
111
+ --exclude 'cursor/mcp/node_modules' \
112
+ --exclude '.cache' \
113
+ "$SRC/" "$SHARE/"
114
+ fi
115
+
116
+ # Cursor IDE package (real directory)
117
+ mkdir -p "${HOME}/.cursor/plugins/local"
118
+ mkdir -p "$PLUGIN_DIR"
119
+ rsync -a --delete \
120
+ --exclude 'mcp/node_modules' \
121
+ "$SHARE/packages/workit-cursor/" "$PLUGIN_DIR/"
122
+
123
+ if ! "$BUN_BIN" "$SHARE/packages/workit-core/src/core/skill-manifests.ts" "$PLUGIN_DIR"; then
124
+ echo "FATAL: Cursor skill validation failed: $PLUGIN_DIR" >&2
125
+ exit 1
126
+ fi
127
+ # Canonical user rules -> Cursor .mdc (compiled by the shared core)
128
+ CONFIG_RULES_DIR="$(resolve_config_dir)/rules"
129
+ if [ -d "$CONFIG_RULES_DIR" ]; then
130
+ # rules.ts is loaded only here (scripts/check-reachability.ts allowlists it):
131
+ # a failure must surface, never leave the Cursor rules silently stale.
132
+ if ! "$BUN_BIN" -e "
133
+ const { writeCompiledCursorRules } = await import('${SHARE}/packages/workit-core/src/core/rules.ts');
134
+ writeCompiledCursorRules('${PLUGIN_DIR}/rules');
135
+ " >/dev/null; then
136
+ echo "FATAL: Cursor rule compilation failed: $CONFIG_RULES_DIR -> $PLUGIN_DIR/rules" >&2
137
+ exit 1
138
+ fi
139
+ fi
140
+ printf '%s\n' "$SHARE/packages/workit-core" >"$PLUGIN_DIR/.workit-root"
141
+
142
+ exit 0
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bun
2
+ // Cursor Marketplace validator (Task 9, CA-13/CA-15/CA-17/CA-21): validates the
3
+ // tracked Marketplace artifact against the official Cursor JSON schemas plus the
4
+ // repository-specific invariants that a JSON Schema cannot express. Runs from a
5
+ // clean `git ls-files` checkout — it never reads dist/ or other ignored build
6
+ // output, so `bun run validate:cursor-marketplace` is a truthful clean-checkout
7
+ // gate for CI.
8
+ //
9
+ // Official schema snapshots (test/fixtures/cursor-schemas/) are verbatim copies
10
+ // of the current official schemas, retrieved 2026-08-13 from:
11
+ // - https://raw.githubusercontent.com/cursor/plugins/main/schemas/plugin.schema.json
12
+ // - https://raw.githubusercontent.com/cursor/plugins/main/schemas/marketplace.schema.json
13
+ //
14
+ // `source` path resolution (Task 7 carry-forward, RESOLVED): the official
15
+ // marketplace.schema.json documents `plugins[].source` as "Path to the plugin
16
+ // directory (relative to the marketplace root)". The marketplace root is the
17
+ // directory that CONTAINS `.cursor-plugin/marketplace.json` — the repo root —
18
+ // NOT `.cursor-plugin/` itself. Evidence: the official cursor/plugins repo keeps
19
+ // `.cursor-plugin/marketplace.json` at the repo root and lists sources such as
20
+ // `continual-learning` and `third_party/gmail` with no `../` prefix, even though
21
+ // `.cursor-plugin/` is a sibling of those directories. Therefore
22
+ // `source: "packages/workit-cursor"` is correct and kept verbatim.
23
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
24
+ import path from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import Ajv from "ajv";
27
+ import addFormats from "ajv-formats";
28
+ import { WORKIT_METHOD_SKILLS, validateSkillManifests } from "../src/core/skill-manifests";
29
+
30
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
31
+ const repoRoot = path.resolve(scriptDir, "..", "..", "..");
32
+ const root = process.argv[2] ? path.resolve(process.argv[2]) : repoRoot;
33
+
34
+ const MARKETPLACE_REL = ".cursor-plugin/marketplace.json";
35
+
36
+ const frontmatterKeys = (file: string): Set<string> => {
37
+ const head = readFileSync(file, "utf8").slice(0, 4096);
38
+ const keys = new Set<string>();
39
+ if (!head.startsWith("---")) return keys;
40
+ const end = head.indexOf("\n---", 3);
41
+ if (end === -1) return keys;
42
+ for (const line of head.slice(3, end).split("\n")) {
43
+ const m = /^([A-Za-z0-9_-]+):/.exec(line);
44
+ if (m) keys.add(m[1]);
45
+ }
46
+ return keys;
47
+ };
48
+
49
+ export const validateMarketplace = (rootArg: string): string[] => {
50
+ const errors: string[] = [];
51
+
52
+ if (!existsSync(path.join(rootArg, MARKETPLACE_REL))) {
53
+ errors.push(`missing ${MARKETPLACE_REL}`);
54
+ return errors;
55
+ }
56
+
57
+ // 1. Official JSON Schema evaluation (AJV + ajv-formats), verbatim snapshots.
58
+ const validate = new Ajv({ strict: true, allErrors: true });
59
+ addFormats(validate);
60
+ const schemaDir = path.join(rootArg, "test/fixtures/cursor-schemas");
61
+ const pluginSchema = JSON.parse(readFileSync(path.join(schemaDir, "plugin.schema.json"), "utf8"));
62
+ const marketSchema = JSON.parse(
63
+ readFileSync(path.join(schemaDir, "marketplace.schema.json"), "utf8"),
64
+ );
65
+ const market = JSON.parse(readFileSync(path.join(rootArg, MARKETPLACE_REL), "utf8"));
66
+ if (!validate.validate(marketSchema, market)) {
67
+ errors.push(
68
+ `marketplace.json invalid: ${(validate.errors ?? []).map((e) => e.message).join("; ")}`,
69
+ );
70
+ }
71
+
72
+ // 2. Resolve each plugin source relative to the repo root; name must match.
73
+ for (const entry of (market.plugins ?? []) as { name: string; source: string }[]) {
74
+ const pluginDir = path.join(rootArg, entry.source);
75
+ if (!existsSync(path.join(pluginDir, ".cursor-plugin/plugin.json"))) {
76
+ errors.push(`plugin ${entry.name}: source ${entry.source} has no .cursor-plugin/plugin.json`);
77
+ continue;
78
+ }
79
+ const plugin = JSON.parse(
80
+ readFileSync(path.join(pluginDir, ".cursor-plugin/plugin.json"), "utf8"),
81
+ );
82
+ if (!validate.validate(pluginSchema, plugin)) {
83
+ errors.push(
84
+ `plugin ${entry.name} plugin.json invalid: ${(validate.errors ?? []).map((e) => e.message).join("; ")}`,
85
+ );
86
+ }
87
+ if (plugin.name !== entry.name) {
88
+ errors.push(`plugin ${entry.name}: plugin.json name ${plugin.name} does not match index`);
89
+ }
90
+
91
+ // 3. Component paths resolve inside the plugin root (no `..`, no absolute).
92
+ // `skills`/`rules` are path-like (string or string[]). `mcpServers`/`hooks`
93
+ // may also be inline objects or arrays of objects, which the JSON-schema
94
+ // check already validates — only string/string[] values get the
95
+ // path-escape/existence check; non-string forms are skipped gracefully.
96
+ for (const field of ["skills", "rules", "mcpServers", "hooks"] as const) {
97
+ const value = plugin[field];
98
+ const rels: string[] =
99
+ typeof value === "string"
100
+ ? [value]
101
+ : Array.isArray(value)
102
+ ? value.filter((v): v is string => typeof v === "string")
103
+ : [];
104
+ for (const rel of rels) {
105
+ if (rel.includes("..") || rel.startsWith("/")) {
106
+ errors.push(`plugin ${entry.name}: ${field} path escapes root: ${rel}`);
107
+ continue;
108
+ }
109
+ if (!existsSync(path.join(pluginDir, rel))) {
110
+ errors.push(`plugin ${entry.name}: ${field} path missing: ${rel}`);
111
+ }
112
+ }
113
+ }
114
+
115
+ // 4. Logo resolves inside the plugin root and exists.
116
+ const logo = plugin.logo as string | undefined;
117
+ if (logo && (logo.includes("..") || logo.startsWith("/"))) {
118
+ errors.push(`plugin ${entry.name}: logo path escapes root: ${logo}`);
119
+ } else if (logo && !existsSync(path.join(pluginDir, logo))) {
120
+ errors.push(`plugin ${entry.name}: logo missing: ${logo}`);
121
+ }
122
+
123
+ // 5. Skills and rules carry valid frontmatter.
124
+ const skillRoots: [string, readonly string[]][] = [
125
+ [path.join(pluginDir, "skills"), WORKIT_METHOD_SKILLS],
126
+ ];
127
+ for (const [dir, expected] of skillRoots) {
128
+ const mismatch = validateSkillManifests(dir, expected, "skills");
129
+ if (mismatch) errors.push(`plugin ${entry.name}: ${mismatch}`);
130
+ for (const skill of expected) {
131
+ const keys = frontmatterKeys(path.join(dir, skill, "SKILL.md"));
132
+ if (!keys.has("name"))
133
+ errors.push(`plugin ${entry.name}: ${skill}/SKILL.md missing frontmatter name`);
134
+ if (!keys.has("description"))
135
+ errors.push(`plugin ${entry.name}: ${skill}/SKILL.md missing frontmatter description`);
136
+ }
137
+ }
138
+ const rulesDir = path.join(pluginDir, "rules");
139
+ if (existsSync(rulesDir)) {
140
+ for (const rule of readdirSync(rulesDir).filter((f) => f.endsWith(".mdc"))) {
141
+ const keys = frontmatterKeys(path.join(rulesDir, rule));
142
+ if (!keys.has("description"))
143
+ errors.push(`plugin ${entry.name}: ${rule} missing frontmatter description`);
144
+ }
145
+ }
146
+
147
+ // 6. No active runtime path targets an ignored dist file.
148
+ for (const rel of ["mcp.json", "hooks/hooks-cursor.json"]) {
149
+ const p = path.join(pluginDir, rel);
150
+ if (!existsSync(p)) continue;
151
+ const raw = readFileSync(p, "utf8");
152
+ if (raw.includes("dist/") || raw.includes("run-server") || raw.includes("session-start.js")) {
153
+ errors.push(`plugin ${entry.name}: ${rel} references ignored runtime output`);
154
+ }
155
+ }
156
+ }
157
+
158
+ return errors;
159
+ };
160
+
161
+ if (import.meta.main) {
162
+ const errors = validateMarketplace(root);
163
+ if (errors.length > 0) {
164
+ for (const e of errors) process.stderr.write(`${e}\n`);
165
+ process.stderr.write(
166
+ `marketplace validation FAILED (${errors.length} error${errors.length === 1 ? "" : "s"})\n`,
167
+ );
168
+ process.exit(1);
169
+ }
170
+ console.log(`marketplace validation passed (root: ${root})`);
171
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bun
2
+ // Release guard: every adapter dist/ must inline the workspace's own
3
+ // @workit-dev sources, never a registry copy. bun build resolves the nearest
4
+ // node_modules first, so an installed packages/<pkg>/node_modules/@workit-dev
5
+ // copy silently replaces the tagged source (8.0.0 shipped 7.7.0's core that
6
+ // way). Two checks:
7
+ // - installs: no packages/*/node_modules/@workit-dev/* entry, and every root
8
+ // node_modules/@workit-dev/* entry resolves inside packages/ (run before
9
+ // the build; it also covers minified bundles, which carry no path headers);
10
+ // - bundles: bun prefixes each inlined module with a `// <path>` header; a
11
+ // header under node_modules/@workit-dev/ names a stale copy, and every
12
+ // non-minified entry (with its split chunks) must inline core from
13
+ // packages/workit-core/src/.
14
+ // `--installs-only` runs only the first check (prepareCmd, before the build).
15
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
16
+ import { dirname, join, relative, resolve, sep } from "node:path";
17
+ import { RELEASE_PACKAGES } from "./analyze-release-scope";
18
+
19
+ // Separator-agnostic: a Windows build may write `\` in module headers.
20
+ const REGISTRY_COPY = /^\/\/ (.*node_modules[\\/]@workit-dev[\\/].+)$/gmu;
21
+ const WORKSPACE_CORE = /^\/\/ packages[\\/]workit-core[\\/]src[\\/]/mu;
22
+ /** bun's unminified output averages well under 100 bytes a line; --minify is thousands. */
23
+ const MINIFIED_BYTES_PER_LINE = 1000;
24
+
25
+ const entries = (dir: string): string[] => (existsSync(dir) ? readdirSync(dir) : []);
26
+ /** `root`-relative path with `/` separators, for messages on every OS. */
27
+ const rel = (root: string, file: string): string => relative(root, file).split(sep).join("/");
28
+ /** Windows paths compare case-insensitively. */
29
+ const fold = (file: string): string => (process.platform === "win32" ? file.toLowerCase() : file);
30
+
31
+ /** Installed @workit-dev packages a build could resolve instead of workspace source. */
32
+ export function registryInstalls(root: string): string[] {
33
+ const nested = entries(join(root, "packages")).flatMap((pkg) => {
34
+ const scope = join(root, "packages", pkg, "node_modules", "@workit-dev");
35
+ return entries(scope).map((name) => rel(root, join(scope, name)));
36
+ });
37
+ const packages = fold(join(realpathSync(root), "packages") + sep);
38
+ const scope = join(root, "node_modules", "@workit-dev");
39
+ const hoisted = entries(scope)
40
+ .filter((name) => !fold(realpathSync(join(scope, name))).startsWith(packages))
41
+ .map((name) => rel(root, join(scope, name)));
42
+ return [...nested, ...hoisted];
43
+ }
44
+
45
+ function bundles(dir: string): string[] {
46
+ return entries(dir).flatMap((name) => {
47
+ const file = join(dir, name);
48
+ if (statSync(file).isDirectory()) return bundles(file);
49
+ return name.endsWith(".js") ? [file] : [];
50
+ });
51
+ }
52
+
53
+ export type BundleProblem = { bundle: string; problem: string };
54
+
55
+ /** Sibling chunks a split bundle imports (`from "./x.js"`, `import("./x.js")`). */
56
+ const LOCAL_CHUNK = /(?:\bfrom|\bimport\s*\()\s*["']\.\/([^"']+\.js)["']/gu;
57
+
58
+ /**
59
+ * Built adapter bundles under `root`, and what each one inlined wrongly. A
60
+ * code-split entry (codex's hook) inlines core through its chunks, so the
61
+ * core-header requirement applies to each entry together with the chunks it
62
+ * reaches; a chunk only another bundle imports is not an entry.
63
+ */
64
+ export function bundleSources(root: string): { scanned: string[]; problems: BundleProblem[] } {
65
+ const scanned = RELEASE_PACKAGES.filter((pkg) => pkg !== "workit-core").flatMap((pkg) =>
66
+ bundles(join(root, "packages", pkg, "dist")),
67
+ );
68
+ const text = new Map(scanned.map((file) => [file, readFileSync(file, "utf8")]));
69
+ const chunks = (file: string): string[] =>
70
+ [...(text.get(file) ?? "").matchAll(LOCAL_CHUNK)]
71
+ .map((m) => join(dirname(file), m[1]))
72
+ .filter((chunk) => text.has(chunk));
73
+ const imported = new Set(scanned.flatMap(chunks));
74
+ const reachesCore = (entry: string): boolean => {
75
+ const seen = new Set<string>();
76
+ const queue = [entry];
77
+ while (queue.length) {
78
+ const file = queue.shift() as string;
79
+ if (seen.has(file)) continue;
80
+ seen.add(file);
81
+ if (WORKSPACE_CORE.test(text.get(file) ?? "")) return true;
82
+ queue.push(...chunks(file));
83
+ }
84
+ return false;
85
+ };
86
+ const problems = scanned.flatMap((file): BundleProblem[] => {
87
+ const bundle = rel(root, file);
88
+ const body = text.get(file) ?? "";
89
+ const copies = [...body.matchAll(REGISTRY_COPY)].map((m) => m[1].replaceAll("\\", "/"));
90
+ if (copies.length)
91
+ return [{ bundle, problem: `${copies.length} registry-copy modules, e.g. ${copies[0]}` }];
92
+ const minified = body.length / (body.split("\n").length || 1) > MINIFIED_BYTES_PER_LINE;
93
+ if (minified || imported.has(file) || reachesCore(file)) return [];
94
+ const first = /^\s*\/\/ (\S.*)$/mu.exec(body)?.[1] ?? "none";
95
+ return [
96
+ {
97
+ bundle,
98
+ problem: `no \`// packages/workit-core/src/\` module header in it or its chunks (first header: ${first})`,
99
+ },
100
+ ];
101
+ });
102
+ return { scanned: scanned.map((file) => rel(root, file)), problems };
103
+ }
104
+
105
+ /** Every failure under `root` as printable lines; empty when the release may proceed. */
106
+ export function verifyBundleSources(root: string, options: { installsOnly?: boolean } = {}) {
107
+ const failures = registryInstalls(root).map(
108
+ (path) => `${path}: installed copy outside packages/ (bundles would inline it)`,
109
+ );
110
+ if (options.installsOnly) return { failures, scanned: 0 };
111
+ const { scanned, problems } = bundleSources(root);
112
+ if (scanned.length === 0)
113
+ failures.push(`no adapter bundles under packages/*/dist; run \`bun run build\` first`);
114
+ for (const { bundle, problem } of problems) failures.push(`${bundle}: ${problem}`);
115
+ return { failures, scanned: scanned.length };
116
+ }
117
+
118
+ if (import.meta.main) {
119
+ const args = process.argv.slice(2);
120
+ const installsOnly = args.includes("--installs-only");
121
+ const root = resolve(args.find((arg) => !arg.startsWith("--")) ?? process.cwd());
122
+ const { failures, scanned } = verifyBundleSources(root, { installsOnly });
123
+ if (failures.length) {
124
+ console.error("adapter bundles would not inline the workspace's own sources:");
125
+ for (const failure of failures) console.error(` - ${failure}`);
126
+ console.error("remove the copies and rebuild; see release.yml NPM_CONFIG_WORKSPACES_UPDATE");
127
+ process.exit(1);
128
+ }
129
+ console.log(
130
+ installsOnly
131
+ ? "no registry copies of workspace packages installed"
132
+ : `verified ${scanned} adapter bundles inline workspace sources`,
133
+ );
134
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bun
2
+ // Rewrites packages/workit-core/schemas/ from the zod config schemas.
3
+ // test/workit-core/config-schemas.test.ts fails when a committed file drifts.
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { CONFIG_SCHEMA_FILES, renderConfigSchema } from "../src/core/config-schemas";
7
+
8
+ export const SCHEMAS_DIR = path.join(import.meta.dir, "..", "schemas");
9
+
10
+ if (import.meta.main) {
11
+ mkdirSync(SCHEMAS_DIR, { recursive: true });
12
+ for (const file of CONFIG_SCHEMA_FILES) {
13
+ writeFileSync(path.join(SCHEMAS_DIR, file), renderConfigSchema(file));
14
+ console.log(`wrote ${path.relative(process.cwd(), path.join(SCHEMAS_DIR, file))}`);
15
+ }
16
+ }
@@ -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 (workit-shape).
39
+ 6. **Hand off on approval.** Several slices go into a plan for
40
+ `workit fanout plan <plan.json>`, with scopes and dependencies
41
+ (workit-fanout). One slice goes to a build (workit-implement). 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 (workit-test-audit).
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
+ ```
@@ -0,0 +1,3 @@
1
+ # Codex: user-invoked only (the counterpart of disable-model-invocation).
2
+ policy:
3
+ allow_implicit_invocation: false