@brainervirus/workit-core 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -6,6 +6,7 @@
6
6
  // them; merge-backs and the release's own manifest sync never do.
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { resolve } from "node:path";
9
+ import BUNDLED_DEPS_JSON from "./bundled-deps.json" with { type: "json" };
9
10
 
10
11
  export const RELEASE_PACKAGES = [
11
12
  "workit-core",
@@ -15,8 +16,90 @@ export const RELEASE_PACKAGES = [
15
16
  "workit-cursor",
16
17
  "workit-codex",
17
18
  "workit-pi",
19
+ "workit-claude-code",
18
20
  ] as const;
19
21
 
22
+ type ReleasePackage = (typeof RELEASE_PACKAGES)[number];
23
+
24
+ /**
25
+ * Sources a package's published dist/ inlines from OUTSIDE its own directory.
26
+ * Every adapter build is a no-external `bun build`, so whatever it imports
27
+ * from another workspace package is copied into its bundle: a runtime
28
+ * dependency on that package does not reach the shipped code. A change in
29
+ * these sources must therefore republish the bundling package too. Verified
30
+ * against the build entries' metafiles by
31
+ * test/workit-core/bundled-sources.test.ts.
32
+ */
33
+ const CORE = "packages/workit-core/"; // sources, skills, templates and package.json
34
+ // Cursor and Codex inline the MCP transport and its declared dependencies.
35
+ const MCP = ["packages/workit-mcp/src/", "packages/workit-mcp/package.json"];
36
+ // The bundled CLI inlines its sources, package.json (`workit --version`) and
37
+ // its third-party dependencies.
38
+ const CLI = ["packages/workit-cli/src/", "packages/workit-cli/package.json"];
39
+ export const BUNDLED_SOURCES: Partial<Record<ReleasePackage, readonly string[]>> = {
40
+ "workit-mcp": [CORE],
41
+ "workit-cli": [CORE],
42
+ "workit-opencode": [CORE],
43
+ "workit-cursor": [CORE, ...MCP],
44
+ "workit-codex": [CORE, ...MCP],
45
+ "workit-pi": [CORE],
46
+ "workit-claude-code": [CORE, ...CLI],
47
+ };
48
+
49
+ /**
50
+ * Third-party packages each dist/ inlines, as bun.lock `packages` keys
51
+ * (`zod`, or `@opencode-ai/plugin/zod` for a nested copy). A lockfile change
52
+ * republishes a package only when one of these resolves to a different
53
+ * version, so dev-tooling bumps never release. Kept in sync with the bundle
54
+ * metafiles by test/workit-core/bundled-sources.test.ts.
55
+ */
56
+ export const BUNDLED_DEPS = BUNDLED_DEPS_JSON as Partial<Record<ReleasePackage, string[]>>;
57
+
58
+ const LOCKFILE = "bun.lock";
59
+
60
+ /** `packages` key → resolved `name@version` from a bun.lock text (JSONC). */
61
+ export const lockResolutions = (text: string): Map<string, string> => {
62
+ // bun.lock is JSON with trailing commas.
63
+ const parsed = JSON.parse(text.replace(/,(\s*[}\]])/g, "$1")) as {
64
+ packages?: Record<string, unknown>;
65
+ };
66
+ const resolved = new Map<string, string>();
67
+ for (const [key, entry] of Object.entries(parsed.packages ?? {}))
68
+ if (Array.isArray(entry) && typeof entry[0] === "string") resolved.set(key, entry[0]);
69
+ return resolved;
70
+ };
71
+
72
+ const lockAt = (root: string, rev: string): Map<string, string> | null => {
73
+ try {
74
+ return lockResolutions(
75
+ execFileSync("git", ["show", `${rev}:${LOCKFILE}`], {
76
+ cwd: root,
77
+ encoding: "utf8",
78
+ stdio: ["ignore", "pipe", "ignore"],
79
+ maxBuffer: 64 * 1024 * 1024,
80
+ }),
81
+ );
82
+ } catch {
83
+ return null;
84
+ }
85
+ };
86
+
87
+ /** Packages whose inlined third-party resolutions differ between two revisions. */
88
+ export const lockChangedPackages = (root: string, from: string, to: string): ReleasePackage[] => {
89
+ const before = lockAt(root, from);
90
+ const after = lockAt(root, to);
91
+ if (before === null && after === null) return [];
92
+ return RELEASE_PACKAGES.filter((pkg) =>
93
+ (BUNDLED_DEPS[pkg] ?? []).some((key) => before?.get(key) !== after?.get(key)),
94
+ );
95
+ };
96
+
97
+ /** Every repository path whose change alters `pkg`'s published payload. */
98
+ export const payloadPaths = (pkg: ReleasePackage): string[] => [
99
+ `packages/${pkg}/`,
100
+ ...(BUNDLED_SOURCES[pkg] ?? []),
101
+ ];
102
+
20
103
  /** The release pipeline's own version-sync commit: never a release trigger. */
21
104
  const RELEASE_SYNC = /^chore\(release\): sync manifests\b/;
22
105
 
