@brainervirus/workit-pi 8.7.0 → 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.
Files changed (2) hide show
  1. package/dist/workit.js +353 -103
  2. package/package.json +1 -1
package/dist/workit.js CHANGED
@@ -19487,12 +19487,48 @@ function writeGate(cwd, paths) {
19487
19487
  return NONE2;
19488
19488
  }
19489
19489
  }
19490
+ // packages/workit-core/src/hooks/stop.ts
19491
+ import { spawnSync as spawnSync16 } from "node:child_process";
19492
+
19490
19493
  // packages/workit-core/src/hooks/raw-git.ts
19491
19494
  import { spawnSync as spawnSync15 } from "node:child_process";
19495
+ import { createHash as createHash13 } from "node:crypto";
19496
+ import fs17 from "node:fs";
19497
+ import os4 from "node:os";
19498
+ import path23 from "node:path";
19499
+
19500
+ // packages/workit-core/src/hooks/session-marker.ts
19492
19501
  import { createHash as createHash12 } from "node:crypto";
19493
19502
  import fs16 from "node:fs";
19494
- import os4 from "node:os";
19495
19503
  import path22 from "node:path";
19504
+ var sessionMarkerFile = (input, session, store) => {
19505
+ let dir = store;
19506
+ if (dir === undefined) {
19507
+ const location = resolveStore(input.cwd);
19508
+ if (location instanceof Error || !location.shared)
19509
+ return null;
19510
+ dir = location.dir;
19511
+ }
19512
+ const name = createHash12("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
19513
+ return path22.join(dir, "hooks", `skills-${name}.json`);
19514
+ };
19515
+ var list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
19516
+ var readSessionMarker = (file) => {
19517
+ try {
19518
+ const value = JSON.parse(fs16.readFileSync(file, "utf8"));
19519
+ return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
19520
+ } catch {
19521
+ return { nudged: [], loaded: [], raw: [] };
19522
+ }
19523
+ };
19524
+ var writeSessionMarker = (file, marker) => {
19525
+ try {
19526
+ fs16.mkdirSync(path22.dirname(file), { recursive: true });
19527
+ fs16.writeFileSync(file, JSON.stringify(marker));
19528
+ } catch {}
19529
+ };
19530
+
19531
+ // packages/workit-core/src/hooks/raw-git.ts
19496
19532
  var NONE3 = { kind: "none" };
19497
19533
  var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
19498
19534
  var PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
@@ -19509,12 +19545,12 @@ var GIT_VALUE_OPTIONS = new Set([
19509
19545
  "--config-env",
19510
19546
  "--super-prefix"
19511
19547
  ]);
19512
- var isAbsolute = (value) => path22.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
19548
+ var isAbsolute = (value) => path23.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
19513
19549
  var joinDir = (base, next) => {
19514
19550
  if (base === undefined || /[$`*?]/.test(next) || next === "-")
19515
19551
  return;
19516
- const expanded = next === "~" || next.startsWith("~/") ? path22.join(os4.homedir(), next.slice(1)) : next;
19517
- return base === null || isAbsolute(expanded) ? expanded : path22.join(base, expanded);
19552
+ const expanded = next === "~" || next.startsWith("~/") ? path23.join(os4.homedir(), next.slice(1)) : next;
19553
+ return base === null || isAbsolute(expanded) ? expanded : path23.join(base, expanded);
19518
19554
  };
19519
19555
  var cdTarget = (words) => {
19520
19556
  const args = words.slice(1);
@@ -19610,6 +19646,37 @@ function rawInvocations(command, dialect = "posix", depth = 0) {
19610
19646
  }
19611
19647
  return found;
19612
19648
  }
19649
+ var LAUNCHERS = new Set([
19650
+ "npx",
19651
+ "bunx",
19652
+ "pnpx",
19653
+ "-y",
19654
+ "--yes",
19655
+ "pnpm",
19656
+ "dlx",
19657
+ "exec",
19658
+ "env",
19659
+ "sudo",
19660
+ "command"
19661
+ ]);
19662
+ var WORKIT_BIN = /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/;
19663
+ function workitInvocations(command, dialect = "posix", depth = 0) {
19664
+ const found = [];
19665
+ for (const { words } of segmentsOf(command, dialect)) {
19666
+ const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
19667
+ if (at < 0)
19668
+ continue;
19669
+ if (WORKIT_BIN.test(words[at])) {
19670
+ found.push(words.slice(at + 1));
19671
+ continue;
19672
+ }
19673
+ const shell = SHELLS.has(commandName(words[at]));
19674
+ const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
19675
+ if (depth < 3 && script !== undefined)
19676
+ found.push(...workitInvocations(script, "posix", depth + 1));
19677
+ }
19678
+ return found;
19679
+ }
19613
19680
  var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
19614
19681
  var pushAction = (args) => {
19615
19682
  let force = false;
@@ -19736,7 +19803,7 @@ var repoAt = (dir) => {
19736
19803
  return { location, dir, branch: resolveTaskKey(dir, location).branch };
19737
19804
  };
19738
19805
  var isWorkitWorkspace = (repo) => {
19739
- if (fs16.existsSync(repo.location.dir))
19806
+ if (fs17.existsSync(repo.location.dir))
19740
19807
  return true;
19741
19808
  try {
19742
19809
  return resolveRuntimeWorkspaceVcs(repo.location.top ?? repo.dir) !== null;
@@ -19767,17 +19834,17 @@ var headSha2 = (repo) => {
19767
19834
  if (!git || git.reftable)
19768
19835
  return null;
19769
19836
  try {
19770
- const head = fs16.readFileSync(path22.join(git.gitDir, "HEAD"), "utf8").trim();
19837
+ const head = fs17.readFileSync(path23.join(git.gitDir, "HEAD"), "utf8").trim();
19771
19838
  if (/^[0-9a-f]{40,64}$/.test(head))
19772
19839
  return head;
19773
19840
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
19774
19841
  if (!ref)
19775
19842
  return null;
19776
- const common = path22.dirname(repo.location.dir);
19843
+ const common = path23.dirname(repo.location.dir);
19777
19844
  try {
19778
- return fs16.readFileSync(path22.join(common, ref), "utf8").trim() || null;
19845
+ return fs17.readFileSync(path23.join(common, ref), "utf8").trim() || null;
19779
19846
  } catch {}
19780
- const packed = fs16.readFileSync(path22.join(common, "packed-refs"), "utf8");
19847
+ const packed = fs17.readFileSync(path23.join(common, "packed-refs"), "utf8");
19781
19848
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
19782
19849
  } catch {
19783
19850
  return null;
@@ -19814,7 +19881,7 @@ var actionsIn = (cwd, command, dialect) => {
19814
19881
  const action = rawAction(invocation);
19815
19882
  if (!action)
19816
19883
  continue;
19817
- const dir = path22.resolve(cwd, invocation.dir ?? ".");
19884
+ const dir = path23.resolve(cwd, invocation.dir ?? ".");
19818
19885
  if (!repos.has(dir)) {
19819
19886
  const repo = repoAt(dir);
19820
19887
  repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
@@ -19832,12 +19899,31 @@ var inWorkitWorkspace = (dir) => {
19832
19899
  var rawDelivery = (cwd, command, dialect) => mentionsRawTool(command) && actionsIn(cwd, command, dialect).some(({ action }) => action.kind !== "commit" && action.kind !== "pr-read");
19833
19900
  var mentionsRawTool = (command) => /\b(?:git|gh|glab)\b/i.test(command);
19834
19901
  var dialectOf = (input) => (input.event.kind === "shell.pre" || input.event.kind === "shell.post" ? input.event.dialect : undefined) ?? "posix";
19835
- function rawGitPre(input, command) {
19902
+ var nudgeKind = (action) => action.kind === "commit" && action.amend ? "commit-amend" : action.kind === "push" && action.blindForce ? "push-force" : action.kind;
19903
+ var unseenNudges = (input, nudges, store) => {
19904
+ const session = input.session.id;
19905
+ const file = session ? sessionMarkerFile(input, session, store) : null;
19906
+ if (!file)
19907
+ return [...nudges.values()];
19908
+ const marker = readSessionMarker(file);
19909
+ const agent = input.session.agentId ? `${input.session.agentId}:` : "";
19910
+ const fresh = [...nudges.keys()].filter((kind) => !marker.raw.includes(agent + kind));
19911
+ if (fresh.length)
19912
+ writeSessionMarker(file, {
19913
+ ...marker,
19914
+ raw: [...marker.raw, ...fresh.map((kind) => agent + kind)]
19915
+ });
19916
+ return fresh.map((kind) => nudges.get(kind));
19917
+ };
19918
+ function rawGitPre(input, command, options = {}) {
19836
19919
  try {
19837
19920
  if (!mentionsRawTool(command))
19838
19921
  return NONE3;
19839
- const nudges = [];
19840
- for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input))) {
19922
+ const dialect = dialectOf(input);
19923
+ const nudges = new Map;
19924
+ let store = null;
19925
+ for (const { action, repo } of actionsIn(input.cwd, command, dialect)) {
19926
+ store ??= repo.location.dir;
19841
19927
  if (action.kind === "merge")
19842
19928
  return deny(refusal(action, null));
19843
19929
  if (action.kind === "push")
@@ -19847,10 +19933,15 @@ function rawGitPre(input, command) {
19847
19933
  return deny(refusal(action, branch));
19848
19934
  }
19849
19935
  const nudge = nudgeFor(action, input.host, input.session.id);
19850
- if (nudge && !nudges.includes(nudge))
19851
- nudges.push(nudge);
19936
+ if (nudge && !nudges.has(nudgeKind(action)))
19937
+ nudges.set(nudgeKind(action), nudge);
19852
19938
  }
19853
- return nudges.length ? { kind: "context", text: nudges.join(`
19939
+ if (options.nudge === false || !store || nudges.size === 0)
19940
+ return NONE3;
19941
+ if (workitInvocations(command, dialect).length > 0)
19942
+ return NONE3;
19943
+ const shown = unseenNudges(input, nudges, store);
19944
+ return shown.length ? { kind: "context", text: shown.join(`
19854
19945
  `) } : NONE3;
19855
19946
  } catch {
19856
19947
  return NONE3;
@@ -19859,8 +19950,8 @@ function rawGitPre(input, command) {
19859
19950
  var deny = (reason) => ({ kind: "deny", reason, unblock: null });
19860
19951
  var NEXT_COMMAND = "next";
19861
19952
  var MARKER_TTL_SECONDS = 3600;
19862
- var markerFile = (repo, session, key) => path22.join(repo.location.dir, "hooks", `commit-${createHash12("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
19863
- var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash12("sha256").update(command).digest("hex")}`;
19953
+ var markerFile = (repo, session, key) => path23.join(repo.location.dir, "hooks", `commit-${createHash13("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
19954
+ var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash13("sha256").update(command).digest("hex")}`;
19864
19955
  function noteRawCommit(input, command, key) {
19865
19956
  try {
19866
19957
  if (!input.session.id || !/\bcommit\b/.test(command))
@@ -19870,8 +19961,8 @@ function noteRawCommit(input, command, key) {
19870
19961
  if (action.kind !== "commit")
19871
19962
  continue;
19872
19963
  const file = markerFile(repo, input.session.id, key);
19873
- fs16.mkdirSync(path22.dirname(file), { recursive: true });
19874
- fs16.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
19964
+ fs17.mkdirSync(path23.dirname(file), { recursive: true });
19965
+ fs17.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
19875
19966
  `);
19876
19967
  }
19877
19968
  } catch {}
@@ -19894,11 +19985,11 @@ var headCommit = (dir) => {
19894
19985
  var settle2 = (input, repo, file) => {
19895
19986
  let marker;
19896
19987
  try {
19897
- marker = JSON.parse(fs16.readFileSync(file, "utf8"));
19988
+ marker = JSON.parse(fs17.readFileSync(file, "utf8"));
19898
19989
  } catch {
19899
19990
  return;
19900
19991
  }
19901
- fs16.rmSync(file, { force: true });
19992
+ fs17.rmSync(file, { force: true });
19902
19993
  const session = input.session.id;
19903
19994
  const now = Date.now() / 1000;
19904
19995
  if (typeof marker.at !== "number" || now - marker.at > MARKER_TTL_SECONDS)
@@ -19947,15 +20038,165 @@ function settlePendingCommit(input) {
19947
20038
  if (!repo)
19948
20039
  return;
19949
20040
  const file = markerFile(repo, input.session.id, NEXT_COMMAND);
19950
- if (fs16.existsSync(file))
20041
+ if (fs17.existsSync(file))
19951
20042
  settle2(input, repo, file);
19952
20043
  } catch {}
19953
20044
  }
19954
- // packages/workit-core/src/hooks/skill-nudge.ts
19955
- import { createHash as createHash13 } from "node:crypto";
19956
- import fs17 from "node:fs";
19957
- import path23 from "node:path";
19958
20045
 
20046
+ // packages/workit-core/src/hooks/stop.ts
20047
+ var NONE4 = { kind: "none" };
20048
+ var QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
20049
+ 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;
20050
+ var YES_NO = /[[(]\s*y(?:es)?\s*\/\s*n(?:o)?\s*[\])]/i;
20051
+ var LIST_ITEM = /^\s*(?:[-*+•]|\d+[.)]|[a-z][.)])\s+/i;
20052
+ var asksUser = (message) => {
20053
+ if (!message)
20054
+ return false;
20055
+ const paragraphs = message.replace(/```[\s\S]*?(?:```|$)/g, `
20056
+
20057
+ `).split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
20058
+ while (paragraphs.length > 1 && paragraphs.at(-1).split(`
20059
+ `).every((line) => LIST_ITEM.test(line)))
20060
+ paragraphs.pop();
20061
+ const tail = paragraphs.slice(-2).join(`
20062
+ `);
20063
+ return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
20064
+ };
20065
+ var short = (sha) => (sha ?? "?").slice(0, 12);
20066
+ var gitList = (cwd, args) => {
20067
+ const run = spawnSync16("git", args, {
20068
+ cwd,
20069
+ encoding: "utf8",
20070
+ timeout: 3000,
20071
+ windowsHide: true,
20072
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" }
20073
+ });
20074
+ return run.status === 0 ? run.stdout.split(`
20075
+ `).filter(Boolean) : null;
20076
+ };
20077
+ var gitLine = (cwd, args) => gitList(cwd, args)?.[0] ?? null;
20078
+ var sessionOf = (row) => (typeof row.session === "string" ? row.session : null) ?? row.actor.session;
20079
+ var prState = (row) => typeof row.state === "string" ? row.state : "open";
20080
+ var ended = (row) => row.observer === "workit_cli" && (row.type === "pr.merged" || row.type === "pr.status" && prState(row) !== "open");
20081
+ var openPr = (rows) => {
20082
+ const seen = rows.findLast((row) => (row.type === "pr.created" || row.type === "pr.status") && row.observer === "workit_cli" && typeof row.pr === "number");
20083
+ if (!seen || ended(seen))
20084
+ return null;
20085
+ const closed = rows.some((row) => ended(row) && row.pr === seen.pr && row.seq > seen.seq);
20086
+ return closed ? null : seen.pr;
20087
+ };
20088
+ var mergedHeads = (rows) => [
20089
+ ...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))
20090
+ ].slice(-3);
20091
+ var localCommit = (cwd, sha) => /^[0-9a-f]{7,64}$/.test(sha) && gitList(cwd, ["cat-file", "-e", `${sha}^{commit}`]) !== null;
20092
+ var upstreamGone = (cwd, branch) => gitLine(cwd, ["for-each-ref", "--format=%(upstream:track)", `refs/heads/${branch}`]) === "[gone]";
20093
+ var landed = (cwd, branch, rows, head, mine) => {
20094
+ if (!head)
20095
+ return false;
20096
+ const merged = mergedHeads(rows);
20097
+ if (merged.length === 0)
20098
+ return upstreamGone(cwd, branch);
20099
+ if (merged.includes(head))
20100
+ return true;
20101
+ if (mine.size === 0)
20102
+ return false;
20103
+ return merged.some((at) => {
20104
+ const after = gitList(cwd, ["rev-list", `--max-count=${UNPUSHED_SCAN}`, "HEAD", "--not", at]);
20105
+ return after !== null && !after.some((sha) => mine.has(sha));
20106
+ });
20107
+ };
20108
+ var UNPUSHED_SCAN = 500;
20109
+ var unpushed = (cwd, branch, endpoint, mine, merged) => {
20110
+ const remote = pushRemoteName(cwd, branch);
20111
+ if (!remote || mine.size === 0)
20112
+ return null;
20113
+ const listed = gitList(cwd, [
20114
+ "rev-list",
20115
+ `--max-count=${UNPUSHED_SCAN}`,
20116
+ "HEAD",
20117
+ "--not",
20118
+ `--remotes=${remote}`,
20119
+ ...merged.filter((sha) => localCommit(cwd, sha))
20120
+ ]);
20121
+ const count = (listed ?? []).filter((sha) => mine.has(sha)).length;
20122
+ if (count === 0)
20123
+ return null;
20124
+ return {
20125
+ kind: "unpushed",
20126
+ 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`)" : ""}.`
20127
+ };
20128
+ };
20129
+ var redChecks = (rows, pr, head, endpoint) => {
20130
+ const status = rows.findLast((row) => row.type === "pr.status" && row.observer === "workit_cli" && row.pr === pr);
20131
+ const state = status && status.head === head && prState(status) === "open" ? status.checks : null;
20132
+ if (state !== "failing" && state !== "pending")
20133
+ return null;
20134
+ return {
20135
+ kind: "checks",
20136
+ 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)" : ""}.`
20137
+ };
20138
+ };
20139
+ var unverified = (cwd, branch, all, descriptor) => {
20140
+ const check = checkVerdicts(cwd, branch, all);
20141
+ if (check.accepted.accepted || check.accepted.reasons.includes("failing_verdict"))
20142
+ return null;
20143
+ const has = check.review === "self-reviewed" ? "only a self verdict" : "no current verdict";
20144
+ const verifier = `${descriptor.subagents.agentPrefix}verifier`;
20145
+ return {
20146
+ kind: "verdict",
20147
+ 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.`
20148
+ };
20149
+ };
20150
+ function stopObligation(input, descriptor) {
20151
+ try {
20152
+ const session = input.session.id;
20153
+ if (!session || input.session.agentId || !inWorkitWorkspace(input.cwd))
20154
+ return null;
20155
+ const branch = currentBranch(input.cwd);
20156
+ if (!branch || guardedBranch(input.cwd, branch))
20157
+ return null;
20158
+ const ledger = readLedger(input.cwd);
20159
+ if (!ledger.ok)
20160
+ return null;
20161
+ const all = ledger.value.rows;
20162
+ const rows = all.filter((row) => row.branch === branch);
20163
+ const mine = rows.filter((row) => sessionOf(row) === session);
20164
+ 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"));
20165
+ const delivered = mine.some((row) => row.observer === "workit_cli" && (row.type === "pr.created" || row.type === "delivery.verified"));
20166
+ if (commits.size === 0 && !delivered)
20167
+ return null;
20168
+ const head = gitLine(input.cwd, ["rev-parse", "HEAD"]);
20169
+ if (landed(input.cwd, branch, rows, head, commits))
20170
+ return null;
20171
+ const endpoint = resolveAutonomy(input.cwd).effectiveEndpoint;
20172
+ if (endpoint !== "commit") {
20173
+ const left = unpushed(input.cwd, branch, endpoint, commits, mergedHeads(rows));
20174
+ if (left)
20175
+ return left;
20176
+ }
20177
+ const pr = openPr(rows);
20178
+ if ((endpoint === "green" || endpoint === "merged") && pr !== null && head) {
20179
+ const left = redChecks(rows, pr, head, endpoint);
20180
+ if (left)
20181
+ return left;
20182
+ }
20183
+ return delivered ? unverified(input.cwd, branch, all, descriptor) : null;
20184
+ } catch {
20185
+ return null;
20186
+ }
20187
+ }
20188
+ function stopDecision(input, descriptor) {
20189
+ const event = input.event;
20190
+ if (event.kind !== "stop" || event.stopHookActive)
20191
+ return NONE4;
20192
+ if (!event.lastMessage?.trim() || asksUser(event.lastMessage))
20193
+ return NONE4;
20194
+ const left = stopObligation(input, descriptor);
20195
+ return left ? {
20196
+ kind: "continue",
20197
+ reason: `Workit: before stopping, ${left.reason} If this cannot be done now, say so to the user and stop.`
20198
+ } : NONE4;
20199
+ }
19959
20200
  // packages/workit-core/src/core/skill-manifests.ts
19960
20201
  var WORKIT_METHOD_SKILLS = [
19961
20202
  "workit-shape",
@@ -20062,27 +20303,6 @@ var promptTrigger = (prompt) => {
20062
20303
  }
20063
20304
  return null;
20064
20305
  };
20065
- var markerFile2 = (input, session) => {
20066
- const location = resolveStore(input.cwd);
20067
- if (location instanceof Error || !location.shared)
20068
- return null;
20069
- const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
20070
- return path23.join(location.dir, "hooks", `skills-${name}.json`);
20071
- };
20072
- var readMarker = (file) => {
20073
- try {
20074
- const value = JSON.parse(fs17.readFileSync(file, "utf8"));
20075
- return { nudged: value.nudged ?? [], loaded: value.loaded ?? [] };
20076
- } catch {
20077
- return { nudged: [], loaded: [] };
20078
- }
20079
- };
20080
- var writeMarker = (file, marker) => {
20081
- try {
20082
- fs17.mkdirSync(path23.dirname(file), { recursive: true });
20083
- fs17.writeFileSync(file, JSON.stringify(marker));
20084
- } catch {}
20085
- };
20086
20306
  var loadedSkills = (cwd, session) => {
20087
20307
  const ledger = readLedger(cwd);
20088
20308
  const loaded = new Set;
@@ -20130,13 +20350,13 @@ function recordSkillLoad(input, name, via) {
20130
20350
  return;
20131
20351
  if (!inWorkitWorkspace(input.cwd))
20132
20352
  return;
20133
- const file = markerFile2(input, session);
20353
+ const file = sessionMarkerFile(input, session);
20134
20354
  if (!file)
20135
20355
  return;
20136
- const marker = readMarker(file);
20356
+ const marker = readSessionMarker(file);
20137
20357
  if (marker.loaded.includes(skill))
20138
20358
  return;
20139
- writeMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
20359
+ writeSessionMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
20140
20360
  if (loadedSkills(input.cwd, session).has(skill))
20141
20361
  return;
20142
20362
  appendHookObserved(input.cwd, {
@@ -20148,23 +20368,24 @@ function recordSkillLoad(input, name, via) {
20148
20368
  } catch {}
20149
20369
  }
20150
20370
  var nudgeOnce = (input, session, skill, line) => {
20151
- const file = markerFile2(input, session);
20371
+ const file = sessionMarkerFile(input, session);
20152
20372
  if (!file)
20153
20373
  return null;
20154
- const marker = readMarker(file);
20374
+ const marker = readSessionMarker(file);
20155
20375
  if (marker.nudged.includes(skill) || marker.loaded.includes(skill))
20156
20376
  return null;
20157
20377
  if (loadedSkills(input.cwd, session).has(skill))
20158
20378
  return null;
20159
- writeMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
20379
+ writeSessionMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
20160
20380
  return line;
20161
20381
  };
20162
20382
  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.`;
20163
20383
  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;
20384
+ var relayedMessage = (prompt) => /^\s*(?:Another Claude session sent a message\b|<agent-message\s+from=|\[SYSTEM NOTIFICATION\b|<task-notification>)/.test(prompt);
20164
20385
  function promptNudge(input, prompt) {
20165
20386
  try {
20166
20387
  const session = input.session.id;
20167
- if (!prompt || !session || input.session.agentId)
20388
+ if (!prompt || !session || input.session.agentId || relayedMessage(prompt))
20168
20389
  return null;
20169
20390
  const loads = promptLoads(prompt);
20170
20391
  if (loads) {
@@ -20180,32 +20401,7 @@ function promptNudge(input, prompt) {
20180
20401
  }
20181
20402
  }
20182
20403
  var WORKIT_DELIVERY = /^(?:pr (?:create|merge)|ci wait|git push)\b/;
20183
- var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
20184
- var LAUNCHERS = new Set([
20185
- "npx",
20186
- "bunx",
20187
- "pnpx",
20188
- "-y",
20189
- "--yes",
20190
- "pnpm",
20191
- "dlx",
20192
- "exec",
20193
- "env",
20194
- "sudo",
20195
- "command"
20196
- ]);
20197
- var runsWorkitDelivery = (command, dialect, depth = 0) => segmentsOf(command, dialect).some(({ words }) => {
20198
- const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
20199
- if (at >= 0 && /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/.test(words[at])) {
20200
- const args = words.slice(at + 1);
20201
- if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run"))
20202
- return false;
20203
- return WORKIT_DELIVERY.test(args.join(" "));
20204
- }
20205
- const shell = SHELLS2.has(words[at]?.split(/[\\/]/).at(-1) ?? "");
20206
- const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
20207
- return depth < 3 && script !== undefined && runsWorkitDelivery(script, "posix", depth + 1);
20208
- });
20404
+ var runsWorkitDelivery = (command, dialect) => workitInvocations(command, dialect).some((args) => !args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run") && WORKIT_DELIVERY.test(args.join(" ")));
20209
20405
  var isDeliveryCommand = (cwd, command, dialect = "posix") => rawDelivery(cwd, command, dialect) || runsWorkitDelivery(command, dialect) && inWorkitWorkspace(cwd);
20210
20406
  function shipNudge(input, command) {
20211
20407
  try {
@@ -20234,7 +20430,7 @@ var shellNudge = (input, command) => withContextLine(rawGitPre(input, command),
20234
20430
  // packages/workit-core/src/hooks/hosts/fields.ts
20235
20431
  var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
20236
20432
  var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
20237
- var SHELLS3 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
20433
+ var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
20238
20434
  var WRITE_TOOLS = new Set([
20239
20435
  "write",
20240
20436
  "edit",
@@ -20268,7 +20464,7 @@ var writePaths = (input) => {
20268
20464
  };
20269
20465
  // packages/workit-core/src/hooks/handle.ts
20270
20466
  var usable2 = (value) => value === "native" || value === "partial";
20271
- var NONE4 = { kind: "none" };
20467
+ var NONE5 = { kind: "none" };
20272
20468
  var resendsOnChange = (descriptor) => descriptor.context.turnResend === "on-change" && usable2(descriptor.context.perTurn);
20273
20469
  var offered = new Set;
20274
20470
  var ROLES = {
@@ -20282,13 +20478,20 @@ var workitRole = (descriptor, agentType) => {
20282
20478
  return Object.hasOwn(ROLES, name) ? ROLES[name] : null;
20283
20479
  };
20284
20480
  var subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128);
20285
- var subagentStartText = (session, descriptor, event) => {
20481
+ var CLAUDE_WORKTREE = /[\\/]\.claude[\\/]worktrees[\\/]/;
20482
+ 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`).';
20483
+ var subagentStartText = (input, descriptor, event) => {
20484
+ const session = input.session.id ?? null;
20286
20485
  const role = workitRole(descriptor, event.agentType);
20287
20486
  const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
20288
- if (role === "implementer") {
20289
- 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";
20290
- 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.`;
20291
- }
20487
+ const isolated = descriptor.subagents.worktreeIsolation === "native" && (role === "implementer" || CLAUDE_WORKTREE.test(input.cwd));
20488
+ if (role === "implementer" && isolated)
20489
+ 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.`;
20490
+ if (isolated)
20491
+ return `${ISOLATED_GIT}
20492
+ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
20493
+ if (role === "implementer")
20494
+ 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.`;
20292
20495
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
20293
20496
  if (role !== "judge" || !event.agentId)
20294
20497
  return readOnly;
@@ -20301,7 +20504,7 @@ function handleHook(input, deps) {
20301
20504
  switch (event.kind) {
20302
20505
  case "session.start": {
20303
20506
  if (!usable2(descriptor.context.sessionStart))
20304
- return NONE4;
20507
+ return NONE5;
20305
20508
  const key = input.session.id ? `${input.host}\x00${input.session.id}` : null;
20306
20509
  const offer = event.source === "startup" && (key === null || !offered.has(key));
20307
20510
  if (offer && key !== null)
@@ -20316,42 +20519,44 @@ function handleHook(input, deps) {
20316
20519
  }
20317
20520
  case "context.turn": {
20318
20521
  if (!usable2(descriptor.context.perTurn))
20319
- return NONE4;
20522
+ return NONE5;
20320
20523
  const current = turnContextText(input, descriptor);
20321
20524
  const text = resendsOnChange(descriptor) ? changedTurnContext(input, current) : current;
20322
- return withContextLine(text ? { kind: "context", text } : NONE4, promptNudge(input, event.prompt));
20525
+ return withContextLine(text ? { kind: "context", text } : NONE5, promptNudge(input, event.prompt));
20323
20526
  }
20324
20527
  case "shell.pre": {
20325
20528
  const skillRead = skillReadIn(event.command, event.dialect);
20326
20529
  if (skillRead)
20327
20530
  recordSkillLoad(input, skillRead, "read");
20328
20531
  const canDeny = usable2(descriptor.shellPolicy.deny);
20329
- const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE4;
20532
+ const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE5;
20330
20533
  if (policy.kind !== "none")
20331
20534
  return policy;
20332
20535
  const postTool = usable2(descriptor.events["shell.post"].support);
20333
20536
  if (!postTool)
20334
20537
  settlePendingCommit(input);
20335
- const raw = canDeny ? rawGitPre(input, event.command) : NONE4;
20538
+ const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
20336
20539
  if (raw.kind === "deny")
20337
20540
  return raw;
20338
20541
  const writes = usable2(descriptor.events["write.pre"].support) ? shellWrites(event.command) : { writes: false, targets: null };
20339
- const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE4;
20542
+ const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE5;
20340
20543
  if (gate.kind === "deny")
20341
20544
  return gate;
20342
20545
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
20343
- return withContextLine(raw, shipNudge(input, event.command));
20546
+ if (descriptor.transport !== "hook-process")
20547
+ return NONE5;
20548
+ return withContextLine(canDeny ? rawGitPre(input, event.command) : NONE5, shipNudge(input, event.command));
20344
20549
  }
20345
20550
  case "shell.post":
20346
20551
  if (usable2(descriptor.events["shell.post"].support))
20347
20552
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
20348
- return NONE4;
20553
+ return NONE5;
20349
20554
  case "write.pre":
20350
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE4;
20555
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE5;
20351
20556
  case "subagent.start":
20352
20557
  return {
20353
20558
  kind: "context",
20354
- text: subagentStartText(input.session.id ?? null, descriptor, event)
20559
+ text: subagentStartText(input, descriptor, event)
20355
20560
  };
20356
20561
  case "compact.pre":
20357
20562
  return {
@@ -20361,11 +20566,12 @@ function handleHook(input, deps) {
20361
20566
  case "tool.pre":
20362
20567
  if (event.skill)
20363
20568
  recordSkillLoad(input, event.skill, "tool");
20364
- return NONE4;
20569
+ return NONE5;
20570
+ case "stop":
20571
+ return usable2(descriptor.events.stop.support) ? stopDecision(input, descriptor) : NONE5;
20365
20572
  case "subagent.stop":
20366
20573
  case "prompt.submit":
20367
- case "stop":
20368
- return NONE4;
20574
+ return NONE5;
20369
20575
  }
20370
20576
  }
20371
20577
  // packages/workit-core/src/hooks/hosts/claude-code.ts
@@ -20384,6 +20590,7 @@ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
20384
20590
  var SOURCES2 = new Set(["startup", "resume", "clear", "compact", "fork"]);
20385
20591
 
20386
20592
  // packages/workit-core/src/hooks/hosts/cursor.ts
20593
+ var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
20387
20594
  var BLOCKING2 = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
20388
20595
 
20389
20596
  // packages/workit-core/src/hooks/run.ts
@@ -20406,7 +20613,7 @@ var PI_DESCRIPTOR = {
20406
20613
  "subagent.stop": { support: "none", native: null },
20407
20614
  "prompt.submit": { support: "undocumented", native: null },
20408
20615
  "compact.pre": { support: "native", native: "session_before_compact" },
20409
- stop: { support: "none", native: null }
20616
+ stop: { support: "native", native: "agent_end" }
20410
20617
  },
20411
20618
  shellPolicy: { deny: "native", channel: "block", failClosed: false },
20412
20619
  context: {
@@ -20426,7 +20633,7 @@ var PI_DESCRIPTOR = {
20426
20633
  },
20427
20634
  provenance: { sessionId: "native", agentIdOnTool: "none", postToolObserve: "native" },
20428
20635
  interaction: { questions: "native", writeBoundary: "native" },
20429
- stopControl: "none",
20636
+ stopControl: "native",
20430
20637
  shellAvailable: "native",
20431
20638
  perEventCost: "low",
20432
20639
  capabilities: [
@@ -21244,9 +21451,29 @@ var persistUncertainCancel = (store, ctx, taskId, workerId, handle, exit) => {
21244
21451
  };
21245
21452
  return observeWorkerExit(lostBinding, handle, exit).ok;
21246
21453
  };
21454
+ var lastAssistantText = (messages) => {
21455
+ for (const message of (messages ?? []).toReversed()) {
21456
+ if (typeof message !== "object" || message === null)
21457
+ continue;
21458
+ const { role, content, stopReason } = message;
21459
+ if (role !== "assistant")
21460
+ continue;
21461
+ if (stopReason === "aborted" || stopReason === "error")
21462
+ return null;
21463
+ if (typeof content === "string")
21464
+ return content;
21465
+ if (!Array.isArray(content))
21466
+ return null;
21467
+ const text = content.filter((part) => typeof part === "object" && part !== null && part.type === "text" && typeof part.text === "string").map((part) => part.text).join(`
21468
+ `);
21469
+ return text || null;
21470
+ }
21471
+ return null;
21472
+ };
21247
21473
  function extension(pi) {
21248
21474
  const sessions = new Set;
21249
21475
  const historyOfferSessions = new Set;
21476
+ const continuedRuns = new Set;
21250
21477
  const workers = new Map;
21251
21478
  const bindings = new Map;
21252
21479
  const childWorker = process.env.WORKIT_PI_WORKER_ID;
@@ -21551,6 +21778,28 @@ ${offer}` : ""}`;
21551
21778
  ${nudge}` : content) };
21552
21779
  return nudge ? { message: { customType: "workit-skill", content: nudge, display: false } } : undefined;
21553
21780
  });
21781
+ pi.on("agent_end", (event, ctx) => {
21782
+ try {
21783
+ const id = ctx.sessionManager.getSessionId();
21784
+ if (continuedRuns.delete(id) || childWorker || !ctx.isProjectTrusted())
21785
+ return;
21786
+ const send = pi.sendMessage;
21787
+ if (typeof send !== "function")
21788
+ return;
21789
+ const decision = stopDecision({
21790
+ ...turnInput(ctx),
21791
+ event: {
21792
+ kind: "stop",
21793
+ lastMessage: lastAssistantText(event.messages),
21794
+ stopHookActive: false
21795
+ }
21796
+ }, PI_DESCRIPTOR);
21797
+ if (decision.kind !== "continue")
21798
+ return;
21799
+ continuedRuns.add(id);
21800
+ send.call(pi, { customType: "workit-stop", content: decision.reason, display: true }, { triggerTurn: true, deliverAs: "followUp" });
21801
+ } catch {}
21802
+ });
21554
21803
  pi.on("session_before_compact", () => {
21555
21804
  return;
21556
21805
  });
@@ -21590,5 +21839,6 @@ ${nudge}` : content) };
21590
21839
  }
21591
21840
  export {
21592
21841
  extension as default,
21842
+ lastAssistantText,
21593
21843
  persistUncertainCancel
21594
21844
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-pi",
3
- "version": "8.7.0",
3
+ "version": "8.8.0",
4
4
  "description": "Workit native extension for Pi — tools, method skills, and session continuity",
5
5
  "keywords": [
6
6
  "agentic-coding",