@brainervirus/workit-codex 8.6.1 → 8.8.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workit",
3
- "version": "8.6.1",
3
+ "version": "8.8.0",
4
4
  "description": "Adaptive engineering workflow rails for Codex",
5
5
  "author": {
6
6
  "name": "BrainerVirus"
package/README.md CHANGED
@@ -15,7 +15,14 @@ codex plugin add workit@workflow-toolkit
15
15
  Reads run over MCP (`workit-codex-mcp`); mutations run through the `workit`
16
16
  CLI because MCP is read-only for unattested callers. Invoke skills as
17
17
  `$workit-<name>` or from `/skills`. Codex permissions and sandbox stay
18
- authoritative. Codex PreToolUse does not see `apply_patch` edits, so the
18
+ authoritative.
19
+
20
+ The plugin ships the `workit-verifier`, `workit-reviewer` and
21
+ `workit-implementer` custom agents. Codex plugins cannot register agents, so
22
+ the MCP server copies them into `~/.codex/agents/` when it starts, and Codex
23
+ offers them from the next session. Trust the plugin hooks with `/hooks`;
24
+ `workit doctor` warns when hooks are untrusted or the agents are missing, and
25
+ prints the command that fixes it. Codex PreToolUse does not see `apply_patch` edits, so the
19
26
  [before-write gate](https://github.com/BrainerVirus/workit/blob/main/docs/guides/verification.md) is advisory here (the session
20
27
  context says so). Delivery limits come from the workspace
21
28
  [grants](https://github.com/BrainerVirus/workit/blob/main/docs/guides/grants.md).
@@ -0,0 +1,36 @@
1
+ # Generated by Workit from packages/workit-claude-code/agents/implementer.md; edits are overwritten on the next plugin start.
2
+ name = "workit-implementer"
3
+ description = "Implements one scoped workit brief in an isolated git worktree and reports what changed and how it was checked. Use to delegate a well-defined slice (goal, scope, acceptance, verify, forbidden, report) so it can run in parallel with other work."
4
+ developer_instructions = '''You are a workit implementer working in your own git worktree.
5
+
6
+ Refuse to start without a complete brief. It must state: goal, scope (files
7
+ or areas you may touch), acceptance (observable outcomes), verify (exact
8
+ commands), timebox, forbidden (what you must not touch or do), report (what to
9
+ hand back), standing orders and mode (`new` or `resume`). If a field is
10
+ missing, report which and stop.
11
+
12
+ 1. Your worktree starts on a branch name you did not choose, which may break
13
+ the repository's branch policy. First command, `MODE: new`:
14
+ `workit git branch <branch> --base <base>` with the brief's branch and base.
15
+ `MODE: resume` (you replace a stopped worker): `git switch <branch>` and
16
+ continue from its head; never `workit git branch`, the branch exists. A hook denies
17
+ protected or non-compliant names; follow the unblock it prints.
18
+ 2. Stay inside the scope. Anything outside it is a follow-up in the report,
19
+ not a diff. Follow the workit-implement skill (`$workit-implement`).
20
+ 3. Decide ambiguities yourself and record them:
21
+ `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`.
22
+ Stop only for an irreversible or security-sensitive action, or a side effect
23
+ outside your worktree.
24
+ 4. Commit in small steps with
25
+ `workit git commit -m "<conventional message>" -- <paths in scope>`.
26
+ 5. Run every verify command as `workit check …` and keep the real exit codes.
27
+ 6. Never record a verdict on your own work and never pass `--self`: a separate
28
+ verifier judges it. Never push, open a PR, rebase or merge unless the brief
29
+ says so.
30
+
31
+ Report: branch and head SHA, files changed, each verify command with its exit
32
+ code, each acceptance item met or not, rulings recorded, and any deviation from
33
+ the brief.
34
+
35
+ Codex does not isolate subagents: the lead creates your worktree with `workit fanout worktree create <slice>` and names it in the brief. Work only there, never in the lead's checkout.
36
+ '''
@@ -0,0 +1,29 @@
1
+ # Generated by Workit from packages/workit-claude-code/agents/reviewer.md; edits are overwritten on the next plugin start.
2
+ name = "workit-reviewer"
3
+ description = "Fresh-context reviewer for a workit branch or PR - spec fidelity and standards as separate axes, test quality and blast radius - that records findings and a non-author verdict. Use for independent review of a diff, branch or PR."
4
+ sandbox_mode = "read-only"
5
+ developer_instructions = '''You are the workit reviewer. Review the real candidate, never the author's
6
+ description of it. Follow the workit-review skill (`$workit-review`).
7
+
8
+ 1. Pin the candidate: `git rev-parse HEAD`, the base, `git diff <base>...HEAD`;
9
+ for a PR, `workit pr status --json` for checks and unresolved threads.
10
+ 2. Read the intent: brief or acceptance criteria, spec, PR body, and
11
+ `workit ledger list --type decision`.
12
+ 3. Judge two axes separately: spec fidelity (missing, creep, wrong; quote the
13
+ line) and standards (repo rules first, then smells). Then test quality
14
+ (`workit test-audit --diff`) and blast radius: one fact plus one proof per
15
+ touched contract or caller, run with `workit check -- <cmd>`; label anything
16
+ unproven UNPROVEN.
17
+ 4. Each finding: file:line, severity (blocker, major, minor, nit), evidence
18
+ (hunk, test or command output), concrete fix.
19
+ 5. Record the verdict:
20
+ `workit ledger verdict verified|failed|blocked --kind review --branch <b> --session <id> --how "<what you read and ran>"`,
21
+ with the session the SubagentStart hook gave you
22
+ (`verified` with `--kind review`: no blocker or major finding left).
23
+ Record a design choice you had to make as `workit ledger ruling`.
24
+
25
+ Stay read-only: read and test commands only, never commits, pushes, merges or
26
+ edits. If the ledger refuses you as an author, report that instead of a verdict.
27
+
28
+ Codex runs you in a read-only sandbox. `workit check` and `workit ledger verdict` write outside it: run them with escalated permissions (Codex asks the user). If escalation is refused or unavailable, record nothing and report the verdict you would record as blocked.
29
+ '''
@@ -0,0 +1,35 @@
1
+ # Generated by Workit from packages/workit-claude-code/agents/verifier.md; edits are overwritten on the next plugin start.
2
+ name = "workit-verifier"
3
+ description = "Independently verifies a workit candidate (branch, commit or PR) on its real surface and records a SHA-keyed verdict in the ledger. Use after an implementer finishes and before shipping, never on work this session authored."
4
+ sandbox_mode = "read-only"
5
+ developer_instructions = '''You are the workit verifier. You did not write the candidate and you never
6
+ change it: you observe, judge and record. Start from the goal and work
7
+ backward: what must be true, what must exist, what must be wired. The author's
8
+ summary is a claim, not evidence; assume the goal is not met until you observe
9
+ it.
10
+
11
+ 1. Pin the candidate in its own checkout (`cd` into the worker's worktree, or
12
+ pass `--cwd`): `git rev-parse HEAD` and `git status --short`. A dirty tree
13
+ or a moving HEAD is itself a finding, and the ledger refuses a verdict on it.
14
+ 2. Read the brief: goal, acceptance (Given/When/Then), verify commands.
15
+ 3. Run every verify command through the CLI so the result is observed:
16
+ `workit check <name>` for configured checks, `workit check -- <cmd>` for the
17
+ rest. Never substitute an easier command.
18
+ 4. Drive the real surface with the project's `verify-<app>` skill when it
19
+ exists (`$workit-verify-app` generates one). Tests alone are unit evidence.
20
+ 5. Judge each acceptance line as met, not met, or not verifiable, citing what
21
+ you observed. Label every claim measured, inferred or guess.
22
+ 6. Record the verdict, then report it:
23
+ `workit ledger verdict verified|tests-verified|type-check-only|failed|blocked --branch <b> --kind live|unit --session <id> --how "<what you ran and saw>"`,
24
+ with the session the SubagentStart hook gave you (you share the lead's
25
+ `WORKIT_SESSION_ID`, which authored the work).
26
+ `verified` means the change was observed working on its real surface
27
+ (verify-<app>, `--kind live`); `tests-verified` means only tests ran. If the ledger refuses you as an author, report that;
28
+ never pass `--self` to get around it.
29
+
30
+ Report: candidate SHA, each check with its exit code, each acceptance line,
31
+ the verdict row you recorded. Do not fix anything you find: a failing check is
32
+ a verdict, not a task.
33
+
34
+ Codex runs you in a read-only sandbox. `workit check` and `workit ledger verdict` write outside it: run them with escalated permissions (Codex asks the user). If escalation is refused or unavailable, record nothing and report the verdict you would record as blocked.
35
+ '''
@@ -6621,8 +6621,9 @@ var require_dist = __commonJS(function(exports, module) {
6621
6621
  });
6622
6622
 
6623
6623
  // packages/workit-codex/scripts/launch-mcp.ts
6624
- import { existsSync as existsSync9, realpathSync as realpathSync7, statSync as statSync3 } from "node:fs";
6625
- import path21 from "node:path";
6624
+ import { existsSync as existsSync10, realpathSync as realpathSync7, statSync as statSync3 } from "node:fs";
6625
+ import path22 from "node:path";
6626
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
6626
6627
 
6627
6628
  // packages/workit-core/src/core.ts
6628
6629
  import { spawnSync as spawnSync14 } from "node:child_process";
@@ -27982,6 +27983,19 @@ var GIT_VALUE_OPTIONS = new Set([
27982
27983
  "--config-env",
27983
27984
  "--super-prefix"
27984
27985
  ]);
27986
+ var LAUNCHERS = new Set([
27987
+ "npx",
27988
+ "bunx",
27989
+ "pnpx",
27990
+ "-y",
27991
+ "--yes",
27992
+ "pnpm",
27993
+ "dlx",
27994
+ "exec",
27995
+ "env",
27996
+ "sudo",
27997
+ "command"
27998
+ ]);
27985
27999
  var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
27986
28000
  var VALUE_OPTIONS = {
27987
28001
  git: new Set([
@@ -28028,22 +28042,8 @@ var READERS = new Set([
28028
28042
  "gc",
28029
28043
  "type"
28030
28044
  ]);
28031
- var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
28032
- var LAUNCHERS = new Set([
28033
- "npx",
28034
- "bunx",
28035
- "pnpx",
28036
- "-y",
28037
- "--yes",
28038
- "pnpm",
28039
- "dlx",
28040
- "exec",
28041
- "env",
28042
- "sudo",
28043
- "command"
28044
- ]);
28045
28045
  // packages/workit-core/src/hooks/hosts/fields.ts
28046
- var SHELLS3 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
28046
+ var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
28047
28047
  var WRITE_TOOLS = new Set([
28048
28048
  "write",
28049
28049
  "edit",
@@ -28097,7 +28097,7 @@ var CODEX_DESCRIPTOR = {
28097
28097
  "subagent.stop": { support: "native", native: "SubagentStop" },
28098
28098
  "prompt.submit": { support: "native", native: "UserPromptSubmit" },
28099
28099
  "compact.pre": { support: "none", native: null },
28100
- stop: { support: "none", native: null }
28100
+ stop: { support: "native", native: "Stop" }
28101
28101
  },
28102
28102
  shellPolicy: { deny: "native", channel: "permissionDecision", failClosed: false },
28103
28103
  context: {
@@ -28117,7 +28117,7 @@ var CODEX_DESCRIPTOR = {
28117
28117
  },
28118
28118
  provenance: { sessionId: "native", agentIdOnTool: "partial", postToolObserve: "native" },
28119
28119
  interaction: { questions: "none", writeBoundary: "partial" },
28120
- stopControl: "undocumented",
28120
+ stopControl: "native",
28121
28121
  shellAvailable: "native",
28122
28122
  perEventCost: "low",
28123
28123
  capabilities: [
@@ -28183,6 +28183,7 @@ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
28183
28183
  var SOURCES2 = new Set(["startup", "resume", "clear", "compact", "fork"]);
28184
28184
 
28185
28185
  // packages/workit-core/src/hooks/hosts/cursor.ts
28186
+ var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
28186
28187
  var BLOCKING2 = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
28187
28188
 
28188
28189
  // packages/workit-core/src/hooks/run.ts
@@ -28197,6 +28198,110 @@ var codexCapabilities = (host, availability = {}) => capabilitiesFor(codexDescri
28197
28198
  if (false)
28198
28199
  ;
28199
28200
 
28201
+ // packages/workit-codex/scripts/agents.ts
28202
+ import {
28203
+ existsSync as existsSync9,
28204
+ lstatSync as lstatSync3,
28205
+ mkdirSync as mkdirSync3,
28206
+ readdirSync as readdirSync5,
28207
+ readFileSync as readFileSync7,
28208
+ renameSync as renameSync2,
28209
+ rmSync as rmSync2,
28210
+ writeFileSync as writeFileSync3
28211
+ } from "node:fs";
28212
+ import os5 from "node:os";
28213
+ import path21 from "node:path";
28214
+ var CODEX_AGENT_MARKER = "# Generated by Workit";
28215
+ var codexHomeOfInstall = (pluginRoot) => {
28216
+ const parts = path21.resolve(pluginRoot).split(path21.sep);
28217
+ const cache = parts.length - 4;
28218
+ return cache >= 2 && parts[cache] === "cache" && parts[cache - 1] === "plugins" ? parts.slice(0, cache - 1).join(path21.sep) || path21.sep : null;
28219
+ };
28220
+ var codexHomeFor = (pluginRoot, env) => codexHomeOfInstall(pluginRoot) ?? (env.CODEX_HOME || path21.join(env.HOME || os5.homedir(), ".codex"));
28221
+ var MARKER_VERSION = /^# Generated by Workit (\d+\.\d+\.\d+)\b/;
28222
+ var agentMarkerVersion = (text) => MARKER_VERSION.exec(text)?.[1] ?? null;
28223
+ var stampCodexAgent = (content, version) => version && content.startsWith(CODEX_AGENT_MARKER) && !agentMarkerVersion(content) ? `${CODEX_AGENT_MARKER} ${version}${content.slice(CODEX_AGENT_MARKER.length)}` : content;
28224
+ var versionAtLeast = (version, min) => {
28225
+ const a = version.split(".").map(Number);
28226
+ const b = min.split(".").map(Number);
28227
+ for (let i = 0;i < 3; i++)
28228
+ if (a[i] !== b[i])
28229
+ return a[i] > b[i];
28230
+ return true;
28231
+ };
28232
+ var pluginVersion = (pluginRoot) => {
28233
+ try {
28234
+ const version = JSON.parse(readFileSync7(path21.join(pluginRoot, "package.json"), "utf8")).version;
28235
+ return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version) ? version.match(/^\d+\.\d+\.\d+/)[0] : null;
28236
+ } catch {
28237
+ return null;
28238
+ }
28239
+ };
28240
+ var lstatOrNull = (file) => {
28241
+ try {
28242
+ return lstatSync3(file);
28243
+ } catch {
28244
+ return null;
28245
+ }
28246
+ };
28247
+ var installCodexAgents = (pluginRoot, codexHome) => {
28248
+ const source = path21.join(pluginRoot, "agents");
28249
+ if (!existsSync9(source))
28250
+ return [];
28251
+ const version = pluginVersion(pluginRoot);
28252
+ const target = path21.join(codexHome, "agents");
28253
+ const results = [];
28254
+ for (const file of readdirSync5(source).filter((name) => name.endsWith(".toml")).toSorted()) {
28255
+ const content = stampCodexAgent(readFileSync7(path21.join(source, file), "utf8"), version);
28256
+ const dest = path21.join(target, file);
28257
+ const stat = lstatOrNull(dest);
28258
+ if (stat?.isSymbolicLink()) {
28259
+ results.push({ file: dest, action: "skipped-symlink" });
28260
+ continue;
28261
+ }
28262
+ if (!stat) {
28263
+ mkdirSync3(target, { recursive: true });
28264
+ try {
28265
+ writeFileSync3(dest, content, { flag: "wx" });
28266
+ results.push({ file: dest, action: "installed" });
28267
+ } catch (error) {
28268
+ if (error.code !== "EEXIST")
28269
+ throw error;
28270
+ results.push({ file: dest, action: "kept-user-file" });
28271
+ }
28272
+ continue;
28273
+ }
28274
+ if (!stat.isFile()) {
28275
+ results.push({ file: dest, action: "kept-user-file" });
28276
+ continue;
28277
+ }
28278
+ const current = readFileSync7(dest, "utf8");
28279
+ if (current === content) {
28280
+ results.push({ file: dest, action: "current" });
28281
+ continue;
28282
+ }
28283
+ if (!current.startsWith(CODEX_AGENT_MARKER)) {
28284
+ results.push({ file: dest, action: "kept-user-file" });
28285
+ continue;
28286
+ }
28287
+ const written = agentMarkerVersion(current);
28288
+ if (written && (!version || !versionAtLeast(version, written))) {
28289
+ results.push({ file: dest, action: "kept-newer-version" });
28290
+ continue;
28291
+ }
28292
+ const temp = path21.join(target, `.${file}.${process.pid}.${Date.now()}.tmp`);
28293
+ writeFileSync3(temp, content, { flag: "wx" });
28294
+ try {
28295
+ renameSync2(temp, dest);
28296
+ } catch (error) {
28297
+ rmSync2(temp, { force: true });
28298
+ throw error;
28299
+ }
28300
+ results.push({ file: dest, action: "updated" });
28301
+ }
28302
+ return results;
28303
+ };
28304
+
28200
28305
  // packages/workit-codex/scripts/launch-mcp.ts
28201
28306
  var codexQualification = (host) => host === "codex_cli" ? { surface: host, cli: SUPPORT_MATRIX.codex.cli } : {
28202
28307
  surface: host,
@@ -28206,8 +28311,8 @@ var codexQualification = (host) => host === "codex_cli" ? { surface: host, cli:
28206
28311
  var resolveCodexWorkspaceRoot = (pluginRoot, env) => {
28207
28312
  const explicit = env.WORKFLOW_WORKSPACE_ROOT;
28208
28313
  const inherited = env.PWD;
28209
- const candidate = explicit ?? (inherited && path21.resolve(inherited) !== path21.resolve(pluginRoot) ? inherited : undefined);
28210
- if (!candidate || !path21.isAbsolute(candidate) || !existsSync9(candidate))
28314
+ const candidate = explicit ?? (inherited && path22.resolve(inherited) !== path22.resolve(pluginRoot) ? inherited : undefined);
28315
+ if (!candidate || !path22.isAbsolute(candidate) || !existsSync10(candidate))
28211
28316
  return null;
28212
28317
  try {
28213
28318
  if (!statSync3(candidate).isDirectory())
@@ -28246,10 +28351,33 @@ var codexContextProvider = (host = detectCodexSurface(process.env), root) => ({
28246
28351
  };
28247
28352
  }
28248
28353
  });
28249
- if (__require.main == __require.module)
28354
+ var pluginRoot = () => path22.resolve(path22.dirname(fileURLToPath3(import.meta.url)), "..");
28355
+ var syncCodexAgents = (root) => {
28356
+ const codexHome = codexHomeOfInstall(root);
28357
+ if (!codexHome)
28358
+ return;
28359
+ try {
28360
+ installCodexAgents(root, codexHome);
28361
+ } catch {}
28362
+ };
28363
+ if (__require.main == __require.module) {
28364
+ if (process.argv.includes("--install-agents")) {
28365
+ const root = pluginRoot();
28366
+ const results = installCodexAgents(root, codexHomeFor(root, process.env));
28367
+ for (const r of results)
28368
+ process.stdout.write(`${r.action} ${r.file}
28369
+ `);
28370
+ if (results.length === 0)
28371
+ process.stdout.write(`no agents bundled in ${root}
28372
+ `);
28373
+ process.exit(0);
28374
+ }
28375
+ syncCodexAgents(pluginRoot());
28250
28376
  await runStdioServer(detectCodexSurface(process.env), codexContextProvider());
28377
+ }
28251
28378
  export {
28252
28379
  codexContextProvider,
28253
28380
  codexQualification,
28254
- resolveCodexWorkspaceRoot
28381
+ resolveCodexWorkspaceRoot,
28382
+ syncCodexAgents
28255
28383
  };
@@ -17906,12 +17906,48 @@ function writeGate(cwd, paths) {
17906
17906
  return NONE2;
17907
17907
  }
17908
17908
  }
17909
+ // packages/workit-core/src/hooks/stop.ts
17910
+ import { spawnSync as spawnSync12 } from "node:child_process";
17911
+
17909
17912
  // packages/workit-core/src/hooks/raw-git.ts
17910
17913
  import { spawnSync as spawnSync11 } from "node:child_process";
17914
+ import { createHash as createHash13 } from "node:crypto";
17915
+ import fs15 from "node:fs";
17916
+ import os4 from "node:os";
17917
+ import path19 from "node:path";
17918
+
17919
+ // packages/workit-core/src/hooks/session-marker.ts
17911
17920
  import { createHash as createHash12 } from "node:crypto";
17912
17921
  import fs14 from "node:fs";
17913
- import os4 from "node:os";
17914
17922
  import path18 from "node:path";
17923
+ var sessionMarkerFile = (input, session, store) => {
17924
+ let dir = store;
17925
+ if (dir === undefined) {
17926
+ const location = resolveStore(input.cwd);
17927
+ if (location instanceof Error || !location.shared)
17928
+ return null;
17929
+ dir = location.dir;
17930
+ }
17931
+ const name = createHash12("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
17932
+ return path18.join(dir, "hooks", `skills-${name}.json`);
17933
+ };
17934
+ var list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
17935
+ var readSessionMarker = (file) => {
17936
+ try {
17937
+ const value = JSON.parse(fs14.readFileSync(file, "utf8"));
17938
+ return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
17939
+ } catch {
17940
+ return { nudged: [], loaded: [], raw: [] };
17941
+ }
17942
+ };
17943
+ var writeSessionMarker = (file, marker) => {
17944
+ try {
17945
+ fs14.mkdirSync(path18.dirname(file), { recursive: true });
17946
+ fs14.writeFileSync(file, JSON.stringify(marker));
17947
+ } catch {}
17948
+ };
17949
+
17950
+ // packages/workit-core/src/hooks/raw-git.ts
17915
17951
  var NONE3 = { kind: "none" };
17916
17952
  var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
17917
17953
  var PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
@@ -17928,12 +17964,12 @@ var GIT_VALUE_OPTIONS = new Set([
17928
17964
  "--config-env",
17929
17965
  "--super-prefix"
17930
17966
  ]);
17931
- var isAbsolute = (value) => path18.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
17967
+ var isAbsolute = (value) => path19.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
17932
17968
  var joinDir = (base, next) => {
17933
17969
  if (base === undefined || /[$`*?]/.test(next) || next === "-")
17934
17970
  return;
17935
- const expanded = next === "~" || next.startsWith("~/") ? path18.join(os4.homedir(), next.slice(1)) : next;
17936
- return base === null || isAbsolute(expanded) ? expanded : path18.join(base, expanded);
17971
+ const expanded = next === "~" || next.startsWith("~/") ? path19.join(os4.homedir(), next.slice(1)) : next;
17972
+ return base === null || isAbsolute(expanded) ? expanded : path19.join(base, expanded);
17937
17973
  };
17938
17974
  var cdTarget = (words) => {
17939
17975
  const args = words.slice(1);
@@ -18029,6 +18065,37 @@ function rawInvocations(command, dialect = "posix", depth = 0) {
18029
18065
  }
18030
18066
  return found;
18031
18067
  }
18068
+ var LAUNCHERS = new Set([
18069
+ "npx",
18070
+ "bunx",
18071
+ "pnpx",
18072
+ "-y",
18073
+ "--yes",
18074
+ "pnpm",
18075
+ "dlx",
18076
+ "exec",
18077
+ "env",
18078
+ "sudo",
18079
+ "command"
18080
+ ]);
18081
+ var WORKIT_BIN = /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/;
18082
+ function workitInvocations(command, dialect = "posix", depth = 0) {
18083
+ const found = [];
18084
+ for (const { words } of segmentsOf(command, dialect)) {
18085
+ const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
18086
+ if (at < 0)
18087
+ continue;
18088
+ if (WORKIT_BIN.test(words[at])) {
18089
+ found.push(words.slice(at + 1));
18090
+ continue;
18091
+ }
18092
+ const shell = SHELLS.has(commandName(words[at]));
18093
+ const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
18094
+ if (depth < 3 && script !== undefined)
18095
+ found.push(...workitInvocations(script, "posix", depth + 1));
18096
+ }
18097
+ return found;
18098
+ }
18032
18099
  var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
18033
18100
  var pushAction = (args) => {
18034
18101
  let force = false;
@@ -18155,7 +18222,7 @@ var repoAt = (dir) => {
18155
18222
  return { location, dir, branch: resolveTaskKey(dir, location).branch };
18156
18223
  };
18157
18224
  var isWorkitWorkspace = (repo) => {
18158
- if (fs14.existsSync(repo.location.dir))
18225
+ if (fs15.existsSync(repo.location.dir))
18159
18226
  return true;
18160
18227
  try {
18161
18228
  return resolveRuntimeWorkspaceVcs(repo.location.top ?? repo.dir) !== null;
@@ -18186,17 +18253,17 @@ var headSha2 = (repo) => {
18186
18253
  if (!git || git.reftable)
18187
18254
  return null;
18188
18255
  try {
18189
- const head = fs14.readFileSync(path18.join(git.gitDir, "HEAD"), "utf8").trim();
18256
+ const head = fs15.readFileSync(path19.join(git.gitDir, "HEAD"), "utf8").trim();
18190
18257
  if (/^[0-9a-f]{40,64}$/.test(head))
18191
18258
  return head;
18192
18259
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
18193
18260
  if (!ref)
18194
18261
  return null;
18195
- const common = path18.dirname(repo.location.dir);
18262
+ const common = path19.dirname(repo.location.dir);
18196
18263
  try {
18197
- return fs14.readFileSync(path18.join(common, ref), "utf8").trim() || null;
18264
+ return fs15.readFileSync(path19.join(common, ref), "utf8").trim() || null;
18198
18265
  } catch {}
18199
- const packed = fs14.readFileSync(path18.join(common, "packed-refs"), "utf8");
18266
+ const packed = fs15.readFileSync(path19.join(common, "packed-refs"), "utf8");
18200
18267
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
18201
18268
  } catch {
18202
18269
  return null;
@@ -18233,7 +18300,7 @@ var actionsIn = (cwd, command, dialect) => {
18233
18300
  const action = rawAction(invocation);
18234
18301
  if (!action)
18235
18302
  continue;
18236
- const dir = path18.resolve(cwd, invocation.dir ?? ".");
18303
+ const dir = path19.resolve(cwd, invocation.dir ?? ".");
18237
18304
  if (!repos.has(dir)) {
18238
18305
  const repo = repoAt(dir);
18239
18306
  repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
@@ -18251,12 +18318,31 @@ var inWorkitWorkspace = (dir) => {
18251
18318
  var rawDelivery = (cwd, command, dialect) => mentionsRawTool(command) && actionsIn(cwd, command, dialect).some(({ action }) => action.kind !== "commit" && action.kind !== "pr-read");
18252
18319
  var mentionsRawTool = (command) => /\b(?:git|gh|glab)\b/i.test(command);
18253
18320
  var dialectOf = (input) => (input.event.kind === "shell.pre" || input.event.kind === "shell.post" ? input.event.dialect : undefined) ?? "posix";
18254
- function rawGitPre(input, command) {
18321
+ var nudgeKind = (action) => action.kind === "commit" && action.amend ? "commit-amend" : action.kind === "push" && action.blindForce ? "push-force" : action.kind;
18322
+ var unseenNudges = (input, nudges, store) => {
18323
+ const session = input.session.id;
18324
+ const file = session ? sessionMarkerFile(input, session, store) : null;
18325
+ if (!file)
18326
+ return [...nudges.values()];
18327
+ const marker = readSessionMarker(file);
18328
+ const agent = input.session.agentId ? `${input.session.agentId}:` : "";
18329
+ const fresh = [...nudges.keys()].filter((kind) => !marker.raw.includes(agent + kind));
18330
+ if (fresh.length)
18331
+ writeSessionMarker(file, {
18332
+ ...marker,
18333
+ raw: [...marker.raw, ...fresh.map((kind) => agent + kind)]
18334
+ });
18335
+ return fresh.map((kind) => nudges.get(kind));
18336
+ };
18337
+ function rawGitPre(input, command, options = {}) {
18255
18338
  try {
18256
18339
  if (!mentionsRawTool(command))
18257
18340
  return NONE3;
18258
- const nudges = [];
18259
- for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input))) {
18341
+ const dialect = dialectOf(input);
18342
+ const nudges = new Map;
18343
+ let store = null;
18344
+ for (const { action, repo } of actionsIn(input.cwd, command, dialect)) {
18345
+ store ??= repo.location.dir;
18260
18346
  if (action.kind === "merge")
18261
18347
  return deny(refusal(action, null));
18262
18348
  if (action.kind === "push")
@@ -18266,10 +18352,15 @@ function rawGitPre(input, command) {
18266
18352
  return deny(refusal(action, branch));
18267
18353
  }
18268
18354
  const nudge = nudgeFor(action, input.host, input.session.id);
18269
- if (nudge && !nudges.includes(nudge))
18270
- nudges.push(nudge);
18355
+ if (nudge && !nudges.has(nudgeKind(action)))
18356
+ nudges.set(nudgeKind(action), nudge);
18271
18357
  }
18272
- return nudges.length ? { kind: "context", text: nudges.join(`
18358
+ if (options.nudge === false || !store || nudges.size === 0)
18359
+ return NONE3;
18360
+ if (workitInvocations(command, dialect).length > 0)
18361
+ return NONE3;
18362
+ const shown = unseenNudges(input, nudges, store);
18363
+ return shown.length ? { kind: "context", text: shown.join(`
18273
18364
  `) } : NONE3;
18274
18365
  } catch {
18275
18366
  return NONE3;
@@ -18278,8 +18369,8 @@ function rawGitPre(input, command) {
18278
18369
  var deny = (reason) => ({ kind: "deny", reason, unblock: null });
18279
18370
  var NEXT_COMMAND = "next";
18280
18371
  var MARKER_TTL_SECONDS = 3600;
18281
- var markerFile = (repo, session, key) => path18.join(repo.location.dir, "hooks", `commit-${createHash12("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
18282
- var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash12("sha256").update(command).digest("hex")}`;
18372
+ var markerFile = (repo, session, key) => path19.join(repo.location.dir, "hooks", `commit-${createHash13("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
18373
+ var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash13("sha256").update(command).digest("hex")}`;
18283
18374
  function noteRawCommit(input, command, key) {
18284
18375
  try {
18285
18376
  if (!input.session.id || !/\bcommit\b/.test(command))
@@ -18289,8 +18380,8 @@ function noteRawCommit(input, command, key) {
18289
18380
  if (action.kind !== "commit")
18290
18381
  continue;
18291
18382
  const file = markerFile(repo, input.session.id, key);
18292
- fs14.mkdirSync(path18.dirname(file), { recursive: true });
18293
- fs14.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
18383
+ fs15.mkdirSync(path19.dirname(file), { recursive: true });
18384
+ fs15.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
18294
18385
  `);
18295
18386
  }
18296
18387
  } catch {}
@@ -18313,11 +18404,11 @@ var headCommit = (dir) => {
18313
18404
  var settle2 = (input, repo, file) => {
18314
18405
  let marker;
18315
18406
  try {
18316
- marker = JSON.parse(fs14.readFileSync(file, "utf8"));
18407
+ marker = JSON.parse(fs15.readFileSync(file, "utf8"));
18317
18408
  } catch {
18318
18409
  return;
18319
18410
  }
18320
- fs14.rmSync(file, { force: true });
18411
+ fs15.rmSync(file, { force: true });
18321
18412
  const session = input.session.id;
18322
18413
  const now = Date.now() / 1000;
18323
18414
  if (typeof marker.at !== "number" || now - marker.at > MARKER_TTL_SECONDS)
@@ -18366,15 +18457,165 @@ function settlePendingCommit(input) {
18366
18457
  if (!repo)
18367
18458
  return;
18368
18459
  const file = markerFile(repo, input.session.id, NEXT_COMMAND);
18369
- if (fs14.existsSync(file))
18460
+ if (fs15.existsSync(file))
18370
18461
  settle2(input, repo, file);
18371
18462
  } catch {}
18372
18463
  }
18373
- // packages/workit-core/src/hooks/skill-nudge.ts
18374
- import { createHash as createHash13 } from "node:crypto";
18375
- import fs15 from "node:fs";
18376
- import path19 from "node:path";
18377
18464
 
18465
+ // packages/workit-core/src/hooks/stop.ts
18466
+ var NONE4 = { kind: "none" };
18467
+ var QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
18468
+ var OFFER = /\b(?:let me know|want me to|should I|shall I|would you like|do you want|if you(?:'d| would) like|may I|can I go ahead)\b/i;
18469
+ var YES_NO = /[[(]\s*y(?:es)?\s*\/\s*n(?:o)?\s*[\])]/i;
18470
+ var LIST_ITEM = /^\s*(?:[-*+•]|\d+[.)]|[a-z][.)])\s+/i;
18471
+ var asksUser = (message) => {
18472
+ if (!message)
18473
+ return false;
18474
+ const paragraphs = message.replace(/```[\s\S]*?(?:```|$)/g, `
18475
+
18476
+ `).split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
18477
+ while (paragraphs.length > 1 && paragraphs.at(-1).split(`
18478
+ `).every((line) => LIST_ITEM.test(line)))
18479
+ paragraphs.pop();
18480
+ const tail = paragraphs.slice(-2).join(`
18481
+ `);
18482
+ return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
18483
+ };
18484
+ var short = (sha) => (sha ?? "?").slice(0, 12);
18485
+ var gitList = (cwd, args) => {
18486
+ const run = spawnSync12("git", args, {
18487
+ cwd,
18488
+ encoding: "utf8",
18489
+ timeout: 3000,
18490
+ windowsHide: true,
18491
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" }
18492
+ });
18493
+ return run.status === 0 ? run.stdout.split(`
18494
+ `).filter(Boolean) : null;
18495
+ };
18496
+ var gitLine = (cwd, args) => gitList(cwd, args)?.[0] ?? null;
18497
+ var sessionOf = (row) => (typeof row.session === "string" ? row.session : null) ?? row.actor.session;
18498
+ var prState = (row) => typeof row.state === "string" ? row.state : "open";
18499
+ var ended = (row) => row.observer === "workit_cli" && (row.type === "pr.merged" || row.type === "pr.status" && prState(row) !== "open");
18500
+ var openPr = (rows) => {
18501
+ const seen = rows.findLast((row) => (row.type === "pr.created" || row.type === "pr.status") && row.observer === "workit_cli" && typeof row.pr === "number");
18502
+ if (!seen || ended(seen))
18503
+ return null;
18504
+ const closed = rows.some((row) => ended(row) && row.pr === seen.pr && row.seq > seen.seq);
18505
+ return closed ? null : seen.pr;
18506
+ };
18507
+ var mergedHeads = (rows) => [
18508
+ ...new Set(rows.filter((row) => row.observer === "workit_cli" && typeof row.head === "string" && (row.type === "pr.merged" || row.type === "pr.status" && prState(row) === "merged")).map((row) => row.head))
18509
+ ].slice(-3);
18510
+ var localCommit = (cwd, sha) => /^[0-9a-f]{7,64}$/.test(sha) && gitList(cwd, ["cat-file", "-e", `${sha}^{commit}`]) !== null;
18511
+ var upstreamGone = (cwd, branch) => gitLine(cwd, ["for-each-ref", "--format=%(upstream:track)", `refs/heads/${branch}`]) === "[gone]";
18512
+ var landed = (cwd, branch, rows, head, mine) => {
18513
+ if (!head)
18514
+ return false;
18515
+ const merged = mergedHeads(rows);
18516
+ if (merged.length === 0)
18517
+ return upstreamGone(cwd, branch);
18518
+ if (merged.includes(head))
18519
+ return true;
18520
+ if (mine.size === 0)
18521
+ return false;
18522
+ return merged.some((at) => {
18523
+ const after = gitList(cwd, ["rev-list", `--max-count=${UNPUSHED_SCAN}`, "HEAD", "--not", at]);
18524
+ return after !== null && !after.some((sha) => mine.has(sha));
18525
+ });
18526
+ };
18527
+ var UNPUSHED_SCAN = 500;
18528
+ var unpushed = (cwd, branch, endpoint, mine, merged) => {
18529
+ const remote = pushRemoteName(cwd, branch);
18530
+ if (!remote || mine.size === 0)
18531
+ return null;
18532
+ const listed = gitList(cwd, [
18533
+ "rev-list",
18534
+ `--max-count=${UNPUSHED_SCAN}`,
18535
+ "HEAD",
18536
+ "--not",
18537
+ `--remotes=${remote}`,
18538
+ ...merged.filter((sha) => localCommit(cwd, sha))
18539
+ ]);
18540
+ const count = (listed ?? []).filter((sha) => mine.has(sha)).length;
18541
+ if (count === 0)
18542
+ return null;
18543
+ return {
18544
+ kind: "unpushed",
18545
+ reason: `${branch} has ${count} commit(s) of this session not on ${remote}, and this workspace's endpoint is \`${endpoint}\` (\`workit grant show\`): push them with \`workit git push\`${endpoint === "pr" ? ", then open or update the PR (`workit pr create --fill`)" : ""}.`
18546
+ };
18547
+ };
18548
+ var redChecks = (rows, pr, head, endpoint) => {
18549
+ const status = rows.findLast((row) => row.type === "pr.status" && row.observer === "workit_cli" && row.pr === pr);
18550
+ const state = status && status.head === head && prState(status) === "open" ? status.checks : null;
18551
+ if (state !== "failing" && state !== "pending")
18552
+ return null;
18553
+ return {
18554
+ kind: "checks",
18555
+ reason: `PR #${pr}'s checks were last seen ${state} at ${short(head)}, and this workspace's endpoint is \`${endpoint}\`: run \`workit ci wait --pr ${pr}\`${state === "failing" ? " after fixing the failures `workit pr status` lists (then push)" : ""}.`
18556
+ };
18557
+ };
18558
+ var unverified = (cwd, branch, all, descriptor) => {
18559
+ const check = checkVerdicts(cwd, branch, all);
18560
+ if (check.accepted.accepted || check.accepted.reasons.includes("failing_verdict"))
18561
+ return null;
18562
+ const has = check.review === "self-reviewed" ? "only a self verdict" : "no current verdict";
18563
+ const verifier = `${descriptor.subagents.agentPrefix}verifier`;
18564
+ return {
18565
+ kind: "verdict",
18566
+ reason: `the head ${short(check.head)} of ${branch}, which this session delivered, has ${has}: if no verifier is already running on it, hand it to a session that did not author it (the ${verifier} agent records \`workit ledger verdict <result> --branch ${branch} --how "<evidence>"\`), or tell the user it is unverified. Never record a verdict on your own work.`
18567
+ };
18568
+ };
18569
+ function stopObligation(input, descriptor) {
18570
+ try {
18571
+ const session = input.session.id;
18572
+ if (!session || input.session.agentId || !inWorkitWorkspace(input.cwd))
18573
+ return null;
18574
+ const branch = currentBranch(input.cwd);
18575
+ if (!branch || guardedBranch(input.cwd, branch))
18576
+ return null;
18577
+ const ledger = readLedger(input.cwd);
18578
+ if (!ledger.ok)
18579
+ return null;
18580
+ const all = ledger.value.rows;
18581
+ const rows = all.filter((row) => row.branch === branch);
18582
+ const mine = rows.filter((row) => sessionOf(row) === session);
18583
+ const commits = new Set(mine.filter((row) => row.type === "commit.recorded").map((row) => typeof row.sha === "string" ? row.sha : row.head).filter((sha) => typeof sha === "string"));
18584
+ const delivered = mine.some((row) => row.observer === "workit_cli" && (row.type === "pr.created" || row.type === "delivery.verified"));
18585
+ if (commits.size === 0 && !delivered)
18586
+ return null;
18587
+ const head = gitLine(input.cwd, ["rev-parse", "HEAD"]);
18588
+ if (landed(input.cwd, branch, rows, head, commits))
18589
+ return null;
18590
+ const endpoint = resolveAutonomy(input.cwd).effectiveEndpoint;
18591
+ if (endpoint !== "commit") {
18592
+ const left = unpushed(input.cwd, branch, endpoint, commits, mergedHeads(rows));
18593
+ if (left)
18594
+ return left;
18595
+ }
18596
+ const pr = openPr(rows);
18597
+ if ((endpoint === "green" || endpoint === "merged") && pr !== null && head) {
18598
+ const left = redChecks(rows, pr, head, endpoint);
18599
+ if (left)
18600
+ return left;
18601
+ }
18602
+ return delivered ? unverified(input.cwd, branch, all, descriptor) : null;
18603
+ } catch {
18604
+ return null;
18605
+ }
18606
+ }
18607
+ function stopDecision(input, descriptor) {
18608
+ const event = input.event;
18609
+ if (event.kind !== "stop" || event.stopHookActive)
18610
+ return NONE4;
18611
+ if (!event.lastMessage?.trim() || asksUser(event.lastMessage))
18612
+ return NONE4;
18613
+ const left = stopObligation(input, descriptor);
18614
+ return left ? {
18615
+ kind: "continue",
18616
+ reason: `Workit: before stopping, ${left.reason} If this cannot be done now, say so to the user and stop.`
18617
+ } : NONE4;
18618
+ }
18378
18619
  // packages/workit-core/src/core/skill-manifests.ts
18379
18620
  var WORKIT_METHOD_SKILLS = [
18380
18621
  "workit-shape",
@@ -18481,27 +18722,6 @@ var promptTrigger = (prompt) => {
18481
18722
  }
18482
18723
  return null;
18483
18724
  };
18484
- var markerFile2 = (input, session) => {
18485
- const location = resolveStore(input.cwd);
18486
- if (location instanceof Error || !location.shared)
18487
- return null;
18488
- const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
18489
- return path19.join(location.dir, "hooks", `skills-${name}.json`);
18490
- };
18491
- var readMarker = (file) => {
18492
- try {
18493
- const value = JSON.parse(fs15.readFileSync(file, "utf8"));
18494
- return { nudged: value.nudged ?? [], loaded: value.loaded ?? [] };
18495
- } catch {
18496
- return { nudged: [], loaded: [] };
18497
- }
18498
- };
18499
- var writeMarker = (file, marker) => {
18500
- try {
18501
- fs15.mkdirSync(path19.dirname(file), { recursive: true });
18502
- fs15.writeFileSync(file, JSON.stringify(marker));
18503
- } catch {}
18504
- };
18505
18725
  var loadedSkills = (cwd, session) => {
18506
18726
  const ledger = readLedger(cwd);
18507
18727
  const loaded = new Set;
@@ -18549,13 +18769,13 @@ function recordSkillLoad(input, name, via) {
18549
18769
  return;
18550
18770
  if (!inWorkitWorkspace(input.cwd))
18551
18771
  return;
18552
- const file = markerFile2(input, session);
18772
+ const file = sessionMarkerFile(input, session);
18553
18773
  if (!file)
18554
18774
  return;
18555
- const marker = readMarker(file);
18775
+ const marker = readSessionMarker(file);
18556
18776
  if (marker.loaded.includes(skill))
18557
18777
  return;
18558
- writeMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
18778
+ writeSessionMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
18559
18779
  if (loadedSkills(input.cwd, session).has(skill))
18560
18780
  return;
18561
18781
  appendHookObserved(input.cwd, {
@@ -18567,23 +18787,24 @@ function recordSkillLoad(input, name, via) {
18567
18787
  } catch {}
18568
18788
  }
18569
18789
  var nudgeOnce = (input, session, skill, line) => {
18570
- const file = markerFile2(input, session);
18790
+ const file = sessionMarkerFile(input, session);
18571
18791
  if (!file)
18572
18792
  return null;
18573
- const marker = readMarker(file);
18793
+ const marker = readSessionMarker(file);
18574
18794
  if (marker.nudged.includes(skill) || marker.loaded.includes(skill))
18575
18795
  return null;
18576
18796
  if (loadedSkills(input.cwd, session).has(skill))
18577
18797
  return null;
18578
- writeMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
18798
+ writeSessionMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
18579
18799
  return line;
18580
18800
  };
18581
18801
  var nudgeText = (input, skill, why) => USER_INVOKED.has(skill) ? `Workit: ${why} looks like ${skill} work, which the user starts; offer it (/${skill.replace(/^workit-/, "wk-")}) rather than loading it.` : `Workit: ${why} looks like ${skill} work; if it is, ${skillLoadWording(SKILL_HOST[input.host], skill)} first.`;
18582
18802
  var promptLoads = (prompt) => /^\s*\/(?:skill:)?([\w:-]+)/.exec(prompt)?.[1] ?? /^\s*<skill name="([\w:-]+)"/.exec(prompt)?.[1] ?? /^\s*Load the (workit-[a-z-]+) skill with the skill tool\b/.exec(prompt)?.[1] ?? null;
18803
+ var relayedMessage = (prompt) => /^\s*(?:Another Claude session sent a message\b|<agent-message\s+from=|\[SYSTEM NOTIFICATION\b|<task-notification>)/.test(prompt);
18583
18804
  function promptNudge(input, prompt) {
18584
18805
  try {
18585
18806
  const session = input.session.id;
18586
- if (!prompt || !session || input.session.agentId)
18807
+ if (!prompt || !session || input.session.agentId || relayedMessage(prompt))
18587
18808
  return null;
18588
18809
  const loads = promptLoads(prompt);
18589
18810
  if (loads) {
@@ -18599,32 +18820,7 @@ function promptNudge(input, prompt) {
18599
18820
  }
18600
18821
  }
18601
18822
  var WORKIT_DELIVERY = /^(?:pr (?:create|merge)|ci wait|git push)\b/;
18602
- var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
18603
- var LAUNCHERS = new Set([
18604
- "npx",
18605
- "bunx",
18606
- "pnpx",
18607
- "-y",
18608
- "--yes",
18609
- "pnpm",
18610
- "dlx",
18611
- "exec",
18612
- "env",
18613
- "sudo",
18614
- "command"
18615
- ]);
18616
- var runsWorkitDelivery = (command, dialect, depth = 0) => segmentsOf(command, dialect).some(({ words }) => {
18617
- const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
18618
- if (at >= 0 && /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/.test(words[at])) {
18619
- const args = words.slice(at + 1);
18620
- if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run"))
18621
- return false;
18622
- return WORKIT_DELIVERY.test(args.join(" "));
18623
- }
18624
- const shell = SHELLS2.has(words[at]?.split(/[\\/]/).at(-1) ?? "");
18625
- const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
18626
- return depth < 3 && script !== undefined && runsWorkitDelivery(script, "posix", depth + 1);
18627
- });
18823
+ var runsWorkitDelivery = (command, dialect) => workitInvocations(command, dialect).some((args) => !args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run") && WORKIT_DELIVERY.test(args.join(" ")));
18628
18824
  var isDeliveryCommand = (cwd, command, dialect = "posix") => rawDelivery(cwd, command, dialect) || runsWorkitDelivery(command, dialect) && inWorkitWorkspace(cwd);
18629
18825
  function shipNudge(input, command) {
18630
18826
  try {
@@ -18664,7 +18860,7 @@ var existingDirectory = (value) => {
18664
18860
  return null;
18665
18861
  }
18666
18862
  };
18667
- var SHELLS3 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
18863
+ var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
18668
18864
  var SCRIPT_FLAG2 = /^(?:-\w*c|-command|\/c)$/i;
18669
18865
  var quoteArg = (arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : JSON.stringify(arg);
18670
18866
  var commandText = (value) => {
@@ -18676,7 +18872,7 @@ var commandText = (value) => {
18676
18872
  if (!argv.every((arg) => typeof arg === "string"))
18677
18873
  return null;
18678
18874
  const shell = argv[0].split(/[\\/]/).at(-1)?.replace(/\.exe$/i, "").toLowerCase() ?? "";
18679
- if (SHELLS3.has(shell))
18875
+ if (SHELLS2.has(shell))
18680
18876
  for (let index = 1;index < argv.length - 1; index++) {
18681
18877
  if (SCRIPT_FLAG2.test(argv[index]))
18682
18878
  return nonEmpty2(argv[index + 1]) ? argv[index + 1] : null;
@@ -18724,7 +18920,7 @@ var writeTargets = (input) => {
18724
18920
  };
18725
18921
  // packages/workit-core/src/hooks/handle.ts
18726
18922
  var usable2 = (value) => value === "native" || value === "partial";
18727
- var NONE4 = { kind: "none" };
18923
+ var NONE5 = { kind: "none" };
18728
18924
  var resendsOnChange = (descriptor) => descriptor.context.turnResend === "on-change" && usable2(descriptor.context.perTurn);
18729
18925
  var offered = new Set;
18730
18926
  var ROLES = {
@@ -18738,13 +18934,20 @@ var workitRole = (descriptor, agentType) => {
18738
18934
  return Object.hasOwn(ROLES, name) ? ROLES[name] : null;
18739
18935
  };
18740
18936
  var subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128);
18741
- var subagentStartText = (session, descriptor, event) => {
18937
+ var CLAUDE_WORKTREE = /[\\/]\.claude[\\/]worktrees[\\/]/;
18938
+ var ISOLATED_GIT = 'Run git as plain, separate commands inside your worktree (`git switch -c <branch> <base>`, `git commit --trailer "Workit-Session=<your session>"`, `git push`; `workit doctor --fix` can install a commit-msg hook that adds the trailer), never `workit git …`: Claude Code\'s worktree isolation refuses a command that wraps git and cannot be turned off (https://code.claude.com/docs/en/worktrees#how-claude-code-enforces-isolation). Never dodge that check by renaming or re-wrapping git. Use workit for the non-git verbs (`workit check`, `workit ledger`, `workit pr`).';
18939
+ var subagentStartText = (input, descriptor, event) => {
18940
+ const session = input.session.id ?? null;
18742
18941
  const role = workitRole(descriptor, event.agentType);
18743
18942
  const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
18744
- if (role === "implementer") {
18745
- const where = descriptor.subagents.worktreeIsolation === "native" ? "working in its own git worktree: it may edit and commit there" : "as an implementer: it edits and commits only in its own git worktree (the lead makes one with `workit fanout worktree create <slice>`), never in the lead's checkout";
18746
- return `Workit observed ${who} ${where}, within its brief's scope. Before the first commit, switch to a policy-compliant branch with \`workit git branch <branch> --base <base>\` (e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
18747
- }
18943
+ const isolated = descriptor.subagents.worktreeIsolation === "native" && (role === "implementer" || CLAUDE_WORKTREE.test(input.cwd));
18944
+ if (role === "implementer" && isolated)
18945
+ return `Workit observed ${who} working in its own git worktree: it may edit and commit there, within its brief's scope. ${ISOLATED_GIT} Use a policy-compliant branch (e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
18946
+ if (isolated)
18947
+ return `${ISOLATED_GIT}
18948
+ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
18949
+ if (role === "implementer")
18950
+ return `Workit observed ${who} as an implementer: it edits and commits only in its own git worktree (the lead makes one with \`workit fanout worktree create <slice>\`), never in the lead's checkout, within its brief's scope. Before the first commit, switch to a policy-compliant branch with \`workit git branch <branch> --base <base>\` (e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
18748
18951
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
18749
18952
  if (role !== "judge" || !event.agentId)
18750
18953
  return readOnly;
@@ -18757,7 +18960,7 @@ function handleHook(input, deps) {
18757
18960
  switch (event.kind) {
18758
18961
  case "session.start": {
18759
18962
  if (!usable2(descriptor.context.sessionStart))
18760
- return NONE4;
18963
+ return NONE5;
18761
18964
  const key = input.session.id ? `${input.host}\x00${input.session.id}` : null;
18762
18965
  const offer = event.source === "startup" && (key === null || !offered.has(key));
18763
18966
  if (offer && key !== null)
@@ -18772,42 +18975,44 @@ function handleHook(input, deps) {
18772
18975
  }
18773
18976
  case "context.turn": {
18774
18977
  if (!usable2(descriptor.context.perTurn))
18775
- return NONE4;
18978
+ return NONE5;
18776
18979
  const current = turnContextText(input, descriptor);
18777
18980
  const text = resendsOnChange(descriptor) ? changedTurnContext(input, current) : current;
18778
- return withContextLine(text ? { kind: "context", text } : NONE4, promptNudge(input, event.prompt));
18981
+ return withContextLine(text ? { kind: "context", text } : NONE5, promptNudge(input, event.prompt));
18779
18982
  }
18780
18983
  case "shell.pre": {
18781
18984
  const skillRead = skillReadIn(event.command, event.dialect);
18782
18985
  if (skillRead)
18783
18986
  recordSkillLoad(input, skillRead, "read");
18784
18987
  const canDeny = usable2(descriptor.shellPolicy.deny);
18785
- const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE4;
18988
+ const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE5;
18786
18989
  if (policy.kind !== "none")
18787
18990
  return policy;
18788
18991
  const postTool = usable2(descriptor.events["shell.post"].support);
18789
18992
  if (!postTool)
18790
18993
  settlePendingCommit(input);
18791
- const raw = canDeny ? rawGitPre(input, event.command) : NONE4;
18994
+ const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
18792
18995
  if (raw.kind === "deny")
18793
18996
  return raw;
18794
18997
  const writes = usable2(descriptor.events["write.pre"].support) ? shellWrites(event.command) : { writes: false, targets: null };
18795
- const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE4;
18998
+ const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE5;
18796
18999
  if (gate.kind === "deny")
18797
19000
  return gate;
18798
19001
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
18799
- return withContextLine(raw, shipNudge(input, event.command));
19002
+ if (descriptor.transport !== "hook-process")
19003
+ return NONE5;
19004
+ return withContextLine(canDeny ? rawGitPre(input, event.command) : NONE5, shipNudge(input, event.command));
18800
19005
  }
18801
19006
  case "shell.post":
18802
19007
  if (usable2(descriptor.events["shell.post"].support))
18803
19008
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
18804
- return NONE4;
19009
+ return NONE5;
18805
19010
  case "write.pre":
18806
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE4;
19011
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE5;
18807
19012
  case "subagent.start":
18808
19013
  return {
18809
19014
  kind: "context",
18810
- text: subagentStartText(input.session.id ?? null, descriptor, event)
19015
+ text: subagentStartText(input, descriptor, event)
18811
19016
  };
18812
19017
  case "compact.pre":
18813
19018
  return {
@@ -18817,19 +19022,20 @@ function handleHook(input, deps) {
18817
19022
  case "tool.pre":
18818
19023
  if (event.skill)
18819
19024
  recordSkillLoad(input, event.skill, "tool");
18820
- return NONE4;
19025
+ return NONE5;
19026
+ case "stop":
19027
+ return usable2(descriptor.events.stop.support) ? stopDecision(input, descriptor) : NONE5;
18821
19028
  case "subagent.stop":
18822
19029
  case "prompt.submit":
18823
- case "stop":
18824
- return NONE4;
19030
+ return NONE5;
18825
19031
  }
18826
19032
  }
18827
19033
  var failureDecision = (descriptor, event, error) => {
18828
19034
  if (event === "shell.pre" || event === "tool.pre" || event === "write.pre")
18829
- return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE4;
19035
+ return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE5;
18830
19036
  if (event === "session.start" || event === "subagent.start")
18831
19037
  return { kind: "context", text: `[workit diagnostic: ${error}]` };
18832
- return NONE4;
19038
+ return NONE5;
18833
19039
  };
18834
19040
  var dispatchHook = (adapter, raw, env = process.env) => {
18835
19041
  const parsed = adapter.parse(raw, env);
@@ -18871,7 +19077,7 @@ var CLAUDE_CODE_DESCRIPTOR = {
18871
19077
  "prompt.submit": { support: "native", native: "UserPromptSubmit" },
18872
19078
  "subagent.stop": { support: "none", native: null },
18873
19079
  "compact.pre": { support: "none", native: null },
18874
- stop: { support: "none", native: null }
19080
+ stop: { support: "native", native: "Stop" }
18875
19081
  },
18876
19082
  shellPolicy: { deny: "native", channel: "permissionDecision", failClosed: false },
18877
19083
  context: {
@@ -19137,7 +19343,7 @@ var CODEX_DESCRIPTOR = {
19137
19343
  "subagent.stop": { support: "native", native: "SubagentStop" },
19138
19344
  "prompt.submit": { support: "native", native: "UserPromptSubmit" },
19139
19345
  "compact.pre": { support: "none", native: null },
19140
- stop: { support: "none", native: null }
19346
+ stop: { support: "native", native: "Stop" }
19141
19347
  },
19142
19348
  shellPolicy: { deny: "native", channel: "permissionDecision", failClosed: false },
19143
19349
  context: {
@@ -19157,7 +19363,7 @@ var CODEX_DESCRIPTOR = {
19157
19363
  },
19158
19364
  provenance: { sessionId: "native", agentIdOnTool: "partial", postToolObserve: "native" },
19159
19365
  interaction: { questions: "none", writeBoundary: "partial" },
19160
- stopControl: "undocumented",
19366
+ stopControl: "native",
19161
19367
  shellAvailable: "native",
19162
19368
  perEventCost: "low",
19163
19369
  capabilities: [
@@ -19224,7 +19430,8 @@ var EVENTS2 = {
19224
19430
  PreToolUse: "shell.pre",
19225
19431
  PostToolUse: "shell.post",
19226
19432
  SubagentStart: "subagent.start",
19227
- SubagentStop: "subagent.stop"
19433
+ SubagentStop: "subagent.stop",
19434
+ Stop: "stop"
19228
19435
  };
19229
19436
  var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
19230
19437
  var isShellTool = (name) => SHELL_TOOLS2.has(String(name).toLowerCase());
@@ -19263,6 +19470,10 @@ var parseCodexHookInput = (value) => {
19263
19470
  ...nonEmpty2(value.tool_use_id) ? { tool_use_id: value.tool_use_id } : {},
19264
19471
  ...nonEmpty2(value.agent_id) ? { agent_id: value.agent_id } : {},
19265
19472
  ...nonEmpty2(value.agent_type) ? { agent_type: value.agent_type } : {},
19473
+ ...event === "Stop" ? {
19474
+ last_assistant_message: optionalText(value.last_assistant_message),
19475
+ stop_hook_active: value.stop_hook_active === true
19476
+ } : {},
19266
19477
  ...event === "SubagentStop" ? {
19267
19478
  agent_transcript_path: optionalText(value.agent_transcript_path),
19268
19479
  last_assistant_message: optionalText(value.last_assistant_message),
@@ -19315,13 +19526,21 @@ var protocolEvent = (input) => {
19315
19526
  lastMessage: input.last_assistant_message ?? null,
19316
19527
  stopHookActive: input.stop_hook_active ?? false
19317
19528
  };
19529
+ case "Stop":
19530
+ return {
19531
+ kind: "stop",
19532
+ lastMessage: input.last_assistant_message ?? null,
19533
+ stopHookActive: input.stop_hook_active ?? false
19534
+ };
19318
19535
  }
19319
19536
  };
19320
19537
  var output = (event, extra = {}) => ({
19321
19538
  hookSpecificOutput: { hookEventName: event, ...extra }
19322
19539
  });
19323
19540
  var render2 = (decision, native) => {
19324
- if (native === "SubagentStop" || native === null)
19541
+ if (native === "Stop" && decision.kind === "continue")
19542
+ return { json: { decision: "block", reason: decision.reason }, exitCode: 0 };
19543
+ if (native === "SubagentStop" || native === "Stop" || native === null)
19325
19544
  return { json: {}, exitCode: 0 };
19326
19545
  if (decision.kind === "deny")
19327
19546
  return {
@@ -19368,6 +19587,7 @@ var codexAdapter = {
19368
19587
  };
19369
19588
 
19370
19589
  // packages/workit-core/src/hooks/hosts/cursor.ts
19590
+ import fs16 from "node:fs";
19371
19591
  import path21 from "node:path";
19372
19592
  var undocumented = { support: "undocumented", native: null };
19373
19593
  var none = { support: "none", native: null };
@@ -19388,7 +19608,7 @@ var CURSOR_DESCRIPTOR = {
19388
19608
  "subagent.stop": { support: "partial", native: "subagentStop" },
19389
19609
  "prompt.submit": undocumented,
19390
19610
  "compact.pre": { support: "partial", native: "preCompact" },
19391
- stop: none
19611
+ stop: { support: "native", native: "stop" }
19392
19612
  },
19393
19613
  shellPolicy: { deny: "native", channel: "exit2+json", failClosed: true },
19394
19614
  context: {
@@ -19412,7 +19632,7 @@ var CURSOR_DESCRIPTOR = {
19412
19632
  postToolObserve: "undocumented"
19413
19633
  },
19414
19634
  interaction: { questions: "none", writeBoundary: "partial" },
19415
- stopControl: "undocumented",
19635
+ stopControl: "native",
19416
19636
  shellAvailable: "native",
19417
19637
  perEventCost: "low",
19418
19638
  capabilities: [
@@ -19479,7 +19699,8 @@ var EVENTS3 = {
19479
19699
  beforeShellExecution: "shell.pre",
19480
19700
  subagentStart: "subagent.start",
19481
19701
  subagentStop: "subagent.stop",
19482
- preCompact: "compact.pre"
19702
+ preCompact: "compact.pre",
19703
+ stop: "stop"
19483
19704
  };
19484
19705
  var parseCursorHookInput = (value) => {
19485
19706
  if (!isRecord4(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
@@ -19515,7 +19736,12 @@ var parseCursorHookInput = (value) => {
19515
19736
  ...event === "subagentStart" && nonEmpty2(value.subagent_id) ? { subagent_id: value.subagent_id } : {},
19516
19737
  ...event === "subagentStart" && nonEmpty2(value.subagent_type) ? { subagent_type: value.subagent_type } : {},
19517
19738
  ...event === "subagentStart" && nonEmpty2(value.parent_conversation_id) ? { parent_conversation_id: value.parent_conversation_id } : {},
19518
- ...event === "subagentStart" && nonEmpty2(value.task) ? { task: value.task } : {}
19739
+ ...event === "subagentStart" && nonEmpty2(value.task) ? { task: value.task } : {},
19740
+ ...event === "stop" ? {
19741
+ ...nonEmpty2(value.status) ? { status: value.status } : {},
19742
+ ...typeof value.loop_count === "number" ? { loop_count: value.loop_count } : {},
19743
+ ...nonEmpty2(value.transcript_path) ? { transcript_path: value.transcript_path } : {}
19744
+ } : {}
19519
19745
  }
19520
19746
  };
19521
19747
  };
@@ -19549,6 +19775,51 @@ var protocolEvent2 = (input) => {
19549
19775
  };
19550
19776
  case "preCompact":
19551
19777
  return { kind: "compact.pre", trigger: "auto" };
19778
+ case "stop":
19779
+ return {
19780
+ kind: "stop",
19781
+ lastMessage: (input.status ?? "completed") === "completed" ? lastAssistantText(input.transcript_path) : null,
19782
+ stopHookActive: (input.loop_count ?? 0) > 0
19783
+ };
19784
+ }
19785
+ };
19786
+ var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
19787
+ var lastAssistantText = (file) => {
19788
+ if (!file || !path21.isAbsolute(file))
19789
+ return null;
19790
+ try {
19791
+ const size = fs16.statSync(file).size;
19792
+ const start = Math.max(0, size - TRANSCRIPT_TAIL_BYTES);
19793
+ const buffer = Buffer.alloc(size - start);
19794
+ const fd = fs16.openSync(file, "r");
19795
+ try {
19796
+ fs16.readSync(fd, buffer, 0, buffer.length, start);
19797
+ } finally {
19798
+ fs16.closeSync(fd);
19799
+ }
19800
+ const lines = buffer.toString("utf8").split(`
19801
+ `);
19802
+ for (let index = lines.length - 1;index >= 0; index--) {
19803
+ let entry;
19804
+ try {
19805
+ entry = JSON.parse(lines[index]);
19806
+ } catch {
19807
+ continue;
19808
+ }
19809
+ if (!isRecord4(entry) || entry.role !== "assistant")
19810
+ continue;
19811
+ const content = isRecord4(entry.message) ? entry.message.content : entry.content;
19812
+ if (typeof content === "string")
19813
+ return content;
19814
+ if (!Array.isArray(content))
19815
+ return null;
19816
+ const text = content.filter((part) => isRecord4(part) && part.type === "text" && typeof part.text === "string").map((part) => part.text).join(`
19817
+ `);
19818
+ return text || null;
19819
+ }
19820
+ return null;
19821
+ } catch {
19822
+ return null;
19552
19823
  }
19553
19824
  };
19554
19825
  var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path21.sep) ? root : `${root}${path21.sep}`);
@@ -19580,6 +19851,8 @@ var render3 = (decision, native) => {
19580
19851
  return { json: { additional_context: decision.text }, exitCode: 0 };
19581
19852
  if (decision.kind === "notice")
19582
19853
  return { json: { user_message: decision.userMessage }, exitCode: 0 };
19854
+ if (decision.kind === "continue" && native === "stop")
19855
+ return { json: { followup_message: decision.reason }, exitCode: 0 };
19583
19856
  return {
19584
19857
  json: native !== null && BLOCKING2.has(native) ? { permission: "allow" } : {},
19585
19858
  exitCode: 0
@@ -37,6 +37,6 @@ if (quiet)
37
37
  process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: quiet } })}
38
38
  `);
39
39
  else {
40
- const { runCodexHook } = await import("./workit-hook-86r77dqq.js");
40
+ const { runCodexHook } = await import("./workit-hook-aw8j630z.js");
41
41
  await runCodexHook([payload]);
42
42
  }
package/hooks/hooks.json CHANGED
@@ -29,6 +29,13 @@
29
29
  "hooks": [{ "type": "command", "command": "node ${PLUGIN_ROOT}/dist/workit-hook.js" }]
30
30
  }
31
31
  ],
32
+ "Stop": [
33
+ {
34
+ "hooks": [
35
+ { "type": "command", "command": "node ${PLUGIN_ROOT}/dist/workit-hook.js", "timeout": 10 }
36
+ ]
37
+ }
38
+ ],
32
39
  "SubagentStop": [
33
40
  {
34
41
  "matcher": "*",
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-codex",
3
- "version": "8.6.1",
3
+ "version": "8.8.0",
4
4
  "private": false,
5
- "description": "Workit Codex CLI/desktop plugin — shared MCP transport, hooks, and sixteen method skills",
5
+ "description": "Workit Codex CLI/desktop plugin — shared MCP transport, hooks, method skills, and verifier/reviewer/implementer agents",
6
6
  "keywords": [
7
7
  "agentic-coding",
8
8
  "ai-agents",
@@ -27,7 +27,8 @@
27
27
  ".codex-plugin/",
28
28
  ".mcp.json",
29
29
  "hooks/hooks.json",
30
- "skills/"
30
+ "skills/",
31
+ "agents/"
31
32
  ],
32
33
  "type": "module",
33
34
  "main": "./dist/launch-mcp.js",
@@ -38,8 +39,8 @@
38
39
  "build": "bun scripts/build.ts"
39
40
  },
40
41
  "dependencies": {
41
- "@brainervirus/workit-core": "^8.6.1",
42
- "@brainervirus/workit-mcp": "^8.6.1"
42
+ "@brainervirus/workit-core": "^8.8.0",
43
+ "@brainervirus/workit-mcp": "^8.8.0"
43
44
  },
44
45
  "engines": {
45
46
  "node": ">=24"