@brainervirus/workit-cli 8.6.0 → 8.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.
Files changed (2) hide show
  1. package/dist/index.js +440 -36
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -65,7 +65,7 @@ var package_default;
65
65
  var init_package = __esm(() => {
66
66
  package_default = {
67
67
  name: "@brainervirus/workit-cli",
68
- version: "8.6.0",
68
+ version: "8.7.0",
69
69
  private: false,
70
70
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
71
71
  keywords: [
@@ -103,9 +103,9 @@ var init_package = __esm(() => {
103
103
  },
104
104
  dependencies: {
105
105
  "@babel/parser": "7.29.9",
106
- "@brainervirus/workit-core": "^8.6.0",
107
- "@brainervirus/workit-cursor": "^8.6.0",
108
- "@brainervirus/workit-opencode": "^8.6.0",
106
+ "@brainervirus/workit-core": "^8.7.0",
107
+ "@brainervirus/workit-cursor": "^8.7.0",
108
+ "@brainervirus/workit-opencode": "^8.7.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -15016,7 +15016,9 @@ function clearStanding(context, input) {
15016
15016
  target: input.target
15017
15017
  }, { now: context.now });
15018
15018
  }
15019
- function dirtyElsewhere(cwd, branch) {
15019
+ function dirtyElsewhere(cwd, branch, head) {
15020
+ if (headSha(cwd) === head)
15021
+ return worktreeTree(cwd)?.dirty !== false;
15020
15022
  const list = git4(cwd, ["worktree", "list", "--porcelain"]);
15021
15023
  let worktree = null;
15022
15024
  for (const line of (list ?? "").split(`
@@ -15051,7 +15053,7 @@ function recordVerdict(context, input) {
15051
15053
  const key = codeKey(context.cwd, { branch, base: context.base });
15052
15054
  if (!key.head)
15053
15055
  return err("not_found", `branch "${branch}" has no commit to judge`);
15054
- if (key.dirty === true || key.dirty === null && dirtyElsewhere(context.cwd, branch))
15056
+ if (key.dirty === true || key.dirty === null && dirtyElsewhere(context.cwd, branch, key.head))
15055
15057
  return err("blocked", `dirty_worktree: ${branch} has uncommitted changes, so its head is not what was judged`, "commit or stash the changes, re-check, then record the verdict");
15056
15058
  const session = context.actor.session;
15057
15059
  const selfReason = input.self === true ? "flag" : session && input.derivedFrom !== null ? null : "no_session";
@@ -63930,11 +63932,13 @@ var init_skill_manifests = __esm(() => {
63930
63932
 
63931
63933
  // packages/workit-cli/src/admin/doctor.ts
63932
63934
  import { spawnSync as spawnSync13 } from "node:child_process";
63935
+ import { createHash as createHash13 } from "node:crypto";
63933
63936
  import {
63934
63937
  existsSync as existsSync13,
63935
63938
  lstatSync as lstatSync4,
63936
63939
  mkdirSync as mkdirSync5,
63937
63940
  mkdtempSync,
63941
+ readdirSync as readdirSync7,
63938
63942
  readFileSync as readFileSync13,
63939
63943
  readlinkSync as readlinkSync2,
63940
63944
  rmSync as rmSync2,
@@ -63975,8 +63979,11 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
63975
63979
  cursorMcp: options.cursorMcp ?? path30.join(home, ".cursor", "mcp.json"),
63976
63980
  cursorPluginDir: options.cursorPluginDir ?? path30.join(home, ".cursor", "plugins", "local", "workit"),
63977
63981
  cursorHeartbeatDir: options.stateDir ?? (env.WORKFLOW_TOOLKIT_STATE || resolveStateDir()),
63982
+ codexHome: options.codexHome,
63983
+ piAgentDir: options.piAgentDir,
63978
63984
  env,
63979
- installer: options.installer ?? false
63985
+ installer: options.installer ?? false,
63986
+ cliVersion: options.cliVersion ?? null
63980
63987
  };
63981
63988
  }, readJson = (p) => {
63982
63989
  try {
@@ -64197,6 +64204,368 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64197
64204
  detail: `codex CLI ${installed} differs from the qualified ${qualified}`,
64198
64205
  fix: `Reinstall the qualified Codex CLI ${qualified} or record fresh qualification evidence`
64199
64206
  };
64207
+ }, readTomlTables = (file) => {
64208
+ const tables = new Map;
64209
+ let text;
64210
+ try {
64211
+ text = readFileSync13(file, "utf8");
64212
+ } catch {
64213
+ return tables;
64214
+ }
64215
+ const unquote = (raw) => {
64216
+ const s = raw.trim();
64217
+ if (s.startsWith('"')) {
64218
+ try {
64219
+ return JSON.parse(s);
64220
+ } catch {
64221
+ return s.slice(1, -1);
64222
+ }
64223
+ }
64224
+ return s.startsWith("'") ? s.slice(1, -1) : s;
64225
+ };
64226
+ const SEGMENT = /\s*("(?:[^"\\]|\\.)*"|'[^']*'|[A-Za-z0-9_-]+)\s*(?:\.|$)/y;
64227
+ let current = {};
64228
+ tables.set("", current);
64229
+ for (const line of text.split(/\r?\n/)) {
64230
+ const header = /^\s*\[([^[\]]+)\]\s*(?:#.*)?$/.exec(line);
64231
+ if (header) {
64232
+ const parts = [];
64233
+ SEGMENT.lastIndex = 0;
64234
+ let match;
64235
+ while (SEGMENT.lastIndex < header[1].length && (match = SEGMENT.exec(header[1])))
64236
+ parts.push(unquote(match[1]));
64237
+ const key = parts.join("\x00");
64238
+ current = tables.get(key) ?? {};
64239
+ tables.set(key, current);
64240
+ continue;
64241
+ }
64242
+ const pair = /^\s*("(?:[^"\\]|\\.)*"|[A-Za-z0-9_-]+)\s*=\s*(.+?)\s*$/.exec(line);
64243
+ if (!pair)
64244
+ continue;
64245
+ const value = pair[2].replace(/\s+#[^"']*$/, "");
64246
+ current[unquote(pair[1])] = value === "true" ? true : value === "false" ? false : unquote(value);
64247
+ }
64248
+ return tables;
64249
+ }, codexHomeOf = (res) => res.codexHome ?? (res.env.CODEX_HOME || path30.join(res.home, ".codex")), codexWorkitInstalls = (res) => {
64250
+ const codexHome = codexHomeOf(res);
64251
+ const configFile = path30.join(codexHome, "config.toml");
64252
+ const installs = [];
64253
+ for (const [key, table] of readTomlTables(configFile)) {
64254
+ const [kind, id, ...rest] = key.split("\x00");
64255
+ if (kind !== "plugins" || rest.length > 0 || !id?.startsWith("workit@") || table.enabled === false)
64256
+ continue;
64257
+ const marketplace = id.slice("workit@".length);
64258
+ const dir = path30.join(codexHome, "plugins", "cache", marketplace, "workit");
64259
+ let versions = [];
64260
+ try {
64261
+ versions = readdirSync7(dir).filter((v) => existsSync13(path30.join(dir, v, ".codex-plugin")));
64262
+ } catch {
64263
+ versions = [];
64264
+ }
64265
+ const version = versions.reduce((best, v) => best && semverAtLeast(best, v) ? best : v, null);
64266
+ installs.push({
64267
+ id,
64268
+ marketplace,
64269
+ root: version ? path30.join(dir, version) : null,
64270
+ version,
64271
+ configFile
64272
+ });
64273
+ }
64274
+ return installs;
64275
+ }, snakeEvent = (event) => event.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase(), codexHookHash = (event, matcher, handler) => {
64276
+ if (handler.type !== "command" || typeof handler.command !== "string" || Object.keys(handler).some((k) => !["type", "command", "timeout", "async"].includes(k)))
64277
+ return null;
64278
+ const hook = {
64279
+ async: handler.async ?? false,
64280
+ command: handler.command,
64281
+ timeout: handler.timeout ?? 600,
64282
+ type: "command"
64283
+ };
64284
+ const body = {
64285
+ event_name: snakeEvent(event),
64286
+ hooks: [hook],
64287
+ ...typeof matcher === "string" ? { matcher } : {}
64288
+ };
64289
+ return `sha256:${createHash13("sha256").update(JSON.stringify(body)).digest("hex")}`;
64290
+ }, codexHookTrust = (install, tables) => {
64291
+ const hooks = install.root ? readJson(path30.join(install.root, "hooks", "hooks.json"))?.hooks : null;
64292
+ if (!hooks || typeof hooks !== "object")
64293
+ return null;
64294
+ const result = [];
64295
+ for (const [event, groups] of Object.entries(hooks)) {
64296
+ if (!Array.isArray(groups))
64297
+ continue;
64298
+ groups.forEach((group, g) => {
64299
+ const handlers = Array.isArray(group?.hooks) ? group.hooks : [];
64300
+ handlers.forEach((handler, h) => {
64301
+ const key = `${install.id}:hooks/hooks.json:${snakeEvent(event)}:${g}:${h}`;
64302
+ const hash = codexHookHash(event, group?.matcher, handler ?? {});
64303
+ const state = tables.get(["hooks", "state", key].join("\x00"));
64304
+ const trusted = typeof state?.trusted_hash === "string" ? state.trusted_hash : null;
64305
+ result.push({
64306
+ key,
64307
+ hash,
64308
+ state: state?.enabled === false ? "disabled" : trusted === null ? "untrusted" : hash !== null && trusted !== hash ? "modified" : "trusted"
64309
+ });
64310
+ });
64311
+ });
64312
+ }
64313
+ return result;
64314
+ }, checkCodexHooks = (res) => {
64315
+ const installs = codexWorkitInstalls(res);
64316
+ if (installs.length === 0)
64317
+ return {
64318
+ id: "codex_hooks",
64319
+ status: "pass",
64320
+ detail: "no Workit Codex plugin enabled — skipping"
64321
+ };
64322
+ const tables = readTomlTables(installs[0].configFile);
64323
+ const problems = [];
64324
+ const toTrust = [];
64325
+ let reinstall;
64326
+ let total = 0;
64327
+ for (const install of installs) {
64328
+ const trust = codexHookTrust(install, tables);
64329
+ if (!trust) {
64330
+ problems.push(`${install.id}: no installed hooks/hooks.json`);
64331
+ reinstall ??= `codex plugin remove ${install.id} && codex plugin add ${install.id}`;
64332
+ continue;
64333
+ }
64334
+ total += trust.length;
64335
+ for (const state of ["untrusted", "modified", "disabled"]) {
64336
+ const hit = trust.filter((t) => t.state === state);
64337
+ if (hit.length > 0)
64338
+ problems.push(`${install.id}: ${hit.length} of ${trust.length} hooks ${state === "modified" ? "changed since trusted" : state} (${hit.map((t) => t.key.split(":")[2]).join(", ")})`);
64339
+ }
64340
+ toTrust.push(...trust.filter((t) => t.state !== "trusted"));
64341
+ }
64342
+ if (problems.length === 0)
64343
+ return {
64344
+ id: "codex_hooks",
64345
+ status: "pass",
64346
+ detail: `${total} Workit Codex plugin hooks trusted (${installs.map((i) => `${i.id} ${i.version ?? "?"}`).join(", ")})`
64347
+ };
64348
+ const disabled = toTrust.some((t) => t.state === "disabled");
64349
+ const fix = reinstall ?? `Start \`codex\`, run \`/hooks\` and ${disabled ? "enable and " : ""}trust the workit hooks (or pick "Trust all and continue" when Codex asks you to review hooks at startup)`;
64350
+ return {
64351
+ id: "codex_hooks",
64352
+ status: "warn",
64353
+ detail: `Codex skips untrusted plugin hooks — ${problems.join("; ")}`,
64354
+ fix
64355
+ };
64356
+ }, CODEX_AGENT_MARKER = "# Generated by Workit", AGENT_MARKER_VERSION, agentMarkerVersion = (text) => AGENT_MARKER_VERSION.exec(text)?.[1] ?? null, stampCodexAgent = (content, version) => version && content.startsWith(CODEX_AGENT_MARKER) && !agentMarkerVersion(content) ? `${CODEX_AGENT_MARKER} ${version}${content.slice(CODEX_AGENT_MARKER.length)}` : content, checkCodexAgents = (res) => {
64357
+ const installs = codexWorkitInstalls(res);
64358
+ if (installs.length === 0)
64359
+ return {
64360
+ id: "codex_agents",
64361
+ status: "pass",
64362
+ detail: "no Workit Codex plugin enabled — skipping"
64363
+ };
64364
+ const agentsDir = path30.join(codexHomeOf(res), "agents");
64365
+ const problems = [];
64366
+ const fixes = [];
64367
+ const installed = [];
64368
+ for (const install of installs) {
64369
+ const source = install.root ? path30.join(install.root, "agents") : null;
64370
+ const files = source && existsSync13(source) ? readdirSync7(source).filter((f) => f.endsWith(".toml")).toSorted() : [];
64371
+ if (files.length === 0) {
64372
+ problems.push(`${install.id} ${install.version ?? "?"} bundles no agents`);
64373
+ fixes.push(`codex plugin remove ${install.id} && codex plugin add ${install.id}`);
64374
+ continue;
64375
+ }
64376
+ const install_fix = `node ${JSON.stringify(path30.join(install.root, "dist", "launch-mcp.js"))} --install-agents, then start a new Codex session`;
64377
+ const pluginVersion = readJson(path30.join(install.root, "package.json"))?.version;
64378
+ const version = typeof pluginVersion === "string" ? pluginVersion.match(SEMVER)?.[0] ?? null : null;
64379
+ let stale = false;
64380
+ for (const file of files) {
64381
+ const dest = path30.join(agentsDir, file);
64382
+ const want = stampCodexAgent(readFileSync13(path30.join(source, file), "utf8"), version);
64383
+ let link = false;
64384
+ try {
64385
+ link = lstatSync4(dest).isSymbolicLink();
64386
+ } catch {
64387
+ link = false;
64388
+ }
64389
+ if (link) {
64390
+ problems.push(`${dest} is a symlink (the plugin never writes through one)`);
64391
+ fixes.push(`replace the symlink ${dest} with a file or remove it, then run ${install_fix}`);
64392
+ continue;
64393
+ }
64394
+ let have = null;
64395
+ try {
64396
+ have = readFileSync13(dest, "utf8");
64397
+ } catch {
64398
+ have = null;
64399
+ }
64400
+ const written = have === null ? null : agentMarkerVersion(have);
64401
+ const newer = written !== null && version !== null && versionBehind(version, written);
64402
+ if (have === want || newer) {
64403
+ installed.push(file.replace(/\.toml$/, ""));
64404
+ continue;
64405
+ }
64406
+ if (have !== null && !have.startsWith(CODEX_AGENT_MARKER)) {
64407
+ problems.push(`${dest} is a user agent that shadows ${install.id}'s ${file}`);
64408
+ fixes.push(`rename or remove ${dest}, then run ${install_fix}`);
64409
+ continue;
64410
+ }
64411
+ problems.push(`${dest} ${have === null ? "missing" : "outdated"}`);
64412
+ stale = true;
64413
+ }
64414
+ if (stale)
64415
+ fixes.push(install_fix);
64416
+ }
64417
+ if (problems.length === 0)
64418
+ return {
64419
+ id: "codex_agents",
64420
+ status: "pass",
64421
+ detail: `Codex agents ${installed.join(", ")} installed in ${agentsDir}`
64422
+ };
64423
+ return {
64424
+ id: "codex_agents",
64425
+ status: "warn",
64426
+ detail: problems.join("; "),
64427
+ fix: [...new Set(fixes)].join("; ")
64428
+ };
64429
+ }, PI_PACKAGE = "@brainervirus/workit-pi", PI_EXTENSION = "dist/workit.js", piAgentDirOf = (res) => {
64430
+ if (res.piAgentDir)
64431
+ return res.piAgentDir;
64432
+ const env = res.env.PI_CODING_AGENT_DIR;
64433
+ if (env)
64434
+ return env === "~" ? res.home : env.replace(/^~(?=\/|\\)/, res.home);
64435
+ return path30.join(res.home, ".pi", "agent");
64436
+ }, npmSpecName = (spec) => {
64437
+ const at = spec.indexOf("@", spec.startsWith("@") ? 1 : 0);
64438
+ return at > 0 ? { name: spec.slice(0, at), pinned: true } : { name: spec, pinned: false };
64439
+ }, findPiEntry = (res, agentDir) => {
64440
+ const scopes = [
64441
+ {
64442
+ scope: "project",
64443
+ settingsFile: path30.join(res.cwd, ".pi", "settings.json"),
64444
+ base: path30.join(res.cwd, ".pi")
64445
+ },
64446
+ { scope: "user", settingsFile: path30.join(agentDir, "settings.json"), base: agentDir }
64447
+ ];
64448
+ for (const { scope, settingsFile, base } of scopes) {
64449
+ const packages = readJson(settingsFile)?.packages;
64450
+ if (!Array.isArray(packages))
64451
+ continue;
64452
+ for (const entry of packages) {
64453
+ const source = typeof entry === "string" ? entry : typeof entry?.source === "string" ? entry.source : null;
64454
+ if (!source)
64455
+ continue;
64456
+ const filter = typeof entry === "object" ? entry : null;
64457
+ if (source.startsWith("npm:")) {
64458
+ const spec = npmSpecName(source.slice(4).trim());
64459
+ if (spec.name !== PI_PACKAGE)
64460
+ continue;
64461
+ const root = path30.join(base, "npm", "node_modules", ...PI_PACKAGE.split("/"));
64462
+ return { scope, settingsFile, source, filter, root, npm: { pinned: spec.pinned } };
64463
+ }
64464
+ if (/^(git:|https?:|ssh:|git@)/.test(source))
64465
+ continue;
64466
+ const local = path30.resolve(base, source.replace(/^~(?=\/|\\)/, res.home));
64467
+ if (readJson(path30.join(local, "package.json"))?.name === PI_PACKAGE)
64468
+ return { scope, settingsFile, source, filter, root: local, npm: null };
64469
+ }
64470
+ }
64471
+ return null;
64472
+ }, globToRegExp3 = (pattern) => new RegExp(`^${pattern.replace(/^\.\//, "").replace(/[.+^${}()|[\]\\]/g, "\\$&").split(/\*\*\/?/).map((part) => part.replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")).join(".*")}$`), piFilterKeeps = (patterns, file) => {
64473
+ if (!Array.isArray(patterns))
64474
+ return true;
64475
+ const list = patterns.filter((p) => typeof p === "string");
64476
+ if (list.length === 0)
64477
+ return false;
64478
+ const name = path30.posix.basename(file);
64479
+ const matches = (p) => {
64480
+ const re = globToRegExp3(p);
64481
+ return re.test(file) || re.test(name);
64482
+ };
64483
+ const exact = (p) => p.replace(/^\.\//, "") === file;
64484
+ const includes = list.filter((p) => !/^[!+-]/.test(p));
64485
+ let kept = includes.length === 0 || includes.some(matches);
64486
+ if (list.some((p) => p.startsWith("!") && matches(p.slice(1))))
64487
+ kept = false;
64488
+ if (list.some((p) => p.startsWith("+") && exact(p.slice(1))))
64489
+ kept = true;
64490
+ if (list.some((p) => p.startsWith("-") && exact(p.slice(1))))
64491
+ kept = false;
64492
+ return kept;
64493
+ }, checkPiExtension = (res) => {
64494
+ const agentDir = piAgentDirOf(res);
64495
+ if (!existsSync13(agentDir) && !commandOnPath("pi", res.env))
64496
+ return { id: "pi_extension", status: "pass", detail: "Pi not installed — skipping" };
64497
+ const installCmd = (scope, spec = `npm:${PI_PACKAGE}`) => `pi install ${spec}${scope === "project" ? " -l" : ""}`;
64498
+ const entry = findPiEntry(res, agentDir);
64499
+ if (!entry)
64500
+ return {
64501
+ id: "pi_extension",
64502
+ status: "warn",
64503
+ detail: `Workit Pi extension missing: no ${PI_PACKAGE} entry in ${path30.join(agentDir, "settings.json")} or ${path30.join(res.cwd, ".pi", "settings.json")}`,
64504
+ fix: installCmd("user")
64505
+ };
64506
+ const where = `${entry.source} (${entry.scope}, ${entry.settingsFile})`;
64507
+ const rebuild = `cd ${JSON.stringify(entry.root)} && bun run build`;
64508
+ const pkg = readJson(path30.join(entry.root, "package.json"));
64509
+ if (!pkg)
64510
+ return {
64511
+ id: "pi_extension",
64512
+ status: "warn",
64513
+ detail: `Workit Pi extension missing: ${where} is listed but not installed at ${entry.root}`,
64514
+ fix: entry.npm ? installCmd(entry.scope, entry.source) : `restore ${entry.root}, then ${rebuild}`
64515
+ };
64516
+ const declared = Array.isArray(pkg.pi?.extensions) ? pkg.pi.extensions.some((e) => typeof e === "string" && e.replace(/^\.\//, "") === PI_EXTENSION) : false;
64517
+ if (!declared || !existsSync13(path30.join(entry.root, PI_EXTENSION)))
64518
+ return {
64519
+ id: "pi_extension",
64520
+ status: "warn",
64521
+ detail: `Workit Pi extension not loading: ${entry.root} ${declared ? `has no ${PI_EXTENSION}` : `does not declare ${PI_EXTENSION} in its pi manifest`}`,
64522
+ fix: entry.npm ? `pi remove ${entry.source}${entry.scope === "project" ? " -l" : ""} && ${installCmd(entry.scope, entry.source)}` : rebuild
64523
+ };
64524
+ const filter = entry.filter?.extensions;
64525
+ if (!piFilterKeeps(filter, PI_EXTENSION))
64526
+ return {
64527
+ id: "pi_extension",
64528
+ status: "warn",
64529
+ detail: `Workit Pi extension not loading: the ${PI_PACKAGE} entry in ${entry.settingsFile} filters out ${PI_EXTENSION} (extensions: ${JSON.stringify(filter)})`,
64530
+ fix: `remove the "extensions" filter from the ${PI_PACKAGE} entry in ${entry.settingsFile}, or enable the extension with \`pi config${entry.scope === "project" ? " -l" : ""}\``
64531
+ };
64532
+ const version = typeof pkg.version === "string" ? pkg.version : null;
64533
+ const cli = res.cliVersion;
64534
+ if (version && cli && versionBehind(version, cli)) {
64535
+ if (!entry.npm)
64536
+ return {
64537
+ id: "pi_extension",
64538
+ status: "pass",
64539
+ detail: `Workit Pi extension ${version} from local checkout ${where} loads ${PI_EXTENSION} (older than workit ${cli}; a checkout is not compared with the registry)`
64540
+ };
64541
+ const latest = registryLatestVersion(res, PI_PACKAGE);
64542
+ const registryProbed = latest !== null && !res.env.WORKIT_DOCTOR_STALE_REGISTRY_VERSION;
64543
+ if (!latest)
64544
+ return {
64545
+ id: "pi_extension",
64546
+ status: "pass",
64547
+ detail: `Workit Pi extension ${version} from ${where} loads ${PI_EXTENSION}; registry unreachable, so whether a version newer than ${version} is published is unknown (workit ${cli})`
64548
+ };
64549
+ if (versionBehind(version, latest))
64550
+ return {
64551
+ id: "pi_extension",
64552
+ status: "warn",
64553
+ detail: `Workit Pi extension stale: ${PI_PACKAGE} ${version} from ${where} is older than workit ${cli} (published ${latest})`,
64554
+ fix: entry.npm.pinned ? installCmd(entry.scope, `npm:${PI_PACKAGE}@${latest}`) : `pi update npm:${PI_PACKAGE}`,
64555
+ registryProbed
64556
+ };
64557
+ return {
64558
+ id: "pi_extension",
64559
+ status: "pass",
64560
+ detail: `Workit Pi extension ${version} from ${where} is the newest published (workit ${cli})`,
64561
+ registryProbed
64562
+ };
64563
+ }
64564
+ return {
64565
+ id: "pi_extension",
64566
+ status: "pass",
64567
+ detail: `Workit Pi extension ${version ?? "?"} from ${where} loads ${PI_EXTENSION}`
64568
+ };
64200
64569
  }, assetPathsFor = (host, dev) => {
64201
64570
  const pkg = path30.join(dev, "packages", `workit-${host}`);
64202
64571
  switch (host) {
@@ -65304,6 +65673,7 @@ var init_doctor = __esm(() => {
65304
65673
  init_host_install();
65305
65674
  init_skill_manifests();
65306
65675
  OPENCODE_V1_FIX = `OpenCode < ${SUPPORT_MATRIX.opencode.minimum} cannot load Workit 3 (V2 plugin API only): upgrade OpenCode to ${SUPPORT_MATRIX.opencode.minimum}+, or stay on Workit 2.x by pinning "@brainervirus/workit-opencode@2" in the opencode.json "plugin" array`;
65676
+ AGENT_MARKER_VERSION = /^# Generated by Workit (\d+\.\d+\.\d+)\b/;
65307
65677
  canonicalCursorHook = cursorHooksEntry().command;
65308
65678
  SILENT_HOOK_GRACE_MS = 2 * 60000;
65309
65679
  SEMVER = /\d+\.\d+\.\d+/;
@@ -65312,6 +65682,9 @@ var init_doctor = __esm(() => {
65312
65682
  checkRuntime,
65313
65683
  checkVersions,
65314
65684
  checkCodexPin,
65685
+ checkCodexHooks,
65686
+ checkCodexAgents,
65687
+ checkPiExtension,
65315
65688
  checkOpencodeVersion,
65316
65689
  checkClaudePlugin,
65317
65690
  checkWorkitOnPath,
@@ -65351,7 +65724,7 @@ import {
65351
65724
  mkdirSync as mkdirSync6,
65352
65725
  mkdtempSync as mkdtempSync2,
65353
65726
  readFileSync as readFileSync14,
65354
- readdirSync as readdirSync7,
65727
+ readdirSync as readdirSync8,
65355
65728
  realpathSync as realpathSync7,
65356
65729
  renameSync as renameSync3,
65357
65730
  rmSync as rmSync3,
@@ -66237,7 +66610,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
66237
66610
  }
66238
66611
  }, samePluginContent = (src, dest, pluginDir, relative = "") => {
66239
66612
  try {
66240
- for (const entry of readdirSync7(src, { withFileTypes: true })) {
66613
+ for (const entry of readdirSync8(src, { withFileTypes: true })) {
66241
66614
  if (entry.name === "node_modules")
66242
66615
  continue;
66243
66616
  const source = path31.join(src, entry.name);
@@ -66252,7 +66625,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
66252
66625
  return false;
66253
66626
  }
66254
66627
  }
66255
- for (const entry of readdirSync7(dest, { withFileTypes: true })) {
66628
+ for (const entry of readdirSync8(dest, { withFileTypes: true })) {
66256
66629
  if (existsSync14(path31.join(src, entry.name)))
66257
66630
  continue;
66258
66631
  const rel = path31.join(relative, entry.name);
@@ -66271,7 +66644,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
66271
66644
  const sourceRules = path31.join(src, "rules");
66272
66645
  const installedRules = path31.join(dest, "rules");
66273
66646
  try {
66274
- for (const entry of readdirSync7(installedRules, { withFileTypes: true })) {
66647
+ for (const entry of readdirSync8(installedRules, { withFileTypes: true })) {
66275
66648
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
66276
66649
  continue;
66277
66650
  if (existsSync14(path31.join(sourceRules, entry.name)))
@@ -66288,7 +66661,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
66288
66661
  if (existsSync14(legacyRules)) {
66289
66662
  try {
66290
66663
  mkdirSync6(path31.join(res.cursorPluginDir, "rules"), { recursive: true });
66291
- for (const entry of readdirSync7(legacyRules, { withFileTypes: true })) {
66664
+ for (const entry of readdirSync8(legacyRules, { withFileTypes: true })) {
66292
66665
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
66293
66666
  continue;
66294
66667
  const target = path31.join(res.cursorPluginDir, "rules", entry.name);
@@ -70080,7 +70453,7 @@ import {
70080
70453
  } from "node:fs";
70081
70454
  import path35 from "node:path";
70082
70455
  import os12 from "node:os";
70083
- import { createHash as createHash13 } from "node:crypto";
70456
+ import { createHash as createHash14 } from "node:crypto";
70084
70457
  function previewUpgrade(hosts = HOSTS2, deps = {}) {
70085
70458
  const paths = upgradePaths(deps);
70086
70459
  const { home, configDir } = paths;
@@ -70208,7 +70581,7 @@ function applyUpgrade(plan, deps = {}) {
70208
70581
  mkdirSync8(backup, { recursive: true, mode: 448 });
70209
70582
  for (const file of Object.keys(plan.configDigests))
70210
70583
  if (existsSync17(file)) {
70211
- const target = path35.join(backup, createHash13("sha256").update(file).digest("hex").slice(0, 12) + "-" + path35.basename(file));
70584
+ const target = path35.join(backup, createHash14("sha256").update(file).digest("hex").slice(0, 12) + "-" + path35.basename(file));
70212
70585
  copyFileSync5(file, target);
70213
70586
  chmodSync(target, 384);
70214
70587
  }
@@ -70463,7 +70836,7 @@ var HOSTS2, upgradePaths = (deps) => {
70463
70836
  if (!data || typeof data !== "object" || Array.isArray(data))
70464
70837
  throw new Error(`${file} is not a configuration object`);
70465
70838
  return data;
70466
- }, digest2 = (file) => existsSync17(file) ? createHash13("sha256").update(readFileSync17(file)).digest("hex") : "missing", runner = (deps) => deps.run ?? ((command, args) => {
70839
+ }, digest2 = (file) => existsSync17(file) ? createHash14("sha256").update(readFileSync17(file)).digest("hex") : "missing", runner = (deps) => deps.run ?? ((command, args) => {
70467
70840
  const { home, env } = upgradePaths(deps);
70468
70841
  let step;
70469
70842
  try {
@@ -70949,7 +71322,12 @@ async function run5(argv, io) {
70949
71322
  }
70950
71323
  fixLock = clearStaleMetadataLock(root, { force });
70951
71324
  }
70952
- const report = runDoctor({ host: "cli", cwd: io.cwd, workspaceRoot: root });
71325
+ const report = runDoctor({
71326
+ host: "cli",
71327
+ cwd: io.cwd,
71328
+ workspaceRoot: root,
71329
+ cliVersion: package_default.version
71330
+ });
70953
71331
  let knowledge = null;
70954
71332
  try {
70955
71333
  knowledge = lintKnowledge(root);
@@ -70982,7 +71360,9 @@ async function run5(argv, io) {
70982
71360
  io.stdout(`${mark} ${check.id} — ${check.detail}
70983
71361
  `);
70984
71362
  if (check.fix)
70985
- io.stdout(` fix: ${check.fix}
71363
+ io.stdout(` fix: ${check.fix.replaceAll(`
71364
+ `, `
71365
+ `)}
70986
71366
  `);
70987
71367
  }
70988
71368
  io.stdout(`${knowledgeLine}
@@ -70992,6 +71372,7 @@ async function run5(argv, io) {
70992
71372
  return report.exitCode;
70993
71373
  }
70994
71374
  var init_doctor2 = __esm(() => {
71375
+ init_package();
70995
71376
  init_doctor();
70996
71377
  init_knowledge();
70997
71378
  init_store_lock();
@@ -71105,7 +71486,7 @@ var init_redact = __esm(() => {
71105
71486
 
71106
71487
  // packages/workit-core/src/checks.ts
71107
71488
  import { spawn, spawnSync as spawnSync16 } from "node:child_process";
71108
- import { createHash as createHash14 } from "node:crypto";
71489
+ import { createHash as createHash15 } from "node:crypto";
71109
71490
  import fs18 from "node:fs";
71110
71491
  import os13 from "node:os";
71111
71492
  import path37 from "node:path";
@@ -71439,7 +71820,7 @@ function pruneCheckLogs(storeRoot, options = {}) {
71439
71820
  }
71440
71821
  return report;
71441
71822
  }
71442
- var MAX_LOG_BYTES, TAIL_LINES = 80, MAX_LINE_CHARS = 400, redactLog = (text) => redactText(text), PEM_BEGIN, PEM_END, BATCH_BYTES, MAX_PENDING, PEM_BODY, MAX_KEY_LINES = 4096, MAX_KEY_BYTES, CMD_META, DRAIN_MS = 250, FORWARDED_SIGNALS, logDigest = (log) => `sha256:${createHash14("sha256").update(log).digest("hex")}`, LOG_RETENTION;
71823
+ var MAX_LOG_BYTES, TAIL_LINES = 80, MAX_LINE_CHARS = 400, redactLog = (text) => redactText(text), PEM_BEGIN, PEM_END, BATCH_BYTES, MAX_PENDING, PEM_BODY, MAX_KEY_LINES = 4096, MAX_KEY_BYTES, CMD_META, DRAIN_MS = 250, FORWARDED_SIGNALS, logDigest = (log) => `sha256:${createHash15("sha256").update(log).digest("hex")}`, LOG_RETENTION;
71443
71824
  var init_checks3 = __esm(() => {
71444
71825
  init_redact();
71445
71826
  MAX_LOG_BYTES = 2 * 1024 * 1024;
@@ -72429,6 +72810,23 @@ function patchEquivalent(cwd, branch, tip) {
72429
72810
  `).filter(Boolean) : [];
72430
72811
  return lines.length > 0 && lines.every((line) => line.startsWith("-"));
72431
72812
  }
72813
+ function resolveExpectPrefix(prefix, tips, label) {
72814
+ const known = [...new Set(tips.filter((tip) => typeof tip === "string"))];
72815
+ if (!SHA_PREFIX.test(prefix))
72816
+ return {
72817
+ ok: false,
72818
+ code: "invalid_input",
72819
+ error: "--expect must be a commit sha (full, or an abbreviation of at least 7 hex digits)"
72820
+ };
72821
+ const matches = known.filter((tip) => tip.startsWith(prefix));
72822
+ if (matches.length === 1)
72823
+ return { ok: true, sha: matches[0] };
72824
+ return {
72825
+ ok: false,
72826
+ code: "invalid_input",
72827
+ error: matches.length > 1 ? `--expect ${prefix} is ambiguous for ${label}; choose one of: ${matches.join(", ")}` : `--expect ${prefix} matches no known tip of ${label}${known.length ? `; known: ${known.join(", ")}` : ""}; pass the full sha`
72828
+ };
72829
+ }
72432
72830
  function executePush(cwd, plan, input) {
72433
72831
  const before = remoteRefTip(cwd, plan.rawUrl, `refs/heads/${plan.branch}`);
72434
72832
  if (!before.ok)
@@ -72439,10 +72837,15 @@ function executePush(cwd, plan, input) {
72439
72837
  const args = ["push", "--porcelain", "--no-follow-tags", "--recurse-submodules=no"];
72440
72838
  if (input.forceWithLease) {
72441
72839
  const explicit = input.expect !== undefined && input.expect !== null;
72442
- if (explicit && !SHA.test(input.expect))
72443
- return failure3("invalid_input", "--expect must be a full commit sha");
72840
+ let expectSha = input.expect ?? null;
72841
+ if (explicit && !SHA.test(expectSha)) {
72842
+ const resolved = resolveExpectPrefix(expectSha, [before.sha, recordedRemoteTip(cwd, plan)], `${plan.remote}/${plan.branch}`);
72843
+ if (!resolved.ok)
72844
+ return failure3(resolved.code, resolved.error);
72845
+ expectSha = resolved.sha;
72846
+ }
72444
72847
  const integrate = `git fetch ${plan.remote} ${plan.branch} && git rebase ${plan.remote}/${plan.branch} # or merge it; then: workit git push`;
72445
- const recorded = explicit ? input.expect : recordedRemoteTip(cwd, plan);
72848
+ const recorded = explicit ? expectSha : recordedRemoteTip(cwd, plan);
72446
72849
  if (recorded === undefined && before.sha !== null)
72447
72850
  return failure3("blocked", `lease_unknown: workit has no recorded push of ${plan.branch}, so it cannot tell whose commits ${plan.remote}/${plan.branch} (${before.sha.slice(0, 12)}) holds`, `${integrate} # or, after reviewing that tip: workit git push --force-with-lease --expect <sha you reviewed> (--overwrite-unintegrated only to drop commits you never had)`);
72448
72851
  const expected = recorded ?? null;
@@ -72574,7 +72977,7 @@ ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trimEnd()).filter((va
72574
72977
  ...lines.slice(-half)
72575
72978
  ].join(`
72576
72979
  `);
72577
- }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, TRAILER_CONFIG, nothingToCommit = (what) => failure3("failed", `nothing_to_commit: ${what}`, "workit git commit --allow-empty -m '<msg>' # only to record an empty commit on purpose"), SHA;
72980
+ }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, TRAILER_CONFIG, nothingToCommit = (what) => failure3("failed", `nothing_to_commit: ${what}`, "workit git commit --allow-empty -m '<msg>' # only to record an empty commit on purpose"), SHA, SHA_PREFIX;
72578
72981
  var init_ops = __esm(() => {
72579
72982
  init_branch();
72580
72983
  init_commit_flavors();
@@ -72588,6 +72991,7 @@ var init_ops = __esm(() => {
72588
72991
  SESSION_SAFE = /^[\w.:@/+-]{1,128}$/u;
72589
72992
  TRAILER_CONFIG = ["-c", "trailer.separators=:", "-c", "trailer.where=end"];
72590
72993
  SHA = /^[0-9a-f]{40,64}$/u;
72994
+ SHA_PREFIX = /^[0-9a-f]{7,39}$/u;
72591
72995
  });
72592
72996
 
72593
72997
  // packages/workit-core/src/forge/reruns.ts
@@ -73401,7 +73805,7 @@ var init_pr_ops = __esm(() => {
73401
73805
 
73402
73806
  // packages/workit-core/src/stack.ts
73403
73807
  import { spawnSync as spawnSync19 } from "node:child_process";
73404
- import { createHash as createHash15, randomBytes as randomBytes3 } from "node:crypto";
73808
+ import { createHash as createHash16, randomBytes as randomBytes3 } from "node:crypto";
73405
73809
  import fs20 from "node:fs";
73406
73810
  import path41 from "node:path";
73407
73811
  function worktreeOf(cwd, branch) {
@@ -74568,7 +74972,7 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
74568
74972
  return dir.ok ? path41.join(dir.data, "worktrees") : null;
74569
74973
  }, firstLine = (run) => (`${run.stderr}
74570
74974
  ${run.stdout}`.split(`
74571
- `).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300), stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash15("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
74975
+ `).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300), stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash16("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
74572
74976
  if (name.length === 0 || name.length > 200)
74573
74977
  return false;
74574
74978
  for (let index = 0;index < name.length; index += 1) {
@@ -76894,8 +77298,8 @@ async function push(argv, io) {
76894
77298
  const expect = flags.values.expect ?? null;
76895
77299
  if ((expect !== null || flags.booleans.has("overwrite-unintegrated")) && !flags.booleans.has("force-with-lease"))
76896
77300
  return usage3(io, "--expect and --overwrite-unintegrated only apply with --force-with-lease", PUSH_USAGE);
76897
- if (expect !== null && !/^[0-9a-f]{40,64}$/u.test(expect))
76898
- return usage3(io, "--expect must be a full commit sha", PUSH_USAGE);
77301
+ if (expect !== null && !/^[0-9a-f]{7,64}$/u.test(expect))
77302
+ return usage3(io, "--expect must be a commit sha (at least 7 hex digits)", PUSH_USAGE);
76899
77303
  const plan = pushPreflight(io.cwd);
76900
77304
  if (!plan.ok)
76901
77305
  return forgeFail(io, plan);
@@ -77497,7 +77901,7 @@ var init_stack2 = __esm(() => {
77497
77901
 
77498
77902
  // packages/workit-core/src/fanout.ts
77499
77903
  import { spawnSync as spawnSync23 } from "node:child_process";
77500
- import { createHash as createHash16 } from "node:crypto";
77904
+ import { createHash as createHash17 } from "node:crypto";
77501
77905
  import fs23 from "node:fs";
77502
77906
  import path45 from "node:path";
77503
77907
  function expandBraces(pattern, limit = 64) {
@@ -77978,7 +78382,7 @@ function sliceHash(slice) {
77978
78382
  context: _context,
77979
78383
  ...definition
77980
78384
  } = slice;
77981
- return createHash16("sha256").update(JSON.stringify(definition)).digest("hex").slice(0, 16);
78385
+ return createHash17("sha256").update(JSON.stringify(definition)).digest("hex").slice(0, 16);
77982
78386
  }
77983
78387
  function fanoutsDir(cwd) {
77984
78388
  const root = storeRoot(cwd);
@@ -78683,13 +79087,13 @@ var init_fanout_worktree = __esm(() => {
78683
79087
  });
78684
79088
 
78685
79089
  // packages/workit-core/src/fanout-brief.ts
78686
- import { createHash as createHash17 } from "node:crypto";
79090
+ import { createHash as createHash18 } from "node:crypto";
78687
79091
  function workerSession(lead, slice, attempt) {
78688
79092
  const tail = attempt === null ? "" : `+try${attempt}`;
78689
79093
  const full = `${lead}-w-${slice}${tail}`;
78690
79094
  if (full.length <= SESSION_MAX)
78691
79095
  return full;
78692
- const digest = createHash17("sha256").update(slice).digest("hex").slice(0, 12);
79096
+ const digest = createHash18("sha256").update(slice).digest("hex").slice(0, 12);
78693
79097
  return `${lead.slice(0, SESSION_MAX - 3 - digest.length - tail.length)}-w-${digest}${tail}`;
78694
79098
  }
78695
79099
  function renderBrief(cwd, plan, input) {
@@ -95875,7 +96279,7 @@ var init_rules = __esm(() => {
95875
96279
 
95876
96280
  // packages/workit-cli/src/test-audit/audit.ts
95877
96281
  import { spawnSync as spawnSync25 } from "node:child_process";
95878
- import { existsSync as existsSync19, readdirSync as readdirSync8, readFileSync as readFileSync22, statSync as statSync7 } from "node:fs";
96282
+ import { existsSync as existsSync19, readdirSync as readdirSync9, readFileSync as readFileSync22, statSync as statSync7 } from "node:fs";
95879
96283
  import path49 from "node:path";
95880
96284
  function git7(cwd, args) {
95881
96285
  const run = spawnSync25("git", ["-c", "core.quotepath=off", ...args], {
@@ -95886,7 +96290,7 @@ function git7(cwd, args) {
95886
96290
  return { ok: run.status === 0, stdout: run.stdout ?? "" };
95887
96291
  }
95888
96292
  function walkTests(dir, out) {
95889
- for (const entry of readdirSync8(dir, { withFileTypes: true })) {
96293
+ for (const entry of readdirSync9(dir, { withFileTypes: true })) {
95890
96294
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
95891
96295
  continue;
95892
96296
  const full = path49.join(dir, entry.name);
@@ -96022,7 +96426,7 @@ import {
96022
96426
  lstatSync as lstatSync7,
96023
96427
  mkdirSync as mkdirSync9,
96024
96428
  mkdtempSync as mkdtempSync4,
96025
- readdirSync as readdirSync9,
96429
+ readdirSync as readdirSync10,
96026
96430
  readFileSync as readFileSync23,
96027
96431
  readlinkSync as readlinkSync3,
96028
96432
  realpathSync as realpathSync9,
@@ -96119,7 +96523,7 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
96119
96523
  let removed = 0;
96120
96524
  let names = [];
96121
96525
  try {
96122
- names = readdirSync9(tmp);
96526
+ names = readdirSync10(tmp);
96123
96527
  } catch {
96124
96528
  return 0;
96125
96529
  }
@@ -96148,7 +96552,7 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
96148
96552
  }
96149
96553
  function linkNodeModules(origin, target, repo, copy) {
96150
96554
  mkdirSync9(target, { recursive: true });
96151
- for (const entry of readdirSync9(origin, { withFileTypes: true })) {
96555
+ for (const entry of readdirSync10(origin, { withFileTypes: true })) {
96152
96556
  const from = path50.join(origin, entry.name);
96153
96557
  const to = path50.join(target, entry.name);
96154
96558
  if (entry.name.startsWith("@") && entry.isDirectory()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cli",
3
- "version": "8.6.0",
3
+ "version": "8.7.0",
4
4
  "private": false,
5
5
  "description": "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
6
6
  "keywords": [
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@babel/parser": "7.29.9",
41
- "@brainervirus/workit-core": "^8.6.0",
42
- "@brainervirus/workit-cursor": "^8.6.0",
43
- "@brainervirus/workit-opencode": "^8.6.0",
41
+ "@brainervirus/workit-core": "^8.7.0",
42
+ "@brainervirus/workit-cursor": "^8.7.0",
43
+ "@brainervirus/workit-opencode": "^8.7.0",
44
44
  "@inkjs/ui": "2.0.0",
45
45
  "ink": "7.1.1",
46
46
  "react": "19.2.8"