@brainervirus/workit-core 2.5.0 → 2.7.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.7.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[] };
@@ -16,12 +16,16 @@ Use this method when assessment selects the `testing` dimension.
16
16
  2. State one behavior and its observable result. Choose the narrowest stable
17
17
  boundary a caller or user depends on; avoid private helpers and incidental
18
18
  representations.
19
- 3. Write one vertical RED slice that fails for the missing behavior, run it, and
20
- preserve the actual failure as evidence. Implement the smallest change, then
21
- run the same slice GREEN and record its result. Close enforces the order: a
22
- testing requirement with GREEN but no preceding RED evidence stays unsatisfied.
23
- 4. Add only another slice for a distinct behavior or risk. Reconcile stale
24
- evidence if the candidate changes.
19
+ 3. Write one vertical RED slice that fails for the missing behavior and run it
20
+ through the CLI so the failure is observed: `workit check test` (the repo's
21
+ configured `test` check; `npx -y @brainervirus/workit-cli check test` when
22
+ `workit` is not on PATH). Implement the smallest change, then run the same
23
+ check GREEN. Close accepts only a fresh passing run of the configured check
24
+ that `workit check` observed; a recorded "tests pass" is a note, and an
25
+ ad-hoc `workit check -- <cmd>` never satisfies the gate. If no `test` check
26
+ is configured or detected, add `workit.checks.json` (committed) with it.
27
+ 4. Add only another slice for a distinct behavior or risk. Any edit makes the
28
+ observed check stale: re-run `workit check test` before closing.
25
29
 
26
30
  ## Reject noisy tests
27
31
 
@@ -36,8 +40,10 @@ Use this method when assessment selects the `testing` dimension.
36
40
  type-only or CSS-class coupling. If the test still passes when every
37
41
  imported function returns undefined, rewrite the assertion or delete it.
38
42
 
39
- Use shared `evidence` operations for RED/GREEN results. Do not add a second
40
- lifecycle, approval chain, or test workflow outside the current task state.
43
+ Run RED/GREEN through `workit check`, which records the observed result on the
44
+ current task; shared `evidence` operations are for notes and non-test evidence.
45
+ Do not add a second lifecycle, approval chain, or test workflow outside the
46
+ current task state.
41
47
 
42
48
  ## Common mistakes
43
49
 
@@ -45,4 +51,4 @@ lifecycle, approval chain, or test workflow outside the current task state.
45
51
  | --- | --- |
46
52
  | "The pin changed, so assert the new string" | Exercise the affected consumer behavior. |
47
53
  | "The code is obvious" | A small vertical slice still proves the contract. |
48
- | Keeping a passing test after the boundary moved | Mark it stale and retest the current candidate. |
54
+ | Keeping a passing test after the boundary moved | Re-run `workit check test` on the current tree. |
@@ -19,7 +19,8 @@ established root cause.
19
19
  inferences, and unknowns with references; trace the failing value and all
20
20
  relevant callers before editing.
21
21
  3. State the root-cause hypothesis and the smallest in-scope fix. Write a focused
22
- regression at the boundary when practical, then run RED and GREEN checks.
22
+ regression at the boundary when practical, then run RED and GREEN through
23
+ `workit check <name>` so the results are observed, not reported.
23
24
  4. Acquire writer authority through `writer` before mutation. Reconcile the
24
25
  candidate, evidence, and findings after the change; investigate sibling paths
25
26
  and stale conclusions rather than assuming the first patch worked.