@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,79 @@
1
+ #!/usr/bin/env bash
2
+ # Install OpenCode plugin: file:// pin to monorepo (or share clone) + native skill/command links.
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
6
+ ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)"
7
+ CORE_SCRIPTS="$ROOT/packages/workit-core/scripts"
8
+ . "$SCRIPT_DIR/lib/config-dir.sh"
9
+ SHARE="$(workit_data_dir)"
10
+ DEV="${WORKIT_DEV:-$ROOT}"
11
+ CONFIG="${HOME}/.config/opencode/opencode.json"
12
+
13
+ chmod +x "$CORE_SCRIPTS/sync-runtime.sh"
14
+ WORKIT_DEV="$ROOT" "$CORE_SCRIPTS/sync-runtime.sh"
15
+
16
+ # Prefer monorepo with .git. file:// pins skip opencode's bundled npm installer —
17
+ # git+file:// installs an EMPTY cache dir and fails silently, so the plugin never
18
+ # loads after restart. The pinned entry resolves via packages/workit-opencode/package.json main.
19
+ if [ -d "${DEV}/.git" ] && [ -f "${DEV}/packages/workit-opencode/src/plugin.ts" ]; then
20
+ PIN="file://${DEV}/packages/workit-opencode/src/plugin.ts"
21
+ elif [ -d "${SHARE}/.git" ] && [ -f "${SHARE}/packages/workit-opencode/src/plugin.ts" ]; then
22
+ PIN="file://${SHARE}/packages/workit-opencode/src/plugin.ts"
23
+ else
24
+ # Last resort: ensure share is a clone, then pin it
25
+ if [ ! -d "${SHARE}/.git" ]; then
26
+ TMP=$(mktemp -d)
27
+ if ! git clone --depth 1 "https://github.com/${WORKIT_REPO:-BrainerVirus/workit}.git" "$TMP"; then
28
+ echo "FATAL: could not clone https://github.com/${WORKIT_REPO:-BrainerVirus/workit}.git" >&2
29
+ exit 1
30
+ fi
31
+ rsync -a --delete --exclude '/versions' --exclude '/current' --exclude '/bin' --exclude node_modules --exclude cursor/mcp/node_modules "$TMP/" "$SHARE/"
32
+ rm -rf "$TMP"
33
+ fi
34
+ PIN="file://${SHARE}/packages/workit-opencode/src/plugin.ts"
35
+ fi
36
+
37
+ mkdir -p "$(dirname "$CONFIG")"
38
+ [ -f "$CONFIG" ] || printf '%s\n' "{}" >"$CONFIG"
39
+
40
+ # Merge/dedupe registrations via the shared core helper (RR-06): one dev pin,
41
+ # no legacy/current duplicates, unrelated user settings preserved.
42
+ CONFIG_PATH="$CONFIG" PIN_PATH="$PIN" REGISTRATION_TS="$ROOT/packages/workit-cli/src/admin/registration.ts" bun -e '
43
+ import fs from "node:fs";
44
+ const { mergeOpenCodeConfig } = await import(process.env.REGISTRATION_TS!);
45
+ const path = process.env.CONFIG_PATH!;
46
+ const data = JSON.parse(fs.readFileSync(path, "utf8"));
47
+ const { config, changed } = mergeOpenCodeConfig(data, process.env.PIN_PATH!);
48
+ fs.writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
49
+ console.log("Pinned:", process.env.PIN_PATH!);
50
+ if (changed.includes("plugins")) console.log("Registered Workit under plugins (deduplicated)");
51
+ if (changed.includes("plugin")) console.log("Moved the Workit entry from the deprecated plugin key to plugins");
52
+ if (changed.includes("skills.paths")) console.log("Dropped share skill paths");
53
+ console.log("Native skills/commands via ~/.config/opencode/{skills,commands}");
54
+ '
55
+
56
+ # file:// pin already ships skills + registers /wk-* via plugin config.
57
+ # Remove leftover native links so OpenCode does not warn about duplicate skill names.
58
+ find "${HOME}/.config/opencode/skills" -maxdepth 1 -name 'wf-*' -exec rm -f {} + 2>/dev/null || true
59
+ find "${HOME}/.config/opencode/skill" -maxdepth 1 -name 'wf-*' -exec rm -f {} + 2>/dev/null || true
60
+ find "${HOME}/.config/opencode/commands" -maxdepth 1 -name 'wf-*.md' -exec rm -f {} + 2>/dev/null || true
61
+ find "${HOME}/.config/opencode/command" -maxdepth 1 -name 'wf-*.md' -exec rm -f {} + 2>/dev/null || true
62
+
63
+ # Post-install verification: file:// pins skip the installer, so a missing/empty
64
+ # pinned entry is a silent no-op. Fail loudly instead of pretending it worked.
65
+ PLUGIN_ENTRY="${PIN#file://}"
66
+ if [ ! -s "$PLUGIN_ENTRY" ]; then
67
+ echo "FATAL: pinned plugin entry missing or empty: $PLUGIN_ENTRY" >&2
68
+ echo "The file:// pin written to $CONFIG will not load after restart." >&2
69
+ echo "Restore the monorepo (or fix WORKIT_DEV) and re-run this script." >&2
70
+ exit 1
71
+ fi
72
+
73
+ # DG-09: verify the just-written registration with the shared offline doctor.
74
+ if ! bun "$ROOT/packages/workit-cli/scripts/doctor-check.ts" opencode; then
75
+ echo "FATAL: post-install doctor found an unhealthy OpenCode registration" >&2
76
+ exit 1
77
+ fi
78
+
79
+ echo "OpenCode install done. Fully quit all opencode processes, then restart."
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # Register the Pi workit extension and skills from the local checkout or share clone.
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
6
+ ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)"
7
+ . "$SCRIPT_DIR/lib/config-dir.sh"
8
+ SHARE="$(workit_data_dir)"
9
+ DEV="${WORKIT_DEV:-$ROOT}"
10
+ PI_CONFIG="${HOME}/.pi/config.json"
11
+
12
+ chmod +x "$ROOT/packages/workit-core/scripts/sync-runtime.sh"
13
+ WORKIT_DEV="$ROOT" "$ROOT/packages/workit-core/scripts/sync-runtime.sh"
14
+
15
+ SOURCE="$DEV"
16
+ if [ ! -f "$SOURCE/packages/workit-pi/dist/workit.js" ] && [ -f "$SHARE/packages/workit-pi/dist/workit.js" ]; then
17
+ SOURCE="$SHARE"
18
+ fi
19
+
20
+ # pi 1.4.0 loads extensions from the settings `packages` list, not from
21
+ # ~/.pi/config.json below. Register the absolute package dir there (honoring
22
+ # PI_CODING_AGENT_DIR) so `pi list` picks it up; the config.json write stays
23
+ # because uninstall and detection still key pi state off it — migrating
24
+ # those two off config.json is a separate contract change.
25
+ AGENT_DIR="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
26
+ SETTINGS="$AGENT_DIR/settings.json"
27
+ mkdir -p "$AGENT_DIR"
28
+ PI_SETTINGS="$SETTINGS" EXT="$SOURCE/packages/workit-pi" bun -e '
29
+ import fs from "node:fs";
30
+ const path = process.env.PI_SETTINGS!;
31
+ const ext = process.env.EXT!;
32
+ const current = fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, "utf8")) : {};
33
+ const packages = Array.isArray(current.packages) ? [...current.packages] : [];
34
+ const next = {
35
+ ...current,
36
+ packages: [...packages.filter((p) => !String(p).includes("workit")), ext],
37
+ };
38
+ fs.writeFileSync(path, JSON.stringify(next, null, 2) + "\n");
39
+ console.log("Registered Pi package:", ext);
40
+ '
41
+
42
+ mkdir -p "$(dirname "$PI_CONFIG")"
43
+ PI_CONFIG="$PI_CONFIG" EXT="$SOURCE/packages/workit-pi/dist/workit.js" SKILLS="$SOURCE/packages/workit-pi/skills" bun -e '
44
+ import fs from "node:fs";
45
+ const path = process.env.PI_CONFIG!;
46
+ const current = fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, "utf8")) : {};
47
+ const next = {
48
+ ...current,
49
+ extensions: [process.env.EXT!],
50
+ skills: [process.env.SKILLS!],
51
+ };
52
+ fs.writeFileSync(path, JSON.stringify(next, null, 2) + "\n");
53
+ console.log("Registered Pi extension:", process.env.EXT);
54
+ '
55
+
56
+ echo "Pi package registered in $SETTINGS (and legacy $PI_CONFIG)"
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # Workit config, data and runtime dirs, as core/workit-paths.mjs resolves them:
3
+ # the functions are its CONFIG_DIR_SH, DATA_DIR_SH and RUNTIME_DIR_SH, verbatim
4
+ # (a test holds them).
5
+ workit_config_dir() {
6
+ if [ -n "${WORKIT_CONFIG:-}" ]; then printf '%s\n' "$WORKIT_CONFIG"; return; fi
7
+ if [ -n "${WORKIT_CONFIG_DIR:-}" ]; then printf '%s\n' "$WORKIT_CONFIG_DIR"; return; fi
8
+ case "${XDG_CONFIG_HOME:-}" in /*) printf '%s\n' "$XDG_CONFIG_HOME/workit"; return ;; esac
9
+ case "$(uname -s 2>/dev/null)" in
10
+ MINGW*|MSYS*|CYGWIN*) printf '%s\n' "${APPDATA:-$HOME/AppData/Roaming}/workit" ;;
11
+ *) printf '%s\n' "$HOME/.config/workit" ;;
12
+ esac
13
+ }
14
+ workit_data_dir() {
15
+ case "${XDG_DATA_HOME:-}" in /*) printf '%s\n' "$XDG_DATA_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/share/workit" ;;
19
+ esac
20
+ }
21
+ workit_runtime_dir() {
22
+ case "${XDG_RUNTIME_DIR:-}" in /*) printf '%s\n' "$XDG_RUNTIME_DIR/workit"; return ;; esac
23
+ case "$(uname -s 2>/dev/null)" in
24
+ MINGW*|MSYS*|CYGWIN*) printf '%s\n' "${TEMP:-${TMP:-/tmp}}/workit" ;;
25
+ *) set -- "${TMPDIR:-/tmp}"; printf '%s\n' "${1%/}/workit-$(id -u)" ;;
26
+ esac
27
+ }
28
+ resolve_config_dir() { workit_config_dir; }
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env bun
2
+ // AR-16: selective publishing. Publishes only packages whose payload changed
3
+ // since the release the registry serves for them; logs an exact skip line per
4
+ // unchanged package so release logs answer "what shipped?" without leaving the
5
+ // terminal.
6
+ import { execFileSync } from "node:child_process";
7
+ import { readFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import {
10
+ latestTag,
11
+ lockChangedPackages,
12
+ npmName,
13
+ payloadPaths,
14
+ RELEASE_PACKAGES,
15
+ RELEASE_TAG,
16
+ TAG_GLOB,
17
+ TAG_PREFIX,
18
+ } from "./analyze-release-scope";
19
+
20
+ const git = (root: string, args: string[]): string =>
21
+ execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
22
+
23
+ export function changedPackages(root: string, fromTag: string): string[] {
24
+ // Committed state only: <tag>..HEAD, never the working tree — unreviewed
25
+ // local edits must not decide what ships.
26
+ // Plus packages whose inlined third-party deps resolve differently in the
27
+ // lockfile (BUNDLED_DEPS); a dev-tooling-only lockfile bump changes none.
28
+ const relocked = new Set<string>(lockChangedPackages(root, fromTag, "HEAD"));
29
+ const changed = new Set<string>(
30
+ RELEASE_PACKAGES.filter((pkg) => {
31
+ if (relocked.has(pkg)) return true;
32
+ // Own directory plus any sources bundled into its dist/ (BUNDLED_SOURCES).
33
+ const out = git(root, [
34
+ "diff",
35
+ "--name-only",
36
+ `${fromTag}..HEAD`,
37
+ "--",
38
+ ...payloadPaths(pkg),
39
+ ]);
40
+ return out !== "";
41
+ }),
42
+ );
43
+ // A published package depends on its internal dependencies at ^<released
44
+ // version> (rewrite-workspace-deps), so each must ship that version too or
45
+ // the package cannot install (#386: workit-cli 8.21.1 required an
46
+ // unpublished workit-cursor 8.21.1).
47
+ const queue = [...changed];
48
+ for (let pkg = queue.pop(); pkg !== undefined; pkg = queue.pop())
49
+ for (const dep of internalDependencies(root, pkg))
50
+ if (!changed.has(dep)) {
51
+ changed.add(dep);
52
+ queue.push(dep);
53
+ }
54
+ return RELEASE_PACKAGES.filter((pkg) => changed.has(pkg));
55
+ }
56
+
57
+ type DependencyGroups = Partial<
58
+ Record<
59
+ "dependencies" | "peerDependencies" | "optionalDependencies" | "devDependencies",
60
+ Record<string, string>
61
+ >
62
+ >;
63
+
64
+ /** The groups npm installs with a published package (devDependencies never). */
65
+ const INSTALLED_GROUPS = ["dependencies", "peerDependencies", "optionalDependencies"] as const;
66
+
67
+ /** The release packages `pkg` declares as install-time dependencies at HEAD. */
68
+ const internalDependencies = (root: string, pkg: string): string[] => {
69
+ const manifest = JSON.parse(
70
+ git(root, ["show", `HEAD:packages/${pkg}/package.json`]),
71
+ ) as DependencyGroups;
72
+ return RELEASE_PACKAGES.filter((other) =>
73
+ INSTALLED_GROUPS.some((group) => manifest[group]?.[npmName(other)] !== undefined),
74
+ );
75
+ };
76
+
77
+ /**
78
+ * The order packages reach the registry (W2.38): each one after every release
79
+ * package it installs, and workit-cli last. `npx @workit-dev/cli@latest
80
+ * upgrade` is the user entry point and installs the host packages @latest, so
81
+ * a CLI published before them failed with ETARGET (v13.0.0: workit-cli landed
82
+ * 1m40s before the workit-cursor it requires). Throws on a dependency cycle.
83
+ */
84
+ export function publishOrder(root: string): string[] {
85
+ const order: string[] = [];
86
+ const visiting = new Set<string>();
87
+ const visit = (pkg: string, path: string[]) => {
88
+ if (order.includes(pkg)) return;
89
+ if (visiting.has(pkg))
90
+ throw new Error(`release dependency cycle: ${[...path, pkg].join(" -> ")}`);
91
+ visiting.add(pkg);
92
+ for (const dep of internalDependencies(root, pkg)) visit(dep, [...path, pkg]);
93
+ visiting.delete(pkg);
94
+ order.push(pkg);
95
+ };
96
+ const cli = "workit-cli";
97
+ for (const pkg of RELEASE_PACKAGES) if (pkg !== cli) visit(pkg, []);
98
+ visit(cli, []);
99
+ return order;
100
+ }
101
+
102
+ const HOST_PLUGIN_MANIFESTS = [
103
+ "packages/workit-cursor/.cursor-plugin/plugin.json",
104
+ "packages/workit-codex/.codex-plugin/plugin.json",
105
+ "packages/workit-claude-code/.claude-plugin/plugin.json",
106
+ ] as const;
107
+
108
+ /**
109
+ * The release tag semantic-release created on HEAD before its publish step,
110
+ * or null. With several (never expected), the highest by version order.
111
+ */
112
+ export function releaseTagAtHead(root: string): string | null {
113
+ return (
114
+ git(root, ["tag", "--points-at", "HEAD", "--list", TAG_GLOB, "--sort=-v:refname"])
115
+ .split("\n")
116
+ .map((line) => line.trim())
117
+ .find((line) => RELEASE_TAG.test(line)) ?? null
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Every place a published package carries the release version must say
123
+ * `tag` (AR-15, W2.34): each release package.json, each host plugin.json and
124
+ * each internal dependency range (`^<version>`, never `workspace:*`). The
125
+ * working tree is read on purpose: the release job writes these values in its
126
+ * own checkout and never commits them, so main keeps its last-synced version.
127
+ */
128
+ export function releaseVersionProblems(root: string, tag: string): string[] {
129
+ const match = RELEASE_TAG.exec(tag);
130
+ if (!match) return [`${tag} is not a ${TAG_PREFIX}<semver> release tag`];
131
+ const version = match[1];
132
+ const problems: string[] = [];
133
+ const read = (rel: string) => JSON.parse(readFileSync(resolve(root, rel), "utf8"));
134
+ for (const pkg of RELEASE_PACKAGES) {
135
+ const rel = `packages/${pkg}/package.json`;
136
+ const manifest = read(rel) as DependencyGroups & { version?: string };
137
+ if (manifest.version !== version)
138
+ problems.push(`${rel} version ${manifest.version} != ${version}`);
139
+ for (const group of [...INSTALLED_GROUPS, "devDependencies"] as const)
140
+ for (const [name, range] of Object.entries(manifest[group] ?? {}))
141
+ if (name.startsWith("@workit-dev/") && range !== `^${version}`)
142
+ problems.push(`${rel} ${group}.${name} is ${range}, not ^${version}`);
143
+ }
144
+ for (const rel of HOST_PLUGIN_MANIFESTS) {
145
+ const plugin = read(rel) as { version?: string };
146
+ if (plugin.version !== version) problems.push(`${rel} version ${plugin.version} != ${version}`);
147
+ }
148
+ return problems;
149
+ }
150
+
151
+ type Run = (cmd: string, args: string[], o: { cwd: string }) => unknown;
152
+
153
+ const RETRY_PAUSE_MS = 10_000;
154
+ const message = (e: unknown): string => (e instanceof Error ? e.message : String(e));
155
+
156
+ // npm view prints the version only when that exact version is on the registry.
157
+ const onRegistry = (run: Run, cwd: string, spec: string, version: string): boolean => {
158
+ try {
159
+ return String(run("npm", ["view", spec, "version"], { cwd }) ?? "").trim() === version;
160
+ } catch {
161
+ return false;
162
+ }
163
+ };
164
+
165
+ /** The version a registry placeholder holds: reserved the name, never a release. */
166
+ const PLACEHOLDER_VERSION = "0.0.1";
167
+
168
+ const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
169
+
170
+ /**
171
+ * The release tag to diff `pkg` from: the tag of the version the registry
172
+ * serves as `latest`. Null means ship it: the name is not on the registry, it
173
+ * holds only the placeholder, the lookup failed, or no local tag matches that
174
+ * version (it was published from elsewhere). A release whose publish failed
175
+ * part-way therefore ships the missed packages next time, whatever changed.
176
+ */
177
+ export function registryBaseTag(root: string, pkg: string, run: Run): string | null {
178
+ let version: string;
179
+ try {
180
+ version = String(run("npm", ["view", npmName(pkg), "version"], { cwd: root }) ?? "").trim();
181
+ } catch {
182
+ return null;
183
+ }
184
+ if (!SEMVER.test(version) || version === PLACEHOLDER_VERSION) return null;
185
+ const tag = `${TAG_PREFIX}${version}`;
186
+ return git(root, ["tag", "--list", tag]) === tag ? tag : null;
187
+ }
188
+
189
+ /**
190
+ * Publishes one package. A failed `npm publish` is checked against the
191
+ * registry first (a publish can land and still report an error), then retried
192
+ * once in a fresh process: v8.41.0 lost workit-core to a transient sigstore
193
+ * `TLOG_CREATE_ENTRY_ERROR` (409, "an equivalent entry already exists").
194
+ * Returns how it ended up on the registry; throws the last error otherwise.
195
+ */
196
+ function publishOne(run: Run, cwd: string, pause: (ms: number) => void): "published" | "present" {
197
+ const manifest = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf8")) as {
198
+ name: string;
199
+ version: string;
200
+ };
201
+ const spec = `${manifest.name}@${manifest.version}`;
202
+ const publish = () => run("npm", ["publish", "--access", "public"], { cwd });
203
+ try {
204
+ publish();
205
+ return "published";
206
+ } catch (first) {
207
+ console.log(`publish of ${spec} failed: ${message(first)}; checking the registry`);
208
+ if (onRegistry(run, cwd, spec, manifest.version)) return "present";
209
+ console.log(`${spec} is not on the registry; retrying the publish once`);
210
+ pause(RETRY_PAUSE_MS);
211
+ try {
212
+ publish();
213
+ return "published";
214
+ } catch (second) {
215
+ if (onRegistry(run, cwd, spec, manifest.version)) return "present";
216
+ throw second;
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * The packages a release ships and the tag each one was diffed from (null:
223
+ * shipped without a diff). Everything ships on a first-ever release (no
224
+ * previous tag) or with `all`; otherwise a package ships when its payload
225
+ * changed since its base, plus the internal dependencies of every package
226
+ * that ships (changedPackages).
227
+ */
228
+ function selectPackages(
229
+ root: string,
230
+ opts: { tag: string | null; all: boolean; registryBaseline: boolean; run: Run },
231
+ ): { changed: Set<string>; baseOf: Map<string, string | null> } {
232
+ const baseOf = new Map<string, string | null>();
233
+ if (opts.all || opts.tag === null) {
234
+ for (const pkg of RELEASE_PACKAGES) baseOf.set(pkg, null);
235
+ return { changed: new Set(RELEASE_PACKAGES), baseOf };
236
+ }
237
+ for (const pkg of RELEASE_PACKAGES)
238
+ baseOf.set(pkg, opts.registryBaseline ? registryBaseTag(root, pkg, opts.run) : opts.tag);
239
+ const changed = new Set<string>();
240
+ const byBase = new Map<string, string[]>();
241
+ for (const [pkg, base] of baseOf) {
242
+ if (base === null) changed.add(pkg);
243
+ else byBase.set(base, [...(byBase.get(base) ?? []), pkg]);
244
+ }
245
+ for (const [base, pkgs] of byBase)
246
+ for (const pkg of changedPackages(root, base)) if (pkgs.includes(pkg)) changed.add(pkg);
247
+ // A package shipped for its own reason still pulls its dependencies along.
248
+ const queue = [...changed];
249
+ for (let pkg = queue.pop(); pkg !== undefined; pkg = queue.pop())
250
+ for (const dep of internalDependencies(root, pkg))
251
+ if (!changed.has(dep)) {
252
+ changed.add(dep);
253
+ queue.push(dep);
254
+ }
255
+ return { changed, baseOf };
256
+ }
257
+
258
+ export function publishChanged(opts: {
259
+ root: string;
260
+ dryRun?: boolean;
261
+ /**
262
+ * Base tag to diff against. Empty string means first-ever release (ship
263
+ * all). Default: latestTag(root). semantic-release creates the NEW release
264
+ * tag before publish plugins run, so production passes the PREVIOUS tag via
265
+ * `${lastRelease.gitTag}` — diffing against latestTag() there is always
266
+ * empty and would skip every package.
267
+ */
268
+ fromTag?: string;
269
+ /**
270
+ * Diff each package from the release the registry serves for it
271
+ * (registryBaseTag) instead of from `fromTag`. The release job sets it.
272
+ */
273
+ registryBaseline?: boolean;
274
+ /** Ship every package whatever changed (PUBLISH_ALL=1 or `--all`). */
275
+ all?: boolean;
276
+ run?: Run;
277
+ /** Wait before the single publish retry. Default: a blocking sleep. */
278
+ pause?: (ms: number) => void;
279
+ }): { published: string[]; skipped: string[]; tag: string | null } {
280
+ const { root, dryRun = false } = opts;
281
+ // stdout is captured so `npm view` can be read; stderr (npm's progress and
282
+ // errors) stays on the release log.
283
+ const run =
284
+ opts.run ??
285
+ ((cmd: string, args: string[], o: { cwd: string }) => {
286
+ const out = execFileSync(cmd, args, {
287
+ cwd: o.cwd,
288
+ encoding: "utf8",
289
+ stdio: ["ignore", "pipe", "inherit"],
290
+ });
291
+ if (args[0] !== "view") process.stdout.write(out);
292
+ return out;
293
+ });
294
+ const pause = opts.pause ?? ((ms: number) => Bun.sleepSync(ms));
295
+ const tag =
296
+ opts.fromTag !== undefined ? (opts.fromTag === "" ? null : opts.fromTag) : latestTag(root);
297
+ const { changed, baseOf } = selectPackages(root, {
298
+ tag,
299
+ all: opts.all === true,
300
+ registryBaseline: opts.registryBaseline === true,
301
+ run,
302
+ });
303
+ const published: string[] = [];
304
+ const skipped: string[] = [];
305
+ const failed: Array<{ pkg: string; error: string }> = [];
306
+ // Every changed package is attempted even after a failure: one package that
307
+ // cannot publish (e.g. a new npm name with no trusted publisher yet) must
308
+ // not leave the others unreleased. Failures throw at the end.
309
+ // The exception is a package that installs a failed one: its ^<version>
310
+ // range would not resolve (ETARGET), so it is held back as failed too.
311
+ for (const pkg of publishOrder(root)) {
312
+ if (!changed.has(pkg)) {
313
+ skipped.push(pkg);
314
+ console.log(`skip ${pkg} (no payload change since ${baseOf.get(pkg) ?? tag})`);
315
+ continue;
316
+ }
317
+ const blockedBy = internalDependencies(root, pkg).filter((dep) =>
318
+ failed.some((f) => f.pkg === dep),
319
+ );
320
+ if (blockedBy.length > 0) {
321
+ const error = `not published: its dependency ${blockedBy.join(", ")} failed to publish`;
322
+ failed.push({ pkg, error });
323
+ console.log(`publish failed ${pkg}: ${error}`);
324
+ continue;
325
+ }
326
+ const cwd = resolve(root, "packages", pkg);
327
+ let outcome: "published" | "present" = "published";
328
+ try {
329
+ if (!dryRun) outcome = publishOne(run, cwd, pause);
330
+ } catch (e) {
331
+ const error = message(e);
332
+ failed.push({ pkg, error });
333
+ console.log(`publish failed ${pkg}: ${error}`);
334
+ continue;
335
+ }
336
+ published.push(pkg);
337
+ console.log(
338
+ outcome === "present"
339
+ ? `published ${pkg} @ ${cwd} (already on the registry)`
340
+ : `published ${pkg} @ ${cwd}`,
341
+ );
342
+ }
343
+ if (failed.length > 0) {
344
+ const summary = `publish failed for ${failed.length} package(s): ${failed.map((f) => f.pkg).join(", ")}; published: ${published.join(", ") || "none"}. Trusted publishing cannot create a package: publish a new @workit-dev name once by hand, then add its trusted publisher (docs/agents/release.md).`;
345
+ console.log(summary);
346
+ throw new Error(summary);
347
+ }
348
+ return { published, skipped, tag };
349
+ }
350
+
351
+ /**
352
+ * The release job's options from argv and env. @semantic-release/exec spawns
353
+ * this Cmd as a shell string whose only optional positional arg is rendered
354
+ * from ${lastRelease.gitTag}: present when a previous release exists, absent
355
+ * (rendered empty) on a first-ever release, which therefore ships every
356
+ * package. It never falls back to latestTag(): semantic-release has already
357
+ * tagged HEAD with the release being cut, so that diff is always empty.
358
+ * `--all` or PUBLISH_ALL=1 ships every package; PUBLISH_DRY_RUN=1 publishes
359
+ * nothing. Each package is diffed from the release the registry serves for it.
360
+ */
361
+ export function cliOptions(
362
+ argv: readonly string[],
363
+ env: Record<string, string | undefined>,
364
+ ): { fromTag: string; all: boolean; dryRun: boolean; registryBaseline: true } {
365
+ return {
366
+ fromTag: argv.find((arg) => !arg.startsWith("--")) ?? "",
367
+ all: argv.includes("--all") || env.PUBLISH_ALL === "1",
368
+ dryRun: env.PUBLISH_DRY_RUN === "1",
369
+ registryBaseline: true,
370
+ };
371
+ }
372
+
373
+ // The repo root is always the spawn cwd (release.config.cjs paths are
374
+ // repo-root-relative), so the CLI takes no root argument.
375
+ // Before anything reaches the registry, the version guard proves the tag
376
+ // semantic-release just created on HEAD and every published manifest agree;
377
+ // a mismatch fails the job with nothing published.
378
+ if (import.meta.main) {
379
+ const root = process.cwd();
380
+ const options = cliOptions(process.argv.slice(2), process.env);
381
+ const releaseTag = releaseTagAtHead(root);
382
+ if (releaseTag === null) {
383
+ if (!options.dryRun)
384
+ throw new Error(
385
+ `no ${TAG_GLOB} release tag on HEAD: publishing runs after semantic-release tags it`,
386
+ );
387
+ console.log(`dry run: no ${TAG_GLOB} release tag on HEAD, version check skipped`);
388
+ } else {
389
+ const problems = releaseVersionProblems(root, releaseTag);
390
+ if (problems.length > 0)
391
+ throw new Error(`release ${releaseTag} manifests disagree:\n ${problems.join("\n ")}`);
392
+ console.log(`every published manifest carries ${releaseTag}`);
393
+ }
394
+ publishChanged({ root, ...options });
395
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bun
2
+ // Release-sync before publish: rewrite every internal workspace:* dependency to
3
+ // ^<released version> in the platform packages, and mirror the core version +
4
+ // canonical URLs into the cursor plugin/marketplace manifests. Neither npm nor
5
+ // bun rewrites workspace:*, so a packed tarball with it silently drops the
6
+ // dependency (RR-01/AR-03). Runs as semantic-release prepareCmd (after the
7
+ // version bumps, before the npm publish phase); the writes stay in CI and never
8
+ // reach the repo (semantic-release does not commit back).
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+
12
+ const root = process.argv[2]
13
+ ? resolve(process.argv[2])
14
+ : resolve(import.meta.dir, "..", "..", "..");
15
+ const core = JSON.parse(readFileSync(resolve(root, "packages/workit-core/package.json"), "utf8"));
16
+ if (!core.version || typeof core.version !== "string") {
17
+ throw new Error(
18
+ `workit-core version missing in ${resolve(root, "packages/workit-core/package.json")}`,
19
+ );
20
+ }
21
+ for (const pkg of [
22
+ "workit-mcp",
23
+ "workit-cli",
24
+ "workit-opencode",
25
+ "workit-cursor",
26
+ "workit-codex",
27
+ "workit-pi",
28
+ "workit-claude-code",
29
+ ]) {
30
+ const file = resolve(root, `packages/${pkg}/package.json`);
31
+ const data = JSON.parse(readFileSync(file, "utf8"));
32
+ // devDependencies too: every adapter bundles core (and the MCP server) into
33
+ // dist/, so they are build-time only (N19) and npm never installs them, but
34
+ // a published manifest must still never carry workspace:*.
35
+ const groups = [data.dependencies, data.devDependencies].filter(Boolean);
36
+ if (groups.length === 0) continue;
37
+ for (const deps of groups)
38
+ for (const name of Object.keys(deps)) {
39
+ if (name.startsWith("@workit-dev/")) deps[name] = `^${core.version}`;
40
+ }
41
+ writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
42
+ }
43
+ // The root marketplace index carries no release version; only the package
44
+ // plugin manifest is versioned at release time.
45
+ for (const file of [
46
+ resolve(root, "packages/workit-cursor/.cursor-plugin/plugin.json"),
47
+ resolve(root, "packages/workit-codex/.codex-plugin/plugin.json"),
48
+ resolve(root, "packages/workit-claude-code/.claude-plugin/plugin.json"),
49
+ ]) {
50
+ const data = JSON.parse(readFileSync(file, "utf8"));
51
+ data.version = core.version;
52
+ writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
53
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ # Stable entrypoint for Cursor MCP deeplink / ~/.cursor/mcp.json (CA-17).
3
+ # Launch the published package's MCP bin through npx, never a repo-relative
4
+ # dist or share clone; a startup/network failure surfaces via npx's nonzero exit.
5
+ set -euo pipefail
6
+ exec npx -y --prefer-online --min-release-age=0 --package=@workit-dev/cursor@latest workit-cursor-mcp "$@"