@@ -56,11 +139,15 @@ const subjectLevel = (commit: string): Level | null => {
56
139
  // combined diff drops files identical to either parent — e.g. hotfix-branch
57
140
  // back-merges), and -z returns raw NUL-delimited paths so spaces/non-ASCII
58
141
  // are never C-quoted. NUL is fine in captured output, never in argv.
59
- const commitsSince = (root: string, from: string): { message: string; files: string[] }[] => {
142
+ const commitsSince = (
143
+ root: string,
144
+ from: string,
145
+ ): { hash: string; message: string; files: string[] }[] => {
60
146
  const hashes = g(root, ["log", "--reverse", "--format=%H", `${from}..HEAD`])
61
147
  .split("\n")
62
148
  .filter(Boolean);
63
149
  return hashes.map((h) => ({
150
+ hash: h,
64
151
  message: g(root, ["show", "-s", "--format=%B", h]),
65
152
  files: g(root, ["diff-tree", "--no-commit-id", "--name-only", "-r", "-m", "--root", "-z", h])
66
153
  .split("\0")
@@ -80,20 +167,24 @@ export function analyzeReleaseScope(root = process.cwd()): {
80
167
  const levels: Level[] = [];
81
168
  const pkgs = new Set<string>();
82
169
  let payloadOnly = false;
83
- for (const { message, files } of commits) {
170
+ for (const { hash, message, files } of commits) {
84
171
  const subject = (message.split("\n")[0] ?? "").trim();
85
172
  if (RELEASE_SYNC.test(subject)) continue;
86
173
  const touched = files.filter((f) =>
87
- RELEASE_PACKAGES.some((p) => f.startsWith(`packages/${p}/`)),
174
+ RELEASE_PACKAGES.some((p) => payloadPaths(p).some((prefix) => f.startsWith(prefix))),
88
175
  );
89
- if (touched.length === 0) continue;
176
+ // A lockfile edit counts only for packages whose inlined deps moved.
177
+ const relocked = files.includes(LOCKFILE) ? lockChangedPackages(root, `${hash}^`, hash) : [];
178
+ if (touched.length === 0 && relocked.length === 0) continue;
90
179
  const lvl = subjectLevel(message);
91
180
  if (lvl) levels.push(lvl);
92
181
  else if (!subject.startsWith("Merge ")) payloadOnly = true;
93
- for (const f of touched) {
94
- const pkg = RELEASE_PACKAGES.find((p) => f.startsWith(`packages/${p}/`));
95
- if (pkg) pkgs.add(pkg);
96
- }
182
+ for (const pkg of RELEASE_PACKAGES)
183
+ if (
184
+ relocked.includes(pkg) ||
185
+ touched.some((f) => payloadPaths(pkg).some((prefix) => f.startsWith(prefix)))
186
+ )
187
+ pkgs.add(pkg);
97
188
  }
98
189
  if (levels.length === 0) return { level: payloadOnly ? "patch" : null, productPkgs: [...pkgs] };
99
190
  const level = levels.reduce<Level>(
@@ -0,0 +1,133 @@
1
+ {
2
+ "workit-mcp": [
3
+ "@modelcontextprotocol/sdk",
4
+ "@openclaw/fs-safe",
5
+ "ajv",
6
+ "ajv-formats",
7
+ "fast-deep-equal",
8
+ "fast-uri",
9
+ "json-schema-traverse",
10
+ "zod",
11
+ "zod-to-json-schema"
12
+ ],
13
+ "workit-cli": [
14
+ "@alcalzone/ansi-tokenize",
15
+ "@inkjs/ui",
16
+ "@openclaw/fs-safe",
17
+ "ansi-escapes",
18
+ "ansi-regex",
19
+ "ansi-styles",
20
+ "auto-bind",
21
+ "chalk",
22
+ "cli-boxes",
23
+ "cli-cursor",
24
+ "cli-truncate",
25
+ "code-excerpt",
26
+ "convert-to-spaces",
27
+ "deepmerge",
28
+ "environment",
29
+ "es-toolkit",
30
+ "escape-string-regexp",
31
+ "figures",
32
+ "get-east-asian-width",
33
+ "indent-string",
34
+ "ink",
35
+ "is-fullwidth-code-point",
36
+ "is-in-ci",
37
+ "is-unicode-supported",
38
+ "patch-console",
39
+ "react",
40
+ "react-devtools-core",
41
+ "react-reconciler",
42
+ "restore-cursor",
43
+ "restore-cursor/onetime",
44
+ "restore-cursor/onetime/mimic-fn",
45
+ "scheduler",
46
+ "signal-exit",
47
+ "slice-ansi",
48
+ "stack-utils",
49
+ "string-width",
50
+ "strip-ansi",
51
+ "terminal-size",
52
+ "widest-line",
53
+ "wrap-ansi",
54
+ "ws",
55
+ "yoga-layout",
56
+ "zod"
57
+ ],
58
+ "workit-opencode": [
59
+ "@openclaw/fs-safe",
60
+ "@opencode-ai/plugin",
61
+ "@opencode-ai/plugin/zod",
62
+ "@opencode/plugin",
63
+ "zod"
64
+ ],
65
+ "workit-cursor": [
66
+ "@modelcontextprotocol/sdk",
67
+ "@openclaw/fs-safe",
68
+ "ajv",
69
+ "ajv-formats",
70
+ "fast-deep-equal",
71
+ "fast-uri",
72
+ "json-schema-traverse",
73
+ "zod",
74
+ "zod-to-json-schema"
75
+ ],
76
+ "workit-codex": [
77
+ "@modelcontextprotocol/sdk",
78
+ "@openclaw/fs-safe",
79
+ "ajv",
80
+ "ajv-formats",
81
+ "fast-deep-equal",
82
+ "fast-uri",
83
+ "json-schema-traverse",
84
+ "zod",
85
+ "zod-to-json-schema"
86
+ ],
87
+ "workit-pi": ["@openclaw/fs-safe", "zod"],
88
+ "workit-claude-code": [
89
+ "@alcalzone/ansi-tokenize",
90
+ "@inkjs/ui",
91
+ "@openclaw/fs-safe",
92
+ "ansi-escapes",
93
+ "ansi-regex",
94
+ "ansi-styles",
95
+ "auto-bind",
96
+ "chalk",
97
+ "cli-boxes",
98
+ "cli-cursor",
99
+ "cli-truncate",
100
+ "code-excerpt",
101
+ "convert-to-spaces",
102
+ "deepmerge",
103
+ "environment",
104
+ "es-toolkit",
105
+ "escape-string-regexp",
106
+ "figures",
107
+ "get-east-asian-width",
108
+ "indent-string",
109
+ "ink",
110
+ "is-fullwidth-code-point",
111
+ "is-in-ci",
112
+ "is-unicode-supported",
113
+ "patch-console",
114
+ "react",
115
+ "react-devtools-core",
116
+ "react-reconciler",
117
+ "restore-cursor",
118
+ "restore-cursor/onetime",
119
+ "restore-cursor/onetime/mimic-fn",
120
+ "scheduler",
121
+ "signal-exit",
122
+ "slice-ansi",
123
+ "stack-utils",
124
+ "string-width",
125
+ "strip-ansi",
126
+ "terminal-size",
127
+ "widest-line",
128
+ "wrap-ansi",
129
+ "ws",
130
+ "yoga-layout",
131
+ "zod"
132
+ ]
133
+ }
@@ -4,7 +4,12 @@
4
4
  // package so release logs answer "what shipped?" without leaving the terminal.
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { resolve } from "node:path";
7
- import { latestTag, RELEASE_PACKAGES } from "./analyze-release-scope";
7
+ import {
8
+ latestTag,
9
+ lockChangedPackages,
10
+ payloadPaths,
11
+ RELEASE_PACKAGES,
12
+ } from "./analyze-release-scope";
8
13
 
9
14
  const git = (root: string, args: string[]): string =>
10
15
  execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
@@ -12,8 +17,13 @@ const git = (root: string, args: string[]): string =>
12
17
  export function changedPackages(root: string, fromTag: string): string[] {
13
18
  // Committed state only: <tag>..HEAD, never the working tree — unreviewed
14
19
  // local edits must not decide what ships.
20
+ // Plus packages whose inlined third-party deps resolve differently in the
21
+ // lockfile (BUNDLED_DEPS); a dev-tooling-only lockfile bump changes none.
22
+ const relocked = new Set<string>(lockChangedPackages(root, fromTag, "HEAD"));
15
23
  return RELEASE_PACKAGES.filter((pkg) => {
16
- const out = git(root, ["diff", "--name-only", `${fromTag}..HEAD`, "--", `packages/${pkg}`]);
24
+ if (relocked.has(pkg)) return true;
25
+ // Own directory plus any sources bundled into its dist/ (BUNDLED_SOURCES).
26
+ const out = git(root, ["diff", "--name-only", `${fromTag}..HEAD`, "--", ...payloadPaths(pkg)]);
17
27
  return out !== "";
18
28
  });
19
29
  }
@@ -38,20 +48,15 @@ export function publishChanged(opts: {
38
48
  execFileSync(cmd, args, { cwd: o.cwd, encoding: "utf8", stdio: "inherit" }));
39
49
  const tag =
40
50
  opts.fromTag !== undefined ? (opts.fromTag === "" ? null : opts.fromTag) : latestTag(root);
41
- if (tag === null) {
42
- // First-ever release: everything ships.
43
- const published: string[] = [];
44
- for (const pkg of RELEASE_PACKAGES) {
45
- const cwd = resolve(root, "packages", pkg);
46
- if (!dryRun) run("npm", ["publish", "--access", "public"], { cwd });
47
- published.push(pkg);
48
- console.log(`published ${pkg} @ ${cwd}`);
49
- }
50
- return { published, skipped: [], tag: null };
51
- }
52
- const changed = new Set(changedPackages(root, tag));
51
+ // First-ever release: everything ships.
52
+ const changed =
53
+ tag === null ? new Set<string>(RELEASE_PACKAGES) : new Set(changedPackages(root, tag));
53
54
  const published: string[] = [];
54
55
  const skipped: string[] = [];
56
+ const failed: Array<{ pkg: string; error: string }> = [];
57
+ // Every changed package is attempted even after a failure: one package that
58
+ // cannot publish (e.g. a first publish of a new npm name the token does not
59
+ // cover) must not leave the others unreleased. Failures throw at the end.
55
60
  for (const pkg of RELEASE_PACKAGES) {
56
61
  if (!changed.has(pkg)) {
57
62
  skipped.push(pkg);
@@ -62,12 +67,19 @@ export function publishChanged(opts: {
62
67
  try {
63
68
  if (!dryRun) run("npm", ["publish", "--access", "public"], { cwd });
64
69
  } catch (e) {
65
- console.log(`publish failed ${pkg}: ${e instanceof Error ? e.message : String(e)}`);
66
- throw e;
70
+ const error = e instanceof Error ? e.message : String(e);
71
+ failed.push({ pkg, error });
72
+ console.log(`publish failed ${pkg}: ${error}`);
73
+ continue;
67
74
  }
68
75
  published.push(pkg);
69
76
  console.log(`published ${pkg} @ ${cwd}`);
70
77
  }
78
+ if (failed.length > 0) {
79
+ const summary = `publish failed for ${failed.length} package(s): ${failed.map((f) => f.pkg).join(", ")}; published: ${published.join(", ") || "none"}. A first publish of a new @brainervirus package needs an npm token allowed to create packages in the scope.`;
80
+ console.log(summary);
81
+ throw new Error(summary);
82
+ }
71
83
  return { published, skipped, tag };
72
84
  }
73
85
 
@@ -25,14 +25,19 @@ for (const pkg of [
25
25
  "workit-cursor",
26
26
  "workit-codex",
27
27
  "workit-pi",
28
+ "workit-claude-code",
28
29
  ]) {
29
30
  const file = resolve(root, `packages/${pkg}/package.json`);
30
31
  const data = JSON.parse(readFileSync(file, "utf8"));
31
- const deps = data.dependencies;
32
- if (!deps) continue;
33
- for (const name of Object.keys(deps)) {
34
- if (name.startsWith("@brainervirus/")) deps[name] = `^${core.version}`;
35
- }
32
+ // devDependencies too: the Claude Code plugin bundles core and the CLI into
33
+ // dist/, so they are build-time only, but a published manifest must still
34
+ // 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("@brainervirus/")) deps[name] = `^${core.version}`;
40
+ }
36
41
  writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
37
42
  }
38
43
  // The root marketplace index carries no release version; only the package
@@ -40,6 +45,7 @@ for (const pkg of [
40
45
  for (const file of [
41
46
  resolve(root, "packages/workit-cursor/.cursor-plugin/plugin.json"),
42
47
  resolve(root, "packages/workit-codex/.codex-plugin/plugin.json"),
48
+ resolve(root, "packages/workit-claude-code/.claude-plugin/plugin.json"),
43
49
  ]) {
44
50
  const data = JSON.parse(readFileSync(file, "utf8"));
45
51
  data.version = core.version;
@@ -9,7 +9,7 @@ import { execFileSync } from "node:child_process";
9
9
  import { readFileSync, writeFileSync } from "node:fs";
10
10
  import { resolve } from "node:path";
11
11
 
12
- const SYNC_MANIFEST_PATHS = [
12
+ export const SYNC_MANIFEST_PATHS = [
13
13
  "package.json",
14
14
  "packages/workit-core/package.json",
15
15
  "packages/workit-mcp/package.json",
@@ -18,9 +18,13 @@ const SYNC_MANIFEST_PATHS = [
18
18
  "packages/workit-cursor/package.json",
19
19
  "packages/workit-codex/package.json",
20
20
  "packages/workit-pi/package.json",
21
+ "packages/workit-claude-code/package.json",
21
22
  // Kept in lockstep with packages/workit-core/package.json by contract test.
22
23
  "packages/workit-cursor/.cursor-plugin/plugin.json",
23
24
  "packages/workit-codex/.codex-plugin/plugin.json",
25
+ // Claude Code reads the plugin version from here (the marketplace entry
26
+ // carries none), so it must track the published npm version.
27
+ "packages/workit-claude-code/.claude-plugin/plugin.json",
24
28
  ];
25
29
 
26
30
  export type ManifestSyncResult = { version: string; changed: string[] };
@@ -9,6 +9,7 @@ import { planUninstall, type UninstallPaths } from "./uninstall";
9
9
  import {
10
10
  findHostExecutable,
11
11
  installedHostApp,
12
+ isClaudeWorkitInstalled,
12
13
  isCodexWorkitInstalled,
13
14
  isPiWorkitInstalled,
14
15
  type HostId,
@@ -24,7 +25,7 @@ export type HostDetection = {
24
25
 
25
26
  export type DetectHostsOptions = UninstallPaths;
26
27
 
27
- const HOSTS: HostId[] = ["opencode", "cursor", "codex", "pi"];
28
+ const HOSTS: HostId[] = ["opencode", "cursor", "codex", "pi", "claude-code"];
28
29
 
29
30
  /** Hosts the setup wizard configures through their native install paths. */
30
31
  export const WIZARD_HOSTS: HostId[] = [...HOSTS];
@@ -40,6 +41,7 @@ export function emptyDetection(): Record<HostId, HostDetection> {
40
41
  cursor: { detected: false, configured: false },
41
42
  codex: { detected: false, configured: false },
42
43
  pi: { detected: false, configured: false },
44
+ "claude-code": { detected: false, configured: false },
43
45
  };
44
46
  }
45
47
 
@@ -52,8 +54,10 @@ export function detectHosts(options: DetectHostsOptions = {}): Record<HostId, Ho
52
54
  const configuredByHost = new Map(plan.hosts.map((h) => [h.host, h.installed]));
53
55
  const found = emptyDetection();
54
56
  for (const host of HOSTS) {
57
+ // Claude Code ships as the `claude` CLI (the desktop app embeds it).
58
+ const executable = host === "claude-code" ? "claude" : host;
55
59
  const detected =
56
- findHostExecutable(host, { home, env }) !== null || installedHostApp(host, home, env);
60
+ findHostExecutable(executable, { home, env }) !== null || installedHostApp(host, home, env);
57
61
  found[host] = {
58
62
  detected,
59
63
  configured:
@@ -61,7 +65,9 @@ export function detectHosts(options: DetectHostsOptions = {}): Record<HostId, Ho
61
65
  ? isCodexWorkitInstalled(home, env) || (configuredByHost.get(host) ?? false)
62
66
  : host === "pi"
63
67
  ? isPiWorkitInstalled(home, env) || (configuredByHost.get(host) ?? false)
64
- : (configuredByHost.get(host) ?? false),
68
+ : host === "claude-code"
69
+ ? isClaudeWorkitInstalled(home, env)
70
+ : (configuredByHost.get(host) ?? false),
65
71
  };
66
72
  }
67
73
  return found;
@@ -33,6 +33,11 @@ import {
33
33
  isWorkitPlugin,
34
34
  } from "./registration";
35
35
  import { readWorkspacesResult, resolveWorkspaceFrom } from "./workspaces";
36
+ import {
37
+ CLAUDE_MARKETPLACE_NAME,
38
+ claudeWorkitInstalls,
39
+ type ClaudeWorkitInstall,
40
+ } from "./host-install";
36
41
  import { validateCursorSkills, WORKIT_METHOD_SKILLS } from "./skill-manifests";
37
42
  import {
38
43
  classifyHostGeneration,
@@ -53,6 +58,7 @@ export type DoctorCheckId =
53
58
  | "runtime"
54
59
  | "versions"
55
60
  | "codex_pin"
61
+ | "claude_plugin"
56
62
  | "assets"
57
63
  | "launcher"
58
64
  | "utility"
@@ -1737,10 +1743,65 @@ const checkWorkspaceLock = (res: Resolved): DoctorCheck => {
1737
1743
  };
1738
1744
  };
1739
1745
 
1746
+ const CLAUDE_PLUGIN_PACKAGE = "@brainervirus/workit-claude-code";
1747
+
1748
+ const versionBehind = (version: string, latest: string): boolean =>
1749
+ version !== latest && semverAtLeast(latest, version);
1750
+
1751
+ /**
1752
+ * Claude Code installs the marketplace plugin as a snapshot of the published
1753
+ * package (auto-update is off by default), so an install can lag the release.
1754
+ * It warns only when a newer plugin version is actually published: a plugin
1755
+ * older than this CLI is normal when no plugin payload changed since (the
1756
+ * plugin is republished only when its own or its bundled sources change).
1757
+ * The plugin keeps working; the fix is one native update. A `--plugin-dir`
1758
+ * local pin is per-session, never recorded, and never checked here.
1759
+ */
1760
+ const checkClaudePlugin = (res: Resolved): DoctorCheck & { registryProbed?: boolean } => {
1761
+ // Only installs Claude loads here: user scope, plus this project's.
1762
+ const installs = claudeWorkitInstalls(res.home, res.env, res.cwd);
1763
+ if (installs.length === 0)
1764
+ return {
1765
+ id: "claude_plugin",
1766
+ status: "pass",
1767
+ detail: "no Workit Claude Code plugin install recorded — skipping",
1768
+ };
1769
+ const fix = (install: ClaudeWorkitInstall) =>
1770
+ `claude plugin marketplace update ${CLAUDE_MARKETPLACE_NAME} && ${
1771
+ install.projectPath ? `cd ${JSON.stringify(install.projectPath)} && ` : ""
1772
+ }claude plugin update ${install.id}${install.scope === "user" ? "" : ` --scope ${install.scope}`}`;
1773
+ const latest = registryLatestVersion(res, CLAUDE_PLUGIN_PACKAGE);
1774
+ const problems: string[] = [];
1775
+ let repair: string | undefined;
1776
+ for (const install of installs) {
1777
+ const label = `${install.id} ${install.version ?? "(unknown version)"}${install.scope === "user" ? "" : ` (${install.scope} scope)`}`;
1778
+ if (latest && install.version && versionBehind(install.version, latest)) {
1779
+ problems.push(`stale_install: ${label} is behind published ${latest}`);
1780
+ repair ??= fix(install);
1781
+ }
1782
+ }
1783
+ const registryProbed = latest !== null && !res.env.WORKIT_DOCTOR_STALE_REGISTRY_VERSION;
1784
+ if (problems.length === 0)
1785
+ return {
1786
+ id: "claude_plugin",
1787
+ status: "pass",
1788
+ detail: `Claude Code plugin ${installs.map((i) => `${i.id} ${i.version ?? "?"}`).join(", ")} is current`,
1789
+ registryProbed,
1790
+ };
1791
+ return {
1792
+ id: "claude_plugin",
1793
+ status: "warn",
1794
+ detail: problems.join("; "),
1795
+ fix: repair,
1796
+ registryProbed,
1797
+ };
1798
+ };
1799
+
1740
1800
  const RUN_CHECKS: Array<(res: Resolved) => DoctorCheck> = [
1741
1801
  checkRuntime,
1742
1802
  checkVersions,
1743
1803
  checkCodexPin,
1804
+ checkClaudePlugin,
1744
1805
  checkAssets,
1745
1806
  checkLauncher,
1746
1807
  checkUtility,
@@ -11,7 +11,7 @@ import {
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
 
14
- export type HostId = "opencode" | "cursor" | "codex" | "pi";
14
+ export type HostId = "opencode" | "cursor" | "codex" | "pi" | "claude-code";
15
15
 
16
16
  /** A reviewed argv vector. Arguments are never composed into a shell string. */
17
17
  export type HostInstallCommand = {
@@ -89,6 +89,12 @@ export type HostInstallOptions = {
89
89
 
90
90
  const MARKETPLACE = "https://github.com/BrainerVirus/workit.git";
91
91
  const CODEX_MARKETPLACE_NAME = "workflow-toolkit";
92
+ /** Claude Code reads the git-hosted marketplace at the repo root
93
+ * (`.claude-plugin/marketplace.json`, name `workit`); its entry installs the
94
+ * published npm package. */
95
+ export const CLAUDE_MARKETPLACE_REPO = "BrainerVirus/workit";
96
+ export const CLAUDE_MARKETPLACE_NAME = "workit";
97
+ export const CLAUDE_PLUGIN_ID = `workit@${CLAUDE_MARKETPLACE_NAME}`;
92
98
  const validPackageVersion = (value: string): boolean =>
93
99
  value === "latest" || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
94
100
 
@@ -224,6 +230,109 @@ export const isCodexWorkitInstalled = (
224
230
  return false;
225
231
  };
226
232
 
233
+ /** Claude Code's config root (`CLAUDE_CONFIG_DIR`, default `~/.claude`). */
234
+ export const claudeConfigDir = (home: string, env: NodeJS.ProcessEnv = process.env): string =>
235
+ env.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude");
236
+
237
+ export type ClaudeInstallScope = "user" | "project" | "local";
238
+
239
+ export type ClaudeWorkitInstall = {
240
+ id: string;
241
+ version: string | null;
242
+ installPath: string;
243
+ scope: ClaudeInstallScope;
244
+ /** The project a project/local-scope install belongs to; null for user scope. */
245
+ projectPath: string | null;
246
+ };
247
+
248
+ const CLAUDE_SCOPES = new Set<string>(["user", "project", "local"]);
249
+
250
+ /** A project/local install applies to `cwd` when cwd is inside its project. */
251
+ const appliesTo = (install: ClaudeWorkitInstall, cwd: string | null): boolean => {
252
+ if (install.scope === "user") return true;
253
+ if (cwd === null || install.projectPath === null) return false;
254
+ const relative = path.relative(path.resolve(install.projectPath), path.resolve(cwd));
255
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
256
+ };
257
+
258
+ const isCanonicalClaudePlugin = (root: string): boolean => {
259
+ try {
260
+ const manifest = JSON.parse(
261
+ readFileSync(path.join(root, ".claude-plugin", "plugin.json"), "utf8"),
262
+ ) as { name?: string; repository?: string | { url?: string } };
263
+ const repository =
264
+ typeof manifest.repository === "string" ? manifest.repository : manifest.repository?.url;
265
+ return (
266
+ manifest.name === "workit" &&
267
+ repository?.replace(/\.git$/, "").toLowerCase() === "https://github.com/brainervirus/workit"
268
+ );
269
+ } catch {
270
+ return false;
271
+ }
272
+ };
273
+
274
+ /**
275
+ * Workit installs recorded in Claude Code's plugin registry
276
+ * (`<config>/plugins/installed_plugins.json`, v2: `{plugins: {"name@market":
277
+ * [{installPath, version}]}}`). Only entries whose install directory carries
278
+ * the canonical Workit manifest count, whatever marketplace they came from.
279
+ * Only installs that apply here count: user scope always, project/local
280
+ * scope only when `cwd` is inside the recorded project (`cwd: null` keeps
281
+ * user scope only). A `--plugin-dir` pin is per-session and never recorded.
282
+ */
283
+ export const claudeWorkitInstalls = (
284
+ home: string,
285
+ env: NodeJS.ProcessEnv = process.env,
286
+ cwd: string | null = process.cwd(),
287
+ ): ClaudeWorkitInstall[] => {
288
+ const file = path.join(claudeConfigDir(home, env), "plugins", "installed_plugins.json");
289
+ let registry: unknown;
290
+ try {
291
+ registry = JSON.parse(readFileSync(file, "utf8"));
292
+ } catch {
293
+ return [];
294
+ }
295
+ const plugins =
296
+ registry && typeof registry === "object" && "plugins" in registry ? registry.plugins : null;
297
+ if (!plugins || typeof plugins !== "object") return [];
298
+ const found: ClaudeWorkitInstall[] = [];
299
+ for (const [id, entries] of Object.entries(plugins as Record<string, unknown>)) {
300
+ if (!id.startsWith("workit@") || !Array.isArray(entries)) continue;
301
+ for (const entry of entries as Array<{
302
+ installPath?: unknown;
303
+ version?: unknown;
304
+ scope?: unknown;
305
+ projectPath?: unknown;
306
+ }>) {
307
+ if (typeof entry?.installPath !== "string" || !isCanonicalClaudePlugin(entry.installPath))
308
+ continue;
309
+ // Older registries omit scope; Claude's default install scope is user.
310
+ const scope = entry.scope === undefined ? "user" : String(entry.scope);
311
+ if (!CLAUDE_SCOPES.has(scope)) continue; // e.g. managed: not ours to change
312
+ const install: ClaudeWorkitInstall = {
313
+ id,
314
+ installPath: entry.installPath,
315
+ version:
316
+ typeof entry.version === "string" && /^\d+\.\d+\.\d+/.test(entry.version)
317
+ ? entry.version
318
+ : null,
319
+ scope: scope as ClaudeInstallScope,
320
+ projectPath:
321
+ scope !== "user" && typeof entry.projectPath === "string" ? entry.projectPath : null,
322
+ };
323
+ if (appliesTo(install, cwd)) found.push(install);
324
+ }
325
+ }
326
+ return found;
327
+ };
328
+
329
+ /** A Workit install Claude Code loads for this user (any project). */
330
+ export const isClaudeWorkitInstalled = (
331
+ home: string,
332
+ env: NodeJS.ProcessEnv = process.env,
333
+ cwd: string | null = process.cwd(),
334
+ ): boolean => claudeWorkitInstalls(home, env, cwd).length > 0;
335
+
227
336
  export function installedHostApp(
228
337
  host: HostId,
229
338
  home: string,
@@ -297,6 +406,39 @@ export function planHostInstall(
297
406
  },
298
407
  ];
299
408
  }
409
+ if (host === "claude-code") {
410
+ if (!upgrading && isClaudeWorkitInstalled(home, env, null)) return [];
411
+ const base = hostCommand("claude", [], { home, cwd, env });
412
+ return [
413
+ nodePrerequisite({ home, cwd, env }),
414
+ {
415
+ ...base,
416
+ args: [...base.args, "plugin", "marketplace", "list", "--json"],
417
+ purpose: "Check whether the Workit Claude Code marketplace is already registered",
418
+ },
419
+ upgrading
420
+ ? {
421
+ ...base,
422
+ args: [...base.args, "plugin", "marketplace", "update", CLAUDE_MARKETPLACE_NAME],
423
+ purpose: "Refresh the Workit Claude Code marketplace",
424
+ }
425
+ : {
426
+ ...base,
427
+ args: [...base.args, "plugin", "marketplace", "add", CLAUDE_MARKETPLACE_REPO],
428
+ purpose: "Register the Workit Claude Code marketplace",
429
+ skipWhenOutputIncludes: `"name": "${CLAUDE_MARKETPLACE_NAME}"`,
430
+ },
431
+ {
432
+ ...base,
433
+ args: upgrading
434
+ ? [...base.args, "plugin", "update", CLAUDE_PLUGIN_ID]
435
+ : [...base.args, "plugin", "install", CLAUDE_PLUGIN_ID, "--scope", "user"],
436
+ purpose: upgrading
437
+ ? "Update the Workit Claude Code plugin"
438
+ : "Install the Workit Claude Code plugin",
439
+ },
440
+ ];
441
+ }
300
442
  if (host === "pi") {
301
443
  if (!upgrading && isPiWorkitInstalled(home, env)) return [];
302
444
  const base = hostCommand("pi", [], { home, cwd, env });
@@ -440,6 +582,7 @@ export function runHostCommand(
440
582
  const env: NodeJS.ProcessEnv = { ...process.env, ...context.env, HOME: home };
441
583
  env.CODEX_HOME = context.env?.CODEX_HOME ?? path.join(home, ".codex");
442
584
  env.PI_CODING_AGENT_DIR = context.env?.PI_CODING_AGENT_DIR ?? path.join(home, ".pi", "agent");
585
+ env.CLAUDE_CONFIG_DIR = context.env?.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude");
443
586
  env.XDG_CONFIG_HOME = context.env?.XDG_CONFIG_HOME ?? path.join(home, ".config");
444
587
  return env;
445
588
  })(),
package/src/core/setup.ts CHANGED
@@ -52,6 +52,8 @@ import { writeFileExclusive } from "./safe-write";
52
52
  import {
53
53
  isCodexWorkitInstalled,
54
54
  isPiWorkitInstalled,
55
+ isClaudeWorkitInstalled,
56
+ claudeConfigDir,
55
57
  planHostInstall,
56
58
  runHostInstall,
57
59
  type HostCommandRunner,
@@ -385,25 +387,27 @@ export function buildSetupPreview(
385
387
  mutations.push({ type: "install-adapter", platform, path: paths.cursorPluginDir });
386
388
  mutations.push({ type: "register-platform", platform, path: paths.cursorSettings });
387
389
  mutations.push({ type: "register-platform", platform, path: paths.cursorMcp });
388
- } else if (platform === "codex" || platform === "pi") {
390
+ } else if (platform === "codex" || platform === "pi" || platform === "claude-code") {
389
391
  const commands = planHostInstall(platform, { home: paths.home, cwd: paths.cwd, env });
390
392
  if (commands.length > 0) {
391
393
  mutations.push({
392
394
  type: "install-host",
393
395
  platform,
394
396
  path:
395
- platform === "codex"
396
- ? path.join(
397
- env.CODEX_HOME ?? path.join(paths.home, ".codex"),
398
- "plugins",
399
- "cache",
400
- "workflow-toolkit",
401
- "workit",
402
- )
403
- : path.join(
404
- env.PI_CODING_AGENT_DIR ?? path.join(paths.home, ".pi", "agent"),
405
- "settings.json",
406
- ),
397
+ platform === "claude-code"
398
+ ? claudeInstallRecord(paths.home, env)
399
+ : platform === "codex"
400
+ ? path.join(
401
+ env.CODEX_HOME ?? path.join(paths.home, ".codex"),
402
+ "plugins",
403
+ "cache",
404
+ "workflow-toolkit",
405
+ "workit",
406
+ )
407
+ : path.join(
408
+ env.PI_CODING_AGENT_DIR ?? path.join(paths.home, ".pi", "agent"),
409
+ "settings.json",
410
+ ),
407
411
  commands,
408
412
  });
409
413
  }
@@ -446,6 +450,10 @@ export function buildSetupPreview(
446
450
  // Apply (Task 14: WZ-09, WZ-10, WZ-13-WZ-15; CA-08, CA-13, CA-14, CA-31).
447
451
  // ---------------------------------------------------------------------------
448
452
 
453
+ /** The Claude Code plugin registry a Workit install is recorded in. */
454
+ const claudeInstallRecord = (home: string, env: NodeJS.ProcessEnv): string =>
455
+ path.join(claudeConfigDir(home, env), "plugins", "installed_plugins.json");
456
+
449
457
  export type Platform = "opencode" | "cursor";
450
458
 
451
459
  export type SetupResultStatus = "Installed" | "Configured" | "Skipped" | "Failed";
@@ -1250,7 +1258,9 @@ export function applySetupPreview(
1250
1258
  res.env.PI_CODING_AGENT_DIR ?? path.join(res.home, ".pi", "agent"),
1251
1259
  "settings.json",
1252
1260
  )
1253
- : "";
1261
+ : mutation.platform === "claude-code"
1262
+ ? claudeInstallRecord(res.home, res.env)
1263
+ : "";
1254
1264
  if (mutation.path !== expectedPath) {
1255
1265
  entries.push({
1256
1266
  platform: mutation.platform,
@@ -1292,7 +1302,9 @@ export function applySetupPreview(
1292
1302
  ? isCodexWorkitInstalled(res.home, res.env)
1293
1303
  : mutation.platform === "pi"
1294
1304
  ? isPiWorkitInstalled(res.home, res.env)
1295
- : true;
1305
+ : mutation.platform === "claude-code"
1306
+ ? isClaudeWorkitInstalled(res.home, res.env, null)
1307
+ : true;
1296
1308
  if (!verified) {
1297
1309
  entries.push({
1298
1310
  platform: mutation.platform,
@@ -16,5 +16,9 @@ export const SUPPORT_MATRIX = {
16
16
  // an installed CLI drifts ahead so a fresh install never silently outruns
17
17
  // the qualification pin.
18
18
  codex: { cli: "0.153.4", desktopPackage: "26.901.20858", bundledCodexCli: "0.153.0-alpha.5" },
19
+ // Claude Code is a qualification host too: CI runs `claude plugin validate
20
+ // --strict` with exactly this CLI, and the hook output schema fixture
21
+ // (test/fixtures/claude-code-schemas) was transcribed from this binary.
22
+ claudeCode: { cli: "2.1.288" },
19
23
  os: ["ubuntu-latest", "macos-latest", "windows-latest"],
20
24
  } as const;
@@ -10,12 +10,28 @@ import { existsSync, readFileSync, realpathSync, rmSync, writeFileSync } from "n
10
10
  import os from "node:os";
11
11
  import path from "node:path";
12
12
  import { isWorkitPlugin } from "./registration";
13
+ import {
14
+ claudeWorkitInstalls,
15
+ findHostExecutable,
16
+ runHostCommand,
17
+ type HostCommandRunner,
18
+ } from "./host-install";
13
19
 
14
- export type UninstallHost = "opencode" | "cursor" | "codex" | "pi";
20
+ export type UninstallHost = "opencode" | "cursor" | "codex" | "pi" | "claude-code";
15
21
 
16
22
  export type UninstallAction =
17
23
  | { kind: "edit-json-remove"; path: string; detail: string }
18
- | { kind: "remove-dir"; path: string; detail: string };
24
+ | { kind: "remove-dir"; path: string; detail: string }
25
+ /** A host-native uninstall (Claude Code owns its plugin cache and registry). */
26
+ | {
27
+ kind: "host-command";
28
+ path: string;
29
+ detail: string;
30
+ command: "claude";
31
+ args: string[];
32
+ /** The project a project/local-scope uninstall must run from; null for user scope. */
33
+ cwd: string | null;
34
+ };
19
35
 
20
36
  export type UninstallHostPlan = {
21
37
  host: UninstallHost;
@@ -49,6 +65,10 @@ export type UninstallPaths = {
49
65
  cursorSettings?: string;
50
66
  cursorMcp?: string;
51
67
  cursorPluginDir?: string;
68
+ /** Injectable host-command runner for host-native uninstall actions. */
69
+ runHostCommand?: HostCommandRunner;
70
+ /** Project directory for Claude Code project/local-scope installs (default: process cwd). */
71
+ cwd?: string;
52
72
  };
53
73
 
54
74
  type ResolvedUninstall = {
@@ -321,7 +341,32 @@ export function planUninstall(paths: UninstallPaths = {}): UninstallPlan {
321
341
  : [],
322
342
  };
323
343
 
324
- return { hosts: [opencode, cursor, codex, pi] };
344
+ // Claude Code: one native `claude plugin uninstall <id> --scope <scope>`
345
+ // per recorded Workit install that applies here (user scope, plus this
346
+ // project's project/local scope, run from that project). A --plugin-dir
347
+ // pin is never recorded.
348
+ const home = paths.home ?? paths.env?.HOME ?? os.homedir();
349
+ const claudeInstalls = claudeWorkitInstalls(
350
+ home,
351
+ paths.env ?? process.env,
352
+ paths.cwd ?? process.cwd(),
353
+ );
354
+ const claude: UninstallHostPlan = {
355
+ host: "claude-code",
356
+ installed: claudeInstalls.length > 0,
357
+ actions: claudeInstalls.map((install) => ({
358
+ kind: "host-command" as const,
359
+ path: install.installPath,
360
+ detail: `claude plugin uninstall ${install.id} --scope ${install.scope}${
361
+ install.projectPath ? ` (in ${install.projectPath})` : ""
362
+ }`,
363
+ command: "claude" as const,
364
+ args: ["plugin", "uninstall", install.id, "--scope", install.scope],
365
+ cwd: install.projectPath,
366
+ })),
367
+ };
368
+
369
+ return { hosts: [opencode, cursor, codex, pi, claude] };
325
370
  }
326
371
 
327
372
  // CA-14 traversal guard: rm -rf is permitted ONLY on the exact resolved
@@ -390,6 +435,60 @@ const applyEditJsonRemove = (
390
435
  return { status: "removed" };
391
436
  };
392
437
 
438
+ const CLAUDE_PLUGIN_ID = /^workit@[A-Za-z0-9._-]+$/;
439
+
440
+ /** Runs a reviewed `claude plugin uninstall workit@<marketplace>`; any other
441
+ * argv is refused, so a tampered plan cannot run an arbitrary command. */
442
+ const applyClaudeUninstall = (
443
+ action: Extract<UninstallAction, { kind: "host-command" }>,
444
+ paths: UninstallPaths,
445
+ ): { status: UninstallResultStatus; detail?: string } => {
446
+ const [verb, sub, id, flag, scope, ...rest] = action.args;
447
+ const projectScoped = scope === "project" || scope === "local";
448
+ if (
449
+ action.command !== "claude" ||
450
+ verb !== "plugin" ||
451
+ sub !== "uninstall" ||
452
+ !CLAUDE_PLUGIN_ID.test(id ?? "") ||
453
+ flag !== "--scope" ||
454
+ !(scope === "user" || projectScoped) ||
455
+ rest.length > 0 ||
456
+ // user scope runs anywhere; project/local only from their project.
457
+ (projectScoped ? typeof action.cwd !== "string" : action.cwd !== null)
458
+ )
459
+ return {
460
+ status: "failed",
461
+ detail: `refusing unreviewed host command: ${action.args.join(" ")}`,
462
+ };
463
+ const home = paths.home ?? paths.env?.HOME ?? os.homedir();
464
+ const env = paths.env ?? process.env;
465
+ const recorded = claudeWorkitInstalls(home, env, action.cwd).some(
466
+ (install) =>
467
+ install.id === id &&
468
+ install.scope === scope &&
469
+ (install.projectPath === null
470
+ ? action.cwd === null
471
+ : path.resolve(install.projectPath) === path.resolve(action.cwd ?? "")),
472
+ );
473
+ if (!recorded)
474
+ return { status: "skipped", detail: `${id} (${scope} scope) is no longer installed` };
475
+ const executable = findHostExecutable("claude", { home, env });
476
+ if (!executable && !paths.runHostCommand)
477
+ return { status: "failed", detail: "claude executable was not found on PATH" };
478
+ const step = {
479
+ command: executable ?? "claude",
480
+ args: action.args,
481
+ ...(action.cwd === null ? {} : { cwd: action.cwd }),
482
+ purpose: `Uninstall the Workit Claude Code plugin ${id} (${scope} scope)`,
483
+ };
484
+ const result = paths.runHostCommand
485
+ ? paths.runHostCommand(step)
486
+ : runHostCommand(step, { home, env });
487
+ return result.exitCode === 0
488
+ ? { status: "removed", detail: action.detail }
489
+ : { status: "failed", detail: result.stderr || `exit ${result.exitCode}` };
490
+ };
491
+
393
492
  /** Applies ONLY the reviewed plan actions with the given path options. Each
394
493
  * planned action yields exactly one result entry; malformed host JSON fails
395
494
  * its own action untouched while the remaining actions proceed (CA-13). */
@@ -400,7 +499,11 @@ export function applyUninstall(plan: UninstallPlan, paths: UninstallPaths = {}):
400
499
  for (const action of hostPlan.actions) {
401
500
  let status: UninstallResultStatus;
402
501
  let detail: string | undefined;
403
- if (action.kind === "remove-dir") {
502
+ if (action.kind === "host-command") {
503
+ const outcome = applyClaudeUninstall(action, paths);
504
+ status = outcome.status;
505
+ detail = outcome.detail;
506
+ } else if (action.kind === "remove-dir") {
404
507
  // Resolve before comparing so ".."/symlink tricks can never widen the rm.
405
508
  const resolved = path.resolve(action.path);
406
509
  const allowed =
@@ -12,6 +12,20 @@ const NONE: HookDecision = { kind: "none" };
12
12
  /** Sessions already offered unfinished tasks in this process. */
13
13
  const offered = new Set<string>();
14
14
 
15
+ /** The Workit plugin's worktree-isolated implementer is the one Claude Code
16
+ * subagent that writes. Claude namespaces plugin agents, so only the exact
17
+ * `workit:implementer` is ours: a bare or other-plugin `implementer` is not. */
18
+ const CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer";
19
+
20
+ const subagentStartText = (
21
+ host: HookInput["host"],
22
+ descriptor: HostDescriptor,
23
+ event: Extract<HookInput["event"], { kind: "subagent.start" }>,
24
+ ): string =>
25
+ host === "claude_code" && event.agentType === CLAUDE_WORKTREE_IMPLEMENTER
26
+ ? `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) working in its own git worktree: it may edit and commit there, within its brief's scope. Before the first commit, switch to a policy-compliant branch (\`git switch -c <type>/<slug>\`, e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never push, open a PR, or merge unless the brief asks for it.`
27
+ : `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`;
28
+
15
29
  export function handleHook(input: HookInput, deps: HookDeps): HookDecision {
16
30
  const { descriptor } = deps;
17
31
  const event = input.event;
@@ -36,10 +50,7 @@ export function handleHook(input: HookInput, deps: HookDeps): HookDecision {
36
50
  case "shell.pre":
37
51
  return usable(descriptor.shellPolicy.deny) ? shellPolicy(input.cwd, event.command) : NONE;
38
52
  case "subagent.start":
39
- return {
40
- kind: "context",
41
- text: `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`,
42
- };
53
+ return { kind: "context", text: subagentStartText(input.host, descriptor, event) };
43
54
  case "compact.pre":
44
55
  return {
45
56
  kind: "notice",