@brainervirus/workit-cursor 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.
@@ -17874,12 +17874,43 @@ var init_write_gate = __esm(() => {
17874
17874
  cache2 = new Map;
17875
17875
  });
17876
17876
 
17877
- // packages/workit-core/src/hooks/raw-git.ts
17878
- import { spawnSync as spawnSync11 } from "node:child_process";
17877
+ // packages/workit-core/src/hooks/session-marker.ts
17879
17878
  import { createHash as createHash12 } from "node:crypto";
17880
17879
  import fs14 from "node:fs";
17881
- import os4 from "node:os";
17882
17880
  import path18 from "node:path";
17881
+ var sessionMarkerFile = (input, session, store) => {
17882
+ let dir = store;
17883
+ if (dir === undefined) {
17884
+ const location = resolveStore(input.cwd);
17885
+ if (location instanceof Error || !location.shared)
17886
+ return null;
17887
+ dir = location.dir;
17888
+ }
17889
+ const name = createHash12("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
17890
+ return path18.join(dir, "hooks", `skills-${name}.json`);
17891
+ }, list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [], readSessionMarker = (file) => {
17892
+ try {
17893
+ const value = JSON.parse(fs14.readFileSync(file, "utf8"));
17894
+ return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
17895
+ } catch {
17896
+ return { nudged: [], loaded: [], raw: [] };
17897
+ }
17898
+ }, writeSessionMarker = (file, marker) => {
17899
+ try {
17900
+ fs14.mkdirSync(path18.dirname(file), { recursive: true });
17901
+ fs14.writeFileSync(file, JSON.stringify(marker));
17902
+ } catch {}
17903
+ };
17904
+ var init_session_marker = __esm(() => {
17905
+ init_paths();
17906
+ });
17907
+
17908
+ // packages/workit-core/src/hooks/raw-git.ts
17909
+ import { spawnSync as spawnSync11 } from "node:child_process";
17910
+ import { createHash as createHash13 } from "node:crypto";
17911
+ import fs15 from "node:fs";
17912
+ import os4 from "node:os";
17913
+ import path19 from "node:path";
17883
17914
  function rawInvocations(command, dialect = "posix", depth = 0) {
17884
17915
  const found = [];
17885
17916
  let dir = null;
@@ -17945,6 +17976,23 @@ function rawInvocations(command, dialect = "posix", depth = 0) {
17945
17976
  }
17946
17977
  return found;
17947
17978
  }
17979
+ function workitInvocations(command, dialect = "posix", depth = 0) {
17980
+ const found = [];
17981
+ for (const { words } of segmentsOf(command, dialect)) {
17982
+ const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
17983
+ if (at < 0)
17984
+ continue;
17985
+ if (WORKIT_BIN.test(words[at])) {
17986
+ found.push(words.slice(at + 1));
17987
+ continue;
17988
+ }
17989
+ const shell = SHELLS.has(commandName(words[at]));
17990
+ const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
17991
+ if (depth < 3 && script !== undefined)
17992
+ found.push(...workitInvocations(script, "posix", depth + 1));
17993
+ }
17994
+ return found;
17995
+ }
17948
17996
  function rawAction(invocation) {
17949
17997
  const args = invocation.args;
17950
17998
  if (asksHelp(invocation))
@@ -17969,12 +18017,15 @@ function rawAction(invocation) {
17969
18017
  return { kind: "pr-read", forge };
17970
18018
  return null;
17971
18019
  }
17972
- function rawGitPre(input, command) {
18020
+ function rawGitPre(input, command, options = {}) {
17973
18021
  try {
17974
18022
  if (!mentionsRawTool(command))
17975
18023
  return NONE3;
17976
- const nudges = [];
17977
- for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input))) {
18024
+ const dialect = dialectOf(input);
18025
+ const nudges = new Map;
18026
+ let store = null;
18027
+ for (const { action, repo } of actionsIn(input.cwd, command, dialect)) {
18028
+ store ??= repo.location.dir;
17978
18029
  if (action.kind === "merge")
17979
18030
  return deny(refusal(action, null));
17980
18031
  if (action.kind === "push")
@@ -17984,10 +18035,15 @@ function rawGitPre(input, command) {
17984
18035
  return deny(refusal(action, branch));
17985
18036
  }
17986
18037
  const nudge = nudgeFor(action, input.host, input.session.id);
17987
- if (nudge && !nudges.includes(nudge))
17988
- nudges.push(nudge);
18038
+ if (nudge && !nudges.has(nudgeKind(action)))
18039
+ nudges.set(nudgeKind(action), nudge);
17989
18040
  }
17990
- return nudges.length ? { kind: "context", text: nudges.join(`
18041
+ if (options.nudge === false || !store || nudges.size === 0)
18042
+ return NONE3;
18043
+ if (workitInvocations(command, dialect).length > 0)
18044
+ return NONE3;
18045
+ const shown = unseenNudges(input, nudges, store);
18046
+ return shown.length ? { kind: "context", text: shown.join(`
17991
18047
  `) } : NONE3;
17992
18048
  } catch {
17993
18049
  return NONE3;
@@ -18002,8 +18058,8 @@ function noteRawCommit(input, command, key) {
18002
18058
  if (action.kind !== "commit")
18003
18059
  continue;
18004
18060
  const file = markerFile(repo, input.session.id, key);
18005
- fs14.mkdirSync(path18.dirname(file), { recursive: true });
18006
- fs14.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
18061
+ fs15.mkdirSync(path19.dirname(file), { recursive: true });
18062
+ fs15.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
18007
18063
  `);
18008
18064
  }
18009
18065
  } catch {}
@@ -18025,15 +18081,15 @@ function settlePendingCommit(input) {
18025
18081
  if (!repo)
18026
18082
  return;
18027
18083
  const file = markerFile(repo, input.session.id, NEXT_COMMAND);
18028
- if (fs14.existsSync(file))
18084
+ if (fs15.existsSync(file))
18029
18085
  settle2(input, repo, file);
18030
18086
  } catch {}
18031
18087
  }
18032
- var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, isAbsolute = (value) => path18.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\"), joinDir = (base, next) => {
18088
+ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, isAbsolute = (value) => path19.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\"), joinDir = (base, next) => {
18033
18089
  if (base === undefined || /[$`*?]/.test(next) || next === "-")
18034
18090
  return;
18035
- const expanded = next === "~" || next.startsWith("~/") ? path18.join(os4.homedir(), next.slice(1)) : next;
18036
- return base === null || isAbsolute(expanded) ? expanded : path18.join(base, expanded);
18091
+ const expanded = next === "~" || next.startsWith("~/") ? path19.join(os4.homedir(), next.slice(1)) : next;
18092
+ return base === null || isAbsolute(expanded) ? expanded : path19.join(base, expanded);
18037
18093
  }, cdTarget = (words) => {
18038
18094
  const args = words.slice(1);
18039
18095
  const flag = args.findIndex((word) => /^-(?:literal)?path$/i.test(word));
@@ -18060,7 +18116,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18060
18116
  } else
18061
18117
  return words;
18062
18118
  }
18063
- }, commandName = (word) => (word?.split(/[\\/]/).at(-1) ?? "").toLowerCase().replace(/\.exe$/, ""), PUSH_VALUE_OPTIONS, pushAction = (args) => {
18119
+ }, commandName = (word) => (word?.split(/[\\/]/).at(-1) ?? "").toLowerCase().replace(/\.exe$/, ""), LAUNCHERS, WORKIT_BIN, PUSH_VALUE_OPTIONS, pushAction = (args) => {
18064
18120
  let force = false;
18065
18121
  let lease = false;
18066
18122
  let wholesale = false;
@@ -18131,7 +18187,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18131
18187
  return null;
18132
18188
  return { location, dir, branch: resolveTaskKey(dir, location).branch };
18133
18189
  }, isWorkitWorkspace = (repo) => {
18134
- if (fs14.existsSync(repo.location.dir))
18190
+ if (fs15.existsSync(repo.location.dir))
18135
18191
  return true;
18136
18192
  try {
18137
18193
  return resolveRuntimeWorkspaceVcs(repo.location.top ?? repo.dir) !== null;
@@ -18159,17 +18215,17 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18159
18215
  if (!git || git.reftable)
18160
18216
  return null;
18161
18217
  try {
18162
- const head = fs14.readFileSync(path18.join(git.gitDir, "HEAD"), "utf8").trim();
18218
+ const head = fs15.readFileSync(path19.join(git.gitDir, "HEAD"), "utf8").trim();
18163
18219
  if (/^[0-9a-f]{40,64}$/.test(head))
18164
18220
  return head;
18165
18221
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
18166
18222
  if (!ref)
18167
18223
  return null;
18168
- const common = path18.dirname(repo.location.dir);
18224
+ const common = path19.dirname(repo.location.dir);
18169
18225
  try {
18170
- return fs14.readFileSync(path18.join(common, ref), "utf8").trim() || null;
18226
+ return fs15.readFileSync(path19.join(common, ref), "utf8").trim() || null;
18171
18227
  } catch {}
18172
- const packed = fs14.readFileSync(path18.join(common, "packed-refs"), "utf8");
18228
+ const packed = fs15.readFileSync(path19.join(common, "packed-refs"), "utf8");
18173
18229
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
18174
18230
  } catch {
18175
18231
  return null;
@@ -18202,7 +18258,7 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18202
18258
  const action = rawAction(invocation);
18203
18259
  if (!action)
18204
18260
  continue;
18205
- const dir = path18.resolve(cwd, invocation.dir ?? ".");
18261
+ const dir = path19.resolve(cwd, invocation.dir ?? ".");
18206
18262
  if (!repos.has(dir)) {
18207
18263
  const repo = repoAt(dir);
18208
18264
  repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
@@ -18215,7 +18271,21 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18215
18271
  }, inWorkitWorkspace = (dir) => {
18216
18272
  const repo = repoAt(dir);
18217
18273
  return repo !== null && isWorkitWorkspace(repo);
18218
- }, rawDelivery = (cwd, command, dialect) => mentionsRawTool(command) && actionsIn(cwd, command, dialect).some(({ action }) => action.kind !== "commit" && action.kind !== "pr-read"), mentionsRawTool = (command) => /\b(?:git|gh|glab)\b/i.test(command), dialectOf = (input) => (input.event.kind === "shell.pre" || input.event.kind === "shell.post" ? input.event.dialect : undefined) ?? "posix", deny = (reason) => ({ kind: "deny", reason, unblock: null }), NEXT_COMMAND = "next", MARKER_TTL_SECONDS = 3600, markerFile = (repo, session, key) => path18.join(repo.location.dir, "hooks", `commit-${createHash12("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`), callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash12("sha256").update(command).digest("hex")}`, headCommit = (dir) => {
18274
+ }, rawDelivery = (cwd, command, dialect) => mentionsRawTool(command) && actionsIn(cwd, command, dialect).some(({ action }) => action.kind !== "commit" && action.kind !== "pr-read"), mentionsRawTool = (command) => /\b(?:git|gh|glab)\b/i.test(command), dialectOf = (input) => (input.event.kind === "shell.pre" || input.event.kind === "shell.post" ? input.event.dialect : undefined) ?? "posix", nudgeKind = (action) => action.kind === "commit" && action.amend ? "commit-amend" : action.kind === "push" && action.blindForce ? "push-force" : action.kind, unseenNudges = (input, nudges, store) => {
18275
+ const session = input.session.id;
18276
+ const file = session ? sessionMarkerFile(input, session, store) : null;
18277
+ if (!file)
18278
+ return [...nudges.values()];
18279
+ const marker = readSessionMarker(file);
18280
+ const agent = input.session.agentId ? `${input.session.agentId}:` : "";
18281
+ const fresh = [...nudges.keys()].filter((kind) => !marker.raw.includes(agent + kind));
18282
+ if (fresh.length)
18283
+ writeSessionMarker(file, {
18284
+ ...marker,
18285
+ raw: [...marker.raw, ...fresh.map((kind) => agent + kind)]
18286
+ });
18287
+ return fresh.map((kind) => nudges.get(kind));
18288
+ }, deny = (reason) => ({ kind: "deny", reason, unblock: null }), NEXT_COMMAND = "next", MARKER_TTL_SECONDS = 3600, markerFile = (repo, session, key) => path19.join(repo.location.dir, "hooks", `commit-${createHash13("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`), callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash13("sha256").update(command).digest("hex")}`, headCommit = (dir) => {
18219
18289
  const run = spawnSync11("git", [
18220
18290
  "log",
18221
18291
  "-1",
@@ -18232,11 +18302,11 @@ var NONE3, SHELLS, PREFIXES3, SCRIPT_FLAG, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, i
18232
18302
  }, settle2 = (input, repo, file) => {
18233
18303
  let marker;
18234
18304
  try {
18235
- marker = JSON.parse(fs14.readFileSync(file, "utf8"));
18305
+ marker = JSON.parse(fs15.readFileSync(file, "utf8"));
18236
18306
  } catch {
18237
18307
  return;
18238
18308
  }
18239
- fs14.rmSync(file, { force: true });
18309
+ fs15.rmSync(file, { force: true });
18240
18310
  const session = input.session.id;
18241
18311
  const now = Date.now() / 1000;
18242
18312
  if (typeof marker.at !== "number" || now - marker.at > MARKER_TTL_SECONDS)
@@ -18274,6 +18344,7 @@ var init_raw_git = __esm(() => {
18274
18344
  init_rev();
18275
18345
  init_ledger();
18276
18346
  init_paths();
18347
+ init_session_marker();
18277
18348
  NONE3 = { kind: "none" };
18278
18349
  SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
18279
18350
  PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
@@ -18290,6 +18361,20 @@ var init_raw_git = __esm(() => {
18290
18361
  "--config-env",
18291
18362
  "--super-prefix"
18292
18363
  ]);
18364
+ LAUNCHERS = new Set([
18365
+ "npx",
18366
+ "bunx",
18367
+ "pnpx",
18368
+ "-y",
18369
+ "--yes",
18370
+ "pnpm",
18371
+ "dlx",
18372
+ "exec",
18373
+ "env",
18374
+ "sudo",
18375
+ "command"
18376
+ ]);
18377
+ WORKIT_BIN = /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/;
18293
18378
  PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
18294
18379
  VALUE_OPTIONS = {
18295
18380
  git: new Set([
@@ -18320,6 +18405,153 @@ var init_raw_git = __esm(() => {
18320
18405
  };
18321
18406
  });
18322
18407
 
18408
+ // packages/workit-core/src/hooks/stop.ts
18409
+ import { spawnSync as spawnSync12 } from "node:child_process";
18410
+ function stopObligation(input, descriptor) {
18411
+ try {
18412
+ const session = input.session.id;
18413
+ if (!session || input.session.agentId || !inWorkitWorkspace(input.cwd))
18414
+ return null;
18415
+ const branch = currentBranch(input.cwd);
18416
+ if (!branch || guardedBranch(input.cwd, branch))
18417
+ return null;
18418
+ const ledger = readLedger(input.cwd);
18419
+ if (!ledger.ok)
18420
+ return null;
18421
+ const all = ledger.value.rows;
18422
+ const rows = all.filter((row) => row.branch === branch);
18423
+ const mine = rows.filter((row) => sessionOf(row) === session);
18424
+ 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"));
18425
+ const delivered = mine.some((row) => row.observer === "workit_cli" && (row.type === "pr.created" || row.type === "delivery.verified"));
18426
+ if (commits.size === 0 && !delivered)
18427
+ return null;
18428
+ const head = gitLine(input.cwd, ["rev-parse", "HEAD"]);
18429
+ if (landed(input.cwd, branch, rows, head, commits))
18430
+ return null;
18431
+ const endpoint = resolveAutonomy(input.cwd).effectiveEndpoint;
18432
+ if (endpoint !== "commit") {
18433
+ const left = unpushed(input.cwd, branch, endpoint, commits, mergedHeads(rows));
18434
+ if (left)
18435
+ return left;
18436
+ }
18437
+ const pr = openPr(rows);
18438
+ if ((endpoint === "green" || endpoint === "merged") && pr !== null && head) {
18439
+ const left = redChecks(rows, pr, head, endpoint);
18440
+ if (left)
18441
+ return left;
18442
+ }
18443
+ return delivered ? unverified(input.cwd, branch, all, descriptor) : null;
18444
+ } catch {
18445
+ return null;
18446
+ }
18447
+ }
18448
+ function stopDecision(input, descriptor) {
18449
+ const event = input.event;
18450
+ if (event.kind !== "stop" || event.stopHookActive)
18451
+ return NONE4;
18452
+ if (!event.lastMessage?.trim() || asksUser(event.lastMessage))
18453
+ return NONE4;
18454
+ const left = stopObligation(input, descriptor);
18455
+ return left ? {
18456
+ kind: "continue",
18457
+ reason: `Workit: before stopping, ${left.reason} If this cannot be done now, say so to the user and stop.`
18458
+ } : NONE4;
18459
+ }
18460
+ var NONE4, QUESTION, OFFER, YES_NO, LIST_ITEM, asksUser = (message) => {
18461
+ if (!message)
18462
+ return false;
18463
+ const paragraphs = message.replace(/```[\s\S]*?(?:```|$)/g, `
18464
+
18465
+ `).split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
18466
+ while (paragraphs.length > 1 && paragraphs.at(-1).split(`
18467
+ `).every((line) => LIST_ITEM.test(line)))
18468
+ paragraphs.pop();
18469
+ const tail = paragraphs.slice(-2).join(`
18470
+ `);
18471
+ return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
18472
+ }, short = (sha) => (sha ?? "?").slice(0, 12), gitList = (cwd, args) => {
18473
+ const run = spawnSync12("git", args, {
18474
+ cwd,
18475
+ encoding: "utf8",
18476
+ timeout: 3000,
18477
+ windowsHide: true,
18478
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" }
18479
+ });
18480
+ return run.status === 0 ? run.stdout.split(`
18481
+ `).filter(Boolean) : null;
18482
+ }, gitLine = (cwd, args) => gitList(cwd, args)?.[0] ?? null, sessionOf = (row) => (typeof row.session === "string" ? row.session : null) ?? row.actor.session, prState = (row) => typeof row.state === "string" ? row.state : "open", ended = (row) => row.observer === "workit_cli" && (row.type === "pr.merged" || row.type === "pr.status" && prState(row) !== "open"), openPr = (rows) => {
18483
+ const seen = rows.findLast((row) => (row.type === "pr.created" || row.type === "pr.status") && row.observer === "workit_cli" && typeof row.pr === "number");
18484
+ if (!seen || ended(seen))
18485
+ return null;
18486
+ const closed = rows.some((row) => ended(row) && row.pr === seen.pr && row.seq > seen.seq);
18487
+ return closed ? null : seen.pr;
18488
+ }, mergedHeads = (rows) => [
18489
+ ...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))
18490
+ ].slice(-3), localCommit = (cwd, sha) => /^[0-9a-f]{7,64}$/.test(sha) && gitList(cwd, ["cat-file", "-e", `${sha}^{commit}`]) !== null, upstreamGone = (cwd, branch) => gitLine(cwd, ["for-each-ref", "--format=%(upstream:track)", `refs/heads/${branch}`]) === "[gone]", landed = (cwd, branch, rows, head, mine) => {
18491
+ if (!head)
18492
+ return false;
18493
+ const merged = mergedHeads(rows);
18494
+ if (merged.length === 0)
18495
+ return upstreamGone(cwd, branch);
18496
+ if (merged.includes(head))
18497
+ return true;
18498
+ if (mine.size === 0)
18499
+ return false;
18500
+ return merged.some((at) => {
18501
+ const after = gitList(cwd, ["rev-list", `--max-count=${UNPUSHED_SCAN}`, "HEAD", "--not", at]);
18502
+ return after !== null && !after.some((sha) => mine.has(sha));
18503
+ });
18504
+ }, UNPUSHED_SCAN = 500, unpushed = (cwd, branch, endpoint, mine, merged) => {
18505
+ const remote = pushRemoteName(cwd, branch);
18506
+ if (!remote || mine.size === 0)
18507
+ return null;
18508
+ const listed = gitList(cwd, [
18509
+ "rev-list",
18510
+ `--max-count=${UNPUSHED_SCAN}`,
18511
+ "HEAD",
18512
+ "--not",
18513
+ `--remotes=${remote}`,
18514
+ ...merged.filter((sha) => localCommit(cwd, sha))
18515
+ ]);
18516
+ const count = (listed ?? []).filter((sha) => mine.has(sha)).length;
18517
+ if (count === 0)
18518
+ return null;
18519
+ return {
18520
+ kind: "unpushed",
18521
+ 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`)" : ""}.`
18522
+ };
18523
+ }, redChecks = (rows, pr, head, endpoint) => {
18524
+ const status = rows.findLast((row) => row.type === "pr.status" && row.observer === "workit_cli" && row.pr === pr);
18525
+ const state = status && status.head === head && prState(status) === "open" ? status.checks : null;
18526
+ if (state !== "failing" && state !== "pending")
18527
+ return null;
18528
+ return {
18529
+ kind: "checks",
18530
+ 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)" : ""}.`
18531
+ };
18532
+ }, unverified = (cwd, branch, all, descriptor) => {
18533
+ const check = checkVerdicts(cwd, branch, all);
18534
+ if (check.accepted.accepted || check.accepted.reasons.includes("failing_verdict"))
18535
+ return null;
18536
+ const has = check.review === "self-reviewed" ? "only a self verdict" : "no current verdict";
18537
+ const verifier = `${descriptor.subagents.agentPrefix}verifier`;
18538
+ return {
18539
+ kind: "verdict",
18540
+ 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.`
18541
+ };
18542
+ };
18543
+ var init_stop = __esm(() => {
18544
+ init_autonomy();
18545
+ init_rev();
18546
+ init_ledger();
18547
+ init_raw_git();
18548
+ NONE4 = { kind: "none" };
18549
+ QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
18550
+ 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;
18551
+ YES_NO = /[[(]\s*y(?:es)?\s*\/\s*n(?:o)?\s*[\])]/i;
18552
+ LIST_ITEM = /^\s*(?:[-*+•]|\d+[.)]|[a-z][.)])\s+/i;
18553
+ });
18554
+
18323
18555
  // packages/workit-core/src/core/skill-manifests.ts
18324
18556
  var WORKIT_METHOD_SKILLS, WORKIT_SKILL_ALIASES, skillLoadWording = (host, skill) => {
18325
18557
  switch (host) {
@@ -18366,9 +18598,6 @@ var init_skill_manifests = __esm(() => {
18366
18598
  });
18367
18599
 
18368
18600
  // packages/workit-core/src/hooks/skill-nudge.ts
18369
- import { createHash as createHash13 } from "node:crypto";
18370
- import fs15 from "node:fs";
18371
- import path19 from "node:path";
18372
18601
  function recordSkillLoad(input, name, via) {
18373
18602
  try {
18374
18603
  const skill = workitSkillOf(name);
@@ -18377,13 +18606,13 @@ function recordSkillLoad(input, name, via) {
18377
18606
  return;
18378
18607
  if (!inWorkitWorkspace(input.cwd))
18379
18608
  return;
18380
- const file = markerFile2(input, session);
18609
+ const file = sessionMarkerFile(input, session);
18381
18610
  if (!file)
18382
18611
  return;
18383
- const marker = readMarker(file);
18612
+ const marker = readSessionMarker(file);
18384
18613
  if (marker.loaded.includes(skill))
18385
18614
  return;
18386
- writeMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
18615
+ writeSessionMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
18387
18616
  if (loadedSkills(input.cwd, session).has(skill))
18388
18617
  return;
18389
18618
  appendHookObserved(input.cwd, {
@@ -18397,7 +18626,7 @@ function recordSkillLoad(input, name, via) {
18397
18626
  function promptNudge(input, prompt) {
18398
18627
  try {
18399
18628
  const session = input.session.id;
18400
- if (!prompt || !session || input.session.agentId)
18629
+ if (!prompt || !session || input.session.agentId || relayedMessage(prompt))
18401
18630
  return null;
18402
18631
  const loads = promptLoads(prompt);
18403
18632
  if (loads) {
@@ -18440,24 +18669,6 @@ var USER_INVOKED, SKILL_HOST, PROMPT_INTENTS, workitSkillOf = (name) => {
18440
18669
  return { skill, trigger: match[1] ?? match[0] };
18441
18670
  }
18442
18671
  return null;
18443
- }, markerFile2 = (input, session) => {
18444
- const location = resolveStore(input.cwd);
18445
- if (location instanceof Error || !location.shared)
18446
- return null;
18447
- const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
18448
- return path19.join(location.dir, "hooks", `skills-${name}.json`);
18449
- }, readMarker = (file) => {
18450
- try {
18451
- const value = JSON.parse(fs15.readFileSync(file, "utf8"));
18452
- return { nudged: value.nudged ?? [], loaded: value.loaded ?? [] };
18453
- } catch {
18454
- return { nudged: [], loaded: [] };
18455
- }
18456
- }, writeMarker = (file, marker) => {
18457
- try {
18458
- fs15.mkdirSync(path19.dirname(file), { recursive: true });
18459
- fs15.writeFileSync(file, JSON.stringify(marker));
18460
- } catch {}
18461
18672
  }, loadedSkills = (cwd, session) => {
18462
18673
  const ledger = readLedger(cwd);
18463
18674
  const loaded = new Set;
@@ -18481,28 +18692,17 @@ var USER_INVOKED, SKILL_HOST, PROMPT_INTENTS, workitSkillOf = (name) => {
18481
18692
  }
18482
18693
  return null;
18483
18694
  }, nudgeOnce = (input, session, skill, line) => {
18484
- const file = markerFile2(input, session);
18695
+ const file = sessionMarkerFile(input, session);
18485
18696
  if (!file)
18486
18697
  return null;
18487
- const marker = readMarker(file);
18698
+ const marker = readSessionMarker(file);
18488
18699
  if (marker.nudged.includes(skill) || marker.loaded.includes(skill))
18489
18700
  return null;
18490
18701
  if (loadedSkills(input.cwd, session).has(skill))
18491
18702
  return null;
18492
- writeMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
18703
+ writeSessionMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
18493
18704
  return line;
18494
- }, 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.`, 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, WORKIT_DELIVERY, SHELLS2, LAUNCHERS, runsWorkitDelivery = (command, dialect, depth = 0) => segmentsOf(command, dialect).some(({ words }) => {
18495
- const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
18496
- if (at >= 0 && /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/.test(words[at])) {
18497
- const args = words.slice(at + 1);
18498
- if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run"))
18499
- return false;
18500
- return WORKIT_DELIVERY.test(args.join(" "));
18501
- }
18502
- const shell = SHELLS2.has(words[at]?.split(/[\\/]/).at(-1) ?? "");
18503
- const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
18504
- return depth < 3 && script !== undefined && runsWorkitDelivery(script, "posix", depth + 1);
18505
- }), isDeliveryCommand = (cwd, command, dialect = "posix") => rawDelivery(cwd, command, dialect) || runsWorkitDelivery(command, dialect) && inWorkitWorkspace(cwd), withContextLine = (decision, line) => {
18705
+ }, 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.`, 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, relayedMessage = (prompt) => /^\s*(?:Another Claude session sent a message\b|<agent-message\s+from=|\[SYSTEM NOTIFICATION\b|<task-notification>)/.test(prompt), WORKIT_DELIVERY, runsWorkitDelivery = (command, dialect) => workitInvocations(command, dialect).some((args) => !args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run") && WORKIT_DELIVERY.test(args.join(" "))), isDeliveryCommand = (cwd, command, dialect = "posix") => rawDelivery(cwd, command, dialect) || runsWorkitDelivery(command, dialect) && inWorkitWorkspace(cwd), withContextLine = (decision, line) => {
18506
18706
  if (!line)
18507
18707
  return decision;
18508
18708
  if (decision.kind === "none")
@@ -18515,8 +18715,8 @@ ${line}` };
18515
18715
  var init_skill_nudge = __esm(() => {
18516
18716
  init_skill_manifests();
18517
18717
  init_ledger();
18518
- init_paths();
18519
18718
  init_raw_git();
18719
+ init_session_marker();
18520
18720
  USER_INVOKED = new Set(["workit-retro", "workit-architecture"]);
18521
18721
  SKILL_HOST = {
18522
18722
  claude_code: "claude-code",
@@ -18577,20 +18777,6 @@ var init_skill_nudge = __esm(() => {
18577
18777
  "type"
18578
18778
  ]);
18579
18779
  WORKIT_DELIVERY = /^(?:pr (?:create|merge)|ci wait|git push)\b/;
18580
- SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
18581
- LAUNCHERS = new Set([
18582
- "npx",
18583
- "bunx",
18584
- "pnpx",
18585
- "-y",
18586
- "--yes",
18587
- "pnpm",
18588
- "dlx",
18589
- "exec",
18590
- "env",
18591
- "sudo",
18592
- "command"
18593
- ]);
18594
18780
  });
18595
18781
 
18596
18782
  // packages/workit-core/src/hooks/hosts/fields.ts
@@ -18604,7 +18790,7 @@ var isRecord4 = (value) => value !== null && typeof value === "object" && !Array
18604
18790
  } catch {
18605
18791
  return null;
18606
18792
  }
18607
- }, SHELLS3, SCRIPT_FLAG2, quoteArg = (arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : JSON.stringify(arg), commandText = (value) => {
18793
+ }, SHELLS2, SCRIPT_FLAG2, quoteArg = (arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : JSON.stringify(arg), commandText = (value) => {
18608
18794
  if (nonEmpty2(value))
18609
18795
  return value;
18610
18796
  if (!Array.isArray(value) || value.length === 0)
@@ -18613,7 +18799,7 @@ var isRecord4 = (value) => value !== null && typeof value === "object" && !Array
18613
18799
  if (!argv.every((arg) => typeof arg === "string"))
18614
18800
  return null;
18615
18801
  const shell = argv[0].split(/[\\/]/).at(-1)?.replace(/\.exe$/i, "").toLowerCase() ?? "";
18616
- if (SHELLS3.has(shell))
18802
+ if (SHELLS2.has(shell))
18617
18803
  for (let index = 1;index < argv.length - 1; index++) {
18618
18804
  if (SCRIPT_FLAG2.test(argv[index]))
18619
18805
  return nonEmpty2(argv[index + 1]) ? argv[index + 1] : null;
@@ -18636,7 +18822,7 @@ var isRecord4 = (value) => value !== null && typeof value === "object" && !Array
18636
18822
  return paths.length > 0 ? paths : null;
18637
18823
  };
18638
18824
  var init_fields = __esm(() => {
18639
- SHELLS3 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
18825
+ SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
18640
18826
  SCRIPT_FLAG2 = /^(?:-\w*c|-command|\/c)$/i;
18641
18827
  WRITE_TOOLS = new Set([
18642
18828
  "write",
@@ -18668,7 +18854,7 @@ function handleHook(input, deps) {
18668
18854
  switch (event.kind) {
18669
18855
  case "session.start": {
18670
18856
  if (!usable2(descriptor.context.sessionStart))
18671
- return NONE4;
18857
+ return NONE5;
18672
18858
  const key = input.session.id ? `${input.host}\x00${input.session.id}` : null;
18673
18859
  const offer = event.source === "startup" && (key === null || !offered.has(key));
18674
18860
  if (offer && key !== null)
@@ -18683,42 +18869,44 @@ function handleHook(input, deps) {
18683
18869
  }
18684
18870
  case "context.turn": {
18685
18871
  if (!usable2(descriptor.context.perTurn))
18686
- return NONE4;
18872
+ return NONE5;
18687
18873
  const current = turnContextText(input, descriptor);
18688
18874
  const text = resendsOnChange(descriptor) ? changedTurnContext(input, current) : current;
18689
- return withContextLine(text ? { kind: "context", text } : NONE4, promptNudge(input, event.prompt));
18875
+ return withContextLine(text ? { kind: "context", text } : NONE5, promptNudge(input, event.prompt));
18690
18876
  }
18691
18877
  case "shell.pre": {
18692
18878
  const skillRead = skillReadIn(event.command, event.dialect);
18693
18879
  if (skillRead)
18694
18880
  recordSkillLoad(input, skillRead, "read");
18695
18881
  const canDeny = usable2(descriptor.shellPolicy.deny);
18696
- const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE4;
18882
+ const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE5;
18697
18883
  if (policy.kind !== "none")
18698
18884
  return policy;
18699
18885
  const postTool = usable2(descriptor.events["shell.post"].support);
18700
18886
  if (!postTool)
18701
18887
  settlePendingCommit(input);
18702
- const raw = canDeny ? rawGitPre(input, event.command) : NONE4;
18888
+ const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
18703
18889
  if (raw.kind === "deny")
18704
18890
  return raw;
18705
18891
  const writes = usable2(descriptor.events["write.pre"].support) ? shellWrites(event.command) : { writes: false, targets: null };
18706
- const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE4;
18892
+ const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE5;
18707
18893
  if (gate.kind === "deny")
18708
18894
  return gate;
18709
18895
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
18710
- return withContextLine(raw, shipNudge(input, event.command));
18896
+ if (descriptor.transport !== "hook-process")
18897
+ return NONE5;
18898
+ return withContextLine(canDeny ? rawGitPre(input, event.command) : NONE5, shipNudge(input, event.command));
18711
18899
  }
18712
18900
  case "shell.post":
18713
18901
  if (usable2(descriptor.events["shell.post"].support))
18714
18902
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
18715
- return NONE4;
18903
+ return NONE5;
18716
18904
  case "write.pre":
18717
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE4;
18905
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE5;
18718
18906
  case "subagent.start":
18719
18907
  return {
18720
18908
  kind: "context",
18721
- text: subagentStartText(input.session.id ?? null, descriptor, event)
18909
+ text: subagentStartText(input, descriptor, event)
18722
18910
  };
18723
18911
  case "compact.pre":
18724
18912
  return {
@@ -18728,24 +18916,30 @@ function handleHook(input, deps) {
18728
18916
  case "tool.pre":
18729
18917
  if (event.skill)
18730
18918
  recordSkillLoad(input, event.skill, "tool");
18731
- return NONE4;
18919
+ return NONE5;
18920
+ case "stop":
18921
+ return usable2(descriptor.events.stop.support) ? stopDecision(input, descriptor) : NONE5;
18732
18922
  case "subagent.stop":
18733
18923
  case "prompt.submit":
18734
- case "stop":
18735
- return NONE4;
18924
+ return NONE5;
18736
18925
  }
18737
18926
  }
18738
- var usable2 = (value) => value === "native" || value === "partial", NONE4, resendsOnChange = (descriptor) => descriptor.context.turnResend === "on-change" && usable2(descriptor.context.perTurn), offered, ROLES, workitRole = (descriptor, agentType) => {
18927
+ var usable2 = (value) => value === "native" || value === "partial", NONE5, resendsOnChange = (descriptor) => descriptor.context.turnResend === "on-change" && usable2(descriptor.context.perTurn), offered, ROLES, workitRole = (descriptor, agentType) => {
18739
18928
  const prefix = descriptor.subagents.agentPrefix;
18740
18929
  const name = agentType.startsWith(prefix) ? agentType.slice(prefix.length) : "";
18741
18930
  return Object.hasOwn(ROLES, name) ? ROLES[name] : null;
18742
- }, subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128), subagentStartText = (session, descriptor, event) => {
18931
+ }, subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128), CLAUDE_WORKTREE, 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`).', subagentStartText = (input, descriptor, event) => {
18932
+ const session = input.session.id ?? null;
18743
18933
  const role = workitRole(descriptor, event.agentType);
18744
18934
  const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
18745
- if (role === "implementer") {
18746
- 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";
18747
- 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.`;
18748
- }
18935
+ const isolated = descriptor.subagents.worktreeIsolation === "native" && (role === "implementer" || CLAUDE_WORKTREE.test(input.cwd));
18936
+ if (role === "implementer" && isolated)
18937
+ 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.`;
18938
+ if (isolated)
18939
+ return `${ISOLATED_GIT}
18940
+ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
18941
+ if (role === "implementer")
18942
+ 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.`;
18749
18943
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
18750
18944
  if (role !== "judge" || !event.agentId)
18751
18945
  return readOnly;
@@ -18753,10 +18947,10 @@ var usable2 = (value) => value === "native" || value === "partial", NONE4, resen
18753
18947
  return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result> --session ${own} ...\` so the ledger tells it apart from the author.`;
18754
18948
  }, failureDecision = (descriptor, event, error) => {
18755
18949
  if (event === "shell.pre" || event === "tool.pre" || event === "write.pre")
18756
- return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE4;
18950
+ return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE5;
18757
18951
  if (event === "session.start" || event === "subagent.start")
18758
18952
  return { kind: "context", text: `[workit diagnostic: ${error}]` };
18759
- return NONE4;
18953
+ return NONE5;
18760
18954
  }, dispatchHook = (adapter, raw, env = process.env) => {
18761
18955
  const parsed = adapter.parse(raw, env);
18762
18956
  if (!parsed.ok)
@@ -18784,14 +18978,16 @@ var init_handle = __esm(() => {
18784
18978
  init_policy();
18785
18979
  init_raw_git();
18786
18980
  init_skill_nudge();
18981
+ init_stop();
18787
18982
  init_write_gate();
18788
- NONE4 = { kind: "none" };
18983
+ NONE5 = { kind: "none" };
18789
18984
  offered = new Set;
18790
18985
  ROLES = {
18791
18986
  implementer: "implementer",
18792
18987
  verifier: "judge",
18793
18988
  reviewer: "judge"
18794
18989
  };
18990
+ CLAUDE_WORKTREE = /[\\/]\.claude[\\/]worktrees[\\/]/;
18795
18991
  });
18796
18992
 
18797
18993
  // packages/workit-core/src/hooks/hosts/claude-code.ts
@@ -18925,7 +19121,7 @@ var init_claude_code = __esm(() => {
18925
19121
  "prompt.submit": { support: "native", native: "UserPromptSubmit" },
18926
19122
  "subagent.stop": { support: "none", native: null },
18927
19123
  "compact.pre": { support: "none", native: null },
18928
- stop: { support: "none", native: null }
19124
+ stop: { support: "native", native: "Stop" }
18929
19125
  },
18930
19126
  shellPolicy: { deny: "native", channel: "permissionDecision", failClosed: false },
18931
19127
  context: {
@@ -19095,6 +19291,10 @@ var CODEX_DESCRIPTOR, EVENTS2, SHELL_TOOLS2, isShellTool = (name) => SHELL_TOOLS
19095
19291
  ...nonEmpty2(value.tool_use_id) ? { tool_use_id: value.tool_use_id } : {},
19096
19292
  ...nonEmpty2(value.agent_id) ? { agent_id: value.agent_id } : {},
19097
19293
  ...nonEmpty2(value.agent_type) ? { agent_type: value.agent_type } : {},
19294
+ ...event === "Stop" ? {
19295
+ last_assistant_message: optionalText(value.last_assistant_message),
19296
+ stop_hook_active: value.stop_hook_active === true
19297
+ } : {},
19098
19298
  ...event === "SubagentStop" ? {
19099
19299
  agent_transcript_path: optionalText(value.agent_transcript_path),
19100
19300
  last_assistant_message: optionalText(value.last_assistant_message),
@@ -19146,11 +19346,19 @@ var CODEX_DESCRIPTOR, EVENTS2, SHELL_TOOLS2, isShellTool = (name) => SHELL_TOOLS
19146
19346
  lastMessage: input.last_assistant_message ?? null,
19147
19347
  stopHookActive: input.stop_hook_active ?? false
19148
19348
  };
19349
+ case "Stop":
19350
+ return {
19351
+ kind: "stop",
19352
+ lastMessage: input.last_assistant_message ?? null,
19353
+ stopHookActive: input.stop_hook_active ?? false
19354
+ };
19149
19355
  }
19150
19356
  }, output = (event, extra = {}) => ({
19151
19357
  hookSpecificOutput: { hookEventName: event, ...extra }
19152
19358
  }), render2 = (decision, native) => {
19153
- if (native === "SubagentStop" || native === null)
19359
+ if (native === "Stop" && decision.kind === "continue")
19360
+ return { json: { decision: "block", reason: decision.reason }, exitCode: 0 };
19361
+ if (native === "SubagentStop" || native === "Stop" || native === null)
19154
19362
  return { json: {}, exitCode: 0 };
19155
19363
  if (decision.kind === "deny")
19156
19364
  return {
@@ -19183,7 +19391,7 @@ var init_codex = __esm(() => {
19183
19391
  "subagent.stop": { support: "native", native: "SubagentStop" },
19184
19392
  "prompt.submit": { support: "native", native: "UserPromptSubmit" },
19185
19393
  "compact.pre": { support: "none", native: null },
19186
- stop: { support: "none", native: null }
19394
+ stop: { support: "native", native: "Stop" }
19187
19395
  },
19188
19396
  shellPolicy: { deny: "native", channel: "permissionDecision", failClosed: false },
19189
19397
  context: {
@@ -19203,7 +19411,7 @@ var init_codex = __esm(() => {
19203
19411
  },
19204
19412
  provenance: { sessionId: "native", agentIdOnTool: "partial", postToolObserve: "native" },
19205
19413
  interaction: { questions: "none", writeBoundary: "partial" },
19206
- stopControl: "undocumented",
19414
+ stopControl: "native",
19207
19415
  shellAvailable: "native",
19208
19416
  perEventCost: "low",
19209
19417
  capabilities: [
@@ -19270,7 +19478,8 @@ var init_codex = __esm(() => {
19270
19478
  PreToolUse: "shell.pre",
19271
19479
  PostToolUse: "shell.post",
19272
19480
  SubagentStart: "subagent.start",
19273
- SubagentStop: "subagent.stop"
19481
+ SubagentStop: "subagent.stop",
19482
+ Stop: "stop"
19274
19483
  };
19275
19484
  SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
19276
19485
  SOURCES2 = new Set(["startup", "resume", "clear", "compact", "fork"]);
@@ -19308,6 +19517,7 @@ var init_codex = __esm(() => {
19308
19517
  });
19309
19518
 
19310
19519
  // packages/workit-core/src/hooks/hosts/cursor.ts
19520
+ import fs16 from "node:fs";
19311
19521
  import path21 from "node:path";
19312
19522
  var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
19313
19523
  if (!isRecord4(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
@@ -19343,7 +19553,12 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
19343
19553
  ...event === "subagentStart" && nonEmpty2(value.subagent_id) ? { subagent_id: value.subagent_id } : {},
19344
19554
  ...event === "subagentStart" && nonEmpty2(value.subagent_type) ? { subagent_type: value.subagent_type } : {},
19345
19555
  ...event === "subagentStart" && nonEmpty2(value.parent_conversation_id) ? { parent_conversation_id: value.parent_conversation_id } : {},
19346
- ...event === "subagentStart" && nonEmpty2(value.task) ? { task: value.task } : {}
19556
+ ...event === "subagentStart" && nonEmpty2(value.task) ? { task: value.task } : {},
19557
+ ...event === "stop" ? {
19558
+ ...nonEmpty2(value.status) ? { status: value.status } : {},
19559
+ ...typeof value.loop_count === "number" ? { loop_count: value.loop_count } : {},
19560
+ ...nonEmpty2(value.transcript_path) ? { transcript_path: value.transcript_path } : {}
19561
+ } : {}
19347
19562
  }
19348
19563
  };
19349
19564
  }, protocolEvent2 = (input) => {
@@ -19376,6 +19591,49 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
19376
19591
  };
19377
19592
  case "preCompact":
19378
19593
  return { kind: "compact.pre", trigger: "auto" };
19594
+ case "stop":
19595
+ return {
19596
+ kind: "stop",
19597
+ lastMessage: (input.status ?? "completed") === "completed" ? lastAssistantText(input.transcript_path) : null,
19598
+ stopHookActive: (input.loop_count ?? 0) > 0
19599
+ };
19600
+ }
19601
+ }, TRANSCRIPT_TAIL_BYTES, lastAssistantText = (file) => {
19602
+ if (!file || !path21.isAbsolute(file))
19603
+ return null;
19604
+ try {
19605
+ const size = fs16.statSync(file).size;
19606
+ const start = Math.max(0, size - TRANSCRIPT_TAIL_BYTES);
19607
+ const buffer = Buffer.alloc(size - start);
19608
+ const fd = fs16.openSync(file, "r");
19609
+ try {
19610
+ fs16.readSync(fd, buffer, 0, buffer.length, start);
19611
+ } finally {
19612
+ fs16.closeSync(fd);
19613
+ }
19614
+ const lines = buffer.toString("utf8").split(`
19615
+ `);
19616
+ for (let index = lines.length - 1;index >= 0; index--) {
19617
+ let entry;
19618
+ try {
19619
+ entry = JSON.parse(lines[index]);
19620
+ } catch {
19621
+ continue;
19622
+ }
19623
+ if (!isRecord4(entry) || entry.role !== "assistant")
19624
+ continue;
19625
+ const content = isRecord4(entry.message) ? entry.message.content : entry.content;
19626
+ if (typeof content === "string")
19627
+ return content;
19628
+ if (!Array.isArray(content))
19629
+ return null;
19630
+ const text = content.filter((part) => isRecord4(part) && part.type === "text" && typeof part.text === "string").map((part) => part.text).join(`
19631
+ `);
19632
+ return text || null;
19633
+ }
19634
+ return null;
19635
+ } catch {
19636
+ return null;
19379
19637
  }
19380
19638
  }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path21.sep) ? root : `${root}${path21.sep}`), hookCwd = (input) => {
19381
19639
  const cwd = existingDirectory(input.cwd);
@@ -19401,6 +19659,8 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
19401
19659
  return { json: { additional_context: decision.text }, exitCode: 0 };
19402
19660
  if (decision.kind === "notice")
19403
19661
  return { json: { user_message: decision.userMessage }, exitCode: 0 };
19662
+ if (decision.kind === "continue" && native === "stop")
19663
+ return { json: { followup_message: decision.reason }, exitCode: 0 };
19404
19664
  return {
19405
19665
  json: native !== null && BLOCKING2.has(native) ? { permission: "allow" } : {},
19406
19666
  exitCode: 0
@@ -19427,7 +19687,7 @@ var init_cursor = __esm(() => {
19427
19687
  "subagent.stop": { support: "partial", native: "subagentStop" },
19428
19688
  "prompt.submit": undocumented,
19429
19689
  "compact.pre": { support: "partial", native: "preCompact" },
19430
- stop: none
19690
+ stop: { support: "native", native: "stop" }
19431
19691
  },
19432
19692
  shellPolicy: { deny: "native", channel: "exit2+json", failClosed: true },
19433
19693
  context: {
@@ -19451,7 +19711,7 @@ var init_cursor = __esm(() => {
19451
19711
  postToolObserve: "undocumented"
19452
19712
  },
19453
19713
  interaction: { questions: "none", writeBoundary: "partial" },
19454
- stopControl: "undocumented",
19714
+ stopControl: "native",
19455
19715
  shellAvailable: "native",
19456
19716
  perEventCost: "low",
19457
19717
  capabilities: [
@@ -19518,8 +19778,10 @@ var init_cursor = __esm(() => {
19518
19778
  beforeShellExecution: "shell.pre",
19519
19779
  subagentStart: "subagent.start",
19520
19780
  subagentStop: "subagent.stop",
19521
- preCompact: "compact.pre"
19781
+ preCompact: "compact.pre",
19782
+ stop: "stop"
19522
19783
  };
19784
+ TRANSCRIPT_TAIL_BYTES = 256 * 1024;
19523
19785
  BLOCKING2 = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
19524
19786
  cursorAdapter = {
19525
19787
  descriptor: CURSOR_DESCRIPTOR,
@@ -19599,6 +19861,7 @@ var init_hooks = __esm(async () => {
19599
19861
  init_context();
19600
19862
  init_policy();
19601
19863
  init_write_gate();
19864
+ init_stop();
19602
19865
  init_raw_git();
19603
19866
  init_skill_nudge();
19604
19867
  init_fields();