@brainervirus/workit-opencode 8.7.0 → 8.9.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/plugin.js +339 -85
  2. package/package.json +2 -2
package/dist/plugin.js CHANGED
@@ -19463,12 +19463,48 @@ function writeGate(cwd, paths) {
19463
19463
  return NONE2;
19464
19464
  }
19465
19465
  }
19466
+ // packages/workit-core/src/hooks/stop.ts
19467
+ import { spawnSync as spawnSync16 } from "node:child_process";
19468
+
19466
19469
  // packages/workit-core/src/hooks/raw-git.ts
19467
19470
  import { spawnSync as spawnSync15 } from "node:child_process";
19471
+ import { createHash as createHash12 } from "node:crypto";
19472
+ import fs16 from "node:fs";
19473
+ import os4 from "node:os";
19474
+ import path23 from "node:path";
19475
+
19476
+ // packages/workit-core/src/hooks/session-marker.ts
19468
19477
  import { createHash as createHash11 } from "node:crypto";
19469
19478
  import fs15 from "node:fs";
19470
- import os4 from "node:os";
19471
19479
  import path22 from "node:path";
19480
+ var sessionMarkerFile = (input, session, store) => {
19481
+ let dir = store;
19482
+ if (dir === undefined) {
19483
+ const location = resolveStore(input.cwd);
19484
+ if (location instanceof Error || !location.shared)
19485
+ return null;
19486
+ dir = location.dir;
19487
+ }
19488
+ const name = createHash11("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
19489
+ return path22.join(dir, "hooks", `skills-${name}.json`);
19490
+ };
19491
+ var list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
19492
+ var readSessionMarker = (file) => {
19493
+ try {
19494
+ const value = JSON.parse(fs15.readFileSync(file, "utf8"));
19495
+ return { nudged: list2(value.nudged), loaded: list2(value.loaded), raw: list2(value.raw) };
19496
+ } catch {
19497
+ return { nudged: [], loaded: [], raw: [] };
19498
+ }
19499
+ };
19500
+ var writeSessionMarker = (file, marker) => {
19501
+ try {
19502
+ fs15.mkdirSync(path22.dirname(file), { recursive: true });
19503
+ fs15.writeFileSync(file, JSON.stringify(marker));
19504
+ } catch {}
19505
+ };
19506
+
19507
+ // packages/workit-core/src/hooks/raw-git.ts
19472
19508
  var NONE3 = { kind: "none" };
19473
19509
  var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
19474
19510
  var PREFIXES3 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "nice", "&"]);
@@ -19485,12 +19521,12 @@ var GIT_VALUE_OPTIONS = new Set([
19485
19521
  "--config-env",
19486
19522
  "--super-prefix"
19487
19523
  ]);
19488
- var isAbsolute = (value) => path22.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
19524
+ var isAbsolute = (value) => path23.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
19489
19525
  var joinDir = (base, next) => {
19490
19526
  if (base === undefined || /[$`*?]/.test(next) || next === "-")
19491
19527
  return;
19492
- const expanded = next === "~" || next.startsWith("~/") ? path22.join(os4.homedir(), next.slice(1)) : next;
19493
- return base === null || isAbsolute(expanded) ? expanded : path22.join(base, expanded);
19528
+ const expanded = next === "~" || next.startsWith("~/") ? path23.join(os4.homedir(), next.slice(1)) : next;
19529
+ return base === null || isAbsolute(expanded) ? expanded : path23.join(base, expanded);
19494
19530
  };
19495
19531
  var cdTarget = (words) => {
19496
19532
  const args = words.slice(1);
@@ -19586,6 +19622,37 @@ function rawInvocations(command, dialect = "posix", depth = 0) {
19586
19622
  }
19587
19623
  return found;
19588
19624
  }
19625
+ var LAUNCHERS = new Set([
19626
+ "npx",
19627
+ "bunx",
19628
+ "pnpx",
19629
+ "-y",
19630
+ "--yes",
19631
+ "pnpm",
19632
+ "dlx",
19633
+ "exec",
19634
+ "env",
19635
+ "sudo",
19636
+ "command"
19637
+ ]);
19638
+ var WORKIT_BIN = /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/;
19639
+ function workitInvocations(command, dialect = "posix", depth = 0) {
19640
+ const found = [];
19641
+ for (const { words } of segmentsOf(command, dialect)) {
19642
+ const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
19643
+ if (at < 0)
19644
+ continue;
19645
+ if (WORKIT_BIN.test(words[at])) {
19646
+ found.push(words.slice(at + 1));
19647
+ continue;
19648
+ }
19649
+ const shell = SHELLS.has(commandName(words[at]));
19650
+ const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
19651
+ if (depth < 3 && script !== undefined)
19652
+ found.push(...workitInvocations(script, "posix", depth + 1));
19653
+ }
19654
+ return found;
19655
+ }
19589
19656
  var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
19590
19657
  var pushAction = (args) => {
19591
19658
  let force = false;
@@ -19712,7 +19779,7 @@ var repoAt = (dir) => {
19712
19779
  return { location, dir, branch: resolveTaskKey(dir, location).branch };
19713
19780
  };
19714
19781
  var isWorkitWorkspace = (repo) => {
19715
- if (fs15.existsSync(repo.location.dir))
19782
+ if (fs16.existsSync(repo.location.dir))
19716
19783
  return true;
19717
19784
  try {
19718
19785
  return resolveRuntimeWorkspaceVcs(repo.location.top ?? repo.dir) !== null;
@@ -19743,17 +19810,17 @@ var headSha2 = (repo) => {
19743
19810
  if (!git || git.reftable)
19744
19811
  return null;
19745
19812
  try {
19746
- const head = fs15.readFileSync(path22.join(git.gitDir, "HEAD"), "utf8").trim();
19813
+ const head = fs16.readFileSync(path23.join(git.gitDir, "HEAD"), "utf8").trim();
19747
19814
  if (/^[0-9a-f]{40,64}$/.test(head))
19748
19815
  return head;
19749
19816
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
19750
19817
  if (!ref)
19751
19818
  return null;
19752
- const common = path22.dirname(repo.location.dir);
19819
+ const common = path23.dirname(repo.location.dir);
19753
19820
  try {
19754
- return fs15.readFileSync(path22.join(common, ref), "utf8").trim() || null;
19821
+ return fs16.readFileSync(path23.join(common, ref), "utf8").trim() || null;
19755
19822
  } catch {}
19756
- const packed = fs15.readFileSync(path22.join(common, "packed-refs"), "utf8");
19823
+ const packed = fs16.readFileSync(path23.join(common, "packed-refs"), "utf8");
19757
19824
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
19758
19825
  } catch {
19759
19826
  return null;
@@ -19790,7 +19857,7 @@ var actionsIn = (cwd, command, dialect) => {
19790
19857
  const action = rawAction(invocation);
19791
19858
  if (!action)
19792
19859
  continue;
19793
- const dir = path22.resolve(cwd, invocation.dir ?? ".");
19860
+ const dir = path23.resolve(cwd, invocation.dir ?? ".");
19794
19861
  if (!repos.has(dir)) {
19795
19862
  const repo = repoAt(dir);
19796
19863
  repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
@@ -19808,12 +19875,31 @@ var inWorkitWorkspace = (dir) => {
19808
19875
  var rawDelivery = (cwd, command, dialect) => mentionsRawTool(command) && actionsIn(cwd, command, dialect).some(({ action }) => action.kind !== "commit" && action.kind !== "pr-read");
19809
19876
  var mentionsRawTool = (command) => /\b(?:git|gh|glab)\b/i.test(command);
19810
19877
  var dialectOf = (input) => (input.event.kind === "shell.pre" || input.event.kind === "shell.post" ? input.event.dialect : undefined) ?? "posix";
19811
- function rawGitPre(input, command) {
19878
+ var nudgeKind = (action) => action.kind === "commit" && action.amend ? "commit-amend" : action.kind === "push" && action.blindForce ? "push-force" : action.kind;
19879
+ var unseenNudges = (input, nudges, store) => {
19880
+ const session = input.session.id;
19881
+ const file = session ? sessionMarkerFile(input, session, store) : null;
19882
+ if (!file)
19883
+ return [...nudges.values()];
19884
+ const marker = readSessionMarker(file);
19885
+ const agent = input.session.agentId ? `${input.session.agentId}:` : "";
19886
+ const fresh = [...nudges.keys()].filter((kind) => !marker.raw.includes(agent + kind));
19887
+ if (fresh.length)
19888
+ writeSessionMarker(file, {
19889
+ ...marker,
19890
+ raw: [...marker.raw, ...fresh.map((kind) => agent + kind)]
19891
+ });
19892
+ return fresh.map((kind) => nudges.get(kind));
19893
+ };
19894
+ function rawGitPre(input, command, options = {}) {
19812
19895
  try {
19813
19896
  if (!mentionsRawTool(command))
19814
19897
  return NONE3;
19815
- const nudges = [];
19816
- for (const { action, repo } of actionsIn(input.cwd, command, dialectOf(input))) {
19898
+ const dialect = dialectOf(input);
19899
+ const nudges = new Map;
19900
+ let store = null;
19901
+ for (const { action, repo } of actionsIn(input.cwd, command, dialect)) {
19902
+ store ??= repo.location.dir;
19817
19903
  if (action.kind === "merge")
19818
19904
  return deny(refusal(action, null));
19819
19905
  if (action.kind === "push")
@@ -19823,10 +19909,15 @@ function rawGitPre(input, command) {
19823
19909
  return deny(refusal(action, branch));
19824
19910
  }
19825
19911
  const nudge = nudgeFor(action, input.host, input.session.id);
19826
- if (nudge && !nudges.includes(nudge))
19827
- nudges.push(nudge);
19912
+ if (nudge && !nudges.has(nudgeKind(action)))
19913
+ nudges.set(nudgeKind(action), nudge);
19828
19914
  }
19829
- return nudges.length ? { kind: "context", text: nudges.join(`
19915
+ if (options.nudge === false || !store || nudges.size === 0)
19916
+ return NONE3;
19917
+ if (workitInvocations(command, dialect).length > 0)
19918
+ return NONE3;
19919
+ const shown = unseenNudges(input, nudges, store);
19920
+ return shown.length ? { kind: "context", text: shown.join(`
19830
19921
  `) } : NONE3;
19831
19922
  } catch {
19832
19923
  return NONE3;
@@ -19834,8 +19925,8 @@ function rawGitPre(input, command) {
19834
19925
  }
19835
19926
  var deny = (reason) => ({ kind: "deny", reason, unblock: null });
19836
19927
  var MARKER_TTL_SECONDS = 3600;
19837
- var markerFile = (repo, session, key) => path22.join(repo.location.dir, "hooks", `commit-${createHash11("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
19838
- var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash11("sha256").update(command).digest("hex")}`;
19928
+ var markerFile = (repo, session, key) => path23.join(repo.location.dir, "hooks", `commit-${createHash12("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
19929
+ var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash12("sha256").update(command).digest("hex")}`;
19839
19930
  function noteRawCommit(input, command, key) {
19840
19931
  try {
19841
19932
  if (!input.session.id || !/\bcommit\b/.test(command))
@@ -19845,8 +19936,8 @@ function noteRawCommit(input, command, key) {
19845
19936
  if (action.kind !== "commit")
19846
19937
  continue;
19847
19938
  const file = markerFile(repo, input.session.id, key);
19848
- fs15.mkdirSync(path22.dirname(file), { recursive: true });
19849
- fs15.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
19939
+ fs16.mkdirSync(path23.dirname(file), { recursive: true });
19940
+ fs16.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
19850
19941
  `);
19851
19942
  }
19852
19943
  } catch {}
@@ -19869,11 +19960,11 @@ var headCommit = (dir) => {
19869
19960
  var settle2 = (input, repo, file) => {
19870
19961
  let marker;
19871
19962
  try {
19872
- marker = JSON.parse(fs15.readFileSync(file, "utf8"));
19963
+ marker = JSON.parse(fs16.readFileSync(file, "utf8"));
19873
19964
  } catch {
19874
19965
  return;
19875
19966
  }
19876
- fs15.rmSync(file, { force: true });
19967
+ fs16.rmSync(file, { force: true });
19877
19968
  const session = input.session.id;
19878
19969
  const now = Date.now() / 1000;
19879
19970
  if (typeof marker.at !== "number" || now - marker.at > MARKER_TTL_SECONDS)
@@ -19914,11 +20005,161 @@ function rawGitPost(input, command, key) {
19914
20005
  settle2(input, repo, markerFile(repo, input.session.id, key));
19915
20006
  } catch {}
19916
20007
  }
19917
- // packages/workit-core/src/hooks/skill-nudge.ts
19918
- import { createHash as createHash12 } from "node:crypto";
19919
- import fs16 from "node:fs";
19920
- import path23 from "node:path";
19921
20008
 
20009
+ // packages/workit-core/src/hooks/stop.ts
20010
+ var NONE4 = { kind: "none" };
20011
+ var QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
20012
+ 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;
20013
+ var YES_NO = /[[(]\s*y(?:es)?\s*\/\s*n(?:o)?\s*[\])]/i;
20014
+ var LIST_ITEM = /^\s*(?:[-*+•]|\d+[.)]|[a-z][.)])\s+/i;
20015
+ var asksUser = (message) => {
20016
+ if (!message)
20017
+ return false;
20018
+ const paragraphs = message.replace(/```[\s\S]*?(?:```|$)/g, `
20019
+
20020
+ `).split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
20021
+ while (paragraphs.length > 1 && paragraphs.at(-1).split(`
20022
+ `).every((line) => LIST_ITEM.test(line)))
20023
+ paragraphs.pop();
20024
+ const tail = paragraphs.slice(-2).join(`
20025
+ `);
20026
+ return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
20027
+ };
20028
+ var short = (sha) => (sha ?? "?").slice(0, 12);
20029
+ var gitList = (cwd, args) => {
20030
+ const run = spawnSync16("git", args, {
20031
+ cwd,
20032
+ encoding: "utf8",
20033
+ timeout: 3000,
20034
+ windowsHide: true,
20035
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" }
20036
+ });
20037
+ return run.status === 0 ? run.stdout.split(`
20038
+ `).filter(Boolean) : null;
20039
+ };
20040
+ var gitLine = (cwd, args) => gitList(cwd, args)?.[0] ?? null;
20041
+ var sessionOf = (row) => (typeof row.session === "string" ? row.session : null) ?? row.actor.session;
20042
+ var prState = (row) => typeof row.state === "string" ? row.state : "open";
20043
+ var ended = (row) => row.observer === "workit_cli" && (row.type === "pr.merged" || row.type === "pr.status" && prState(row) !== "open");
20044
+ var openPr = (rows) => {
20045
+ const seen = rows.findLast((row) => (row.type === "pr.created" || row.type === "pr.status") && row.observer === "workit_cli" && typeof row.pr === "number");
20046
+ if (!seen || ended(seen))
20047
+ return null;
20048
+ const closed = rows.some((row) => ended(row) && row.pr === seen.pr && row.seq > seen.seq);
20049
+ return closed ? null : seen.pr;
20050
+ };
20051
+ var mergedHeads = (rows) => [
20052
+ ...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))
20053
+ ].slice(-3);
20054
+ var localCommit = (cwd, sha) => /^[0-9a-f]{7,64}$/.test(sha) && gitList(cwd, ["cat-file", "-e", `${sha}^{commit}`]) !== null;
20055
+ var upstreamGone = (cwd, branch) => gitLine(cwd, ["for-each-ref", "--format=%(upstream:track)", `refs/heads/${branch}`]) === "[gone]";
20056
+ var landed = (cwd, branch, rows, head, mine) => {
20057
+ if (!head)
20058
+ return false;
20059
+ const merged = mergedHeads(rows);
20060
+ if (merged.length === 0)
20061
+ return upstreamGone(cwd, branch);
20062
+ if (merged.includes(head))
20063
+ return true;
20064
+ if (mine.size === 0)
20065
+ return false;
20066
+ return merged.some((at) => {
20067
+ const after = gitList(cwd, ["rev-list", `--max-count=${UNPUSHED_SCAN}`, "HEAD", "--not", at]);
20068
+ return after !== null && !after.some((sha) => mine.has(sha));
20069
+ });
20070
+ };
20071
+ var UNPUSHED_SCAN = 500;
20072
+ var unpushed = (cwd, branch, endpoint, mine, merged) => {
20073
+ const remote = pushRemoteName(cwd, branch);
20074
+ if (!remote || mine.size === 0)
20075
+ return null;
20076
+ const listed = gitList(cwd, [
20077
+ "rev-list",
20078
+ `--max-count=${UNPUSHED_SCAN}`,
20079
+ "HEAD",
20080
+ "--not",
20081
+ `--remotes=${remote}`,
20082
+ ...merged.filter((sha) => localCommit(cwd, sha))
20083
+ ]);
20084
+ const count = (listed ?? []).filter((sha) => mine.has(sha)).length;
20085
+ if (count === 0)
20086
+ return null;
20087
+ return {
20088
+ kind: "unpushed",
20089
+ 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`)" : ""}.`
20090
+ };
20091
+ };
20092
+ var redChecks = (rows, pr, head, endpoint) => {
20093
+ const status = rows.findLast((row) => row.type === "pr.status" && row.observer === "workit_cli" && row.pr === pr);
20094
+ const state = status && status.head === head && prState(status) === "open" ? status.checks : null;
20095
+ if (state !== "failing" && state !== "pending")
20096
+ return null;
20097
+ return {
20098
+ kind: "checks",
20099
+ 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)" : ""}.`
20100
+ };
20101
+ };
20102
+ var unverified = (cwd, branch, all, descriptor) => {
20103
+ const check = checkVerdicts(cwd, branch, all);
20104
+ if (check.accepted.accepted || check.accepted.reasons.includes("failing_verdict"))
20105
+ return null;
20106
+ const has = check.review === "self-reviewed" ? "only a self verdict" : "no current verdict";
20107
+ const verifier = `${descriptor.subagents.agentPrefix}verifier`;
20108
+ return {
20109
+ kind: "verdict",
20110
+ 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.`
20111
+ };
20112
+ };
20113
+ function stopObligation(input, descriptor) {
20114
+ try {
20115
+ const session = input.session.id;
20116
+ if (!session || input.session.agentId || !inWorkitWorkspace(input.cwd))
20117
+ return null;
20118
+ const branch = currentBranch(input.cwd);
20119
+ if (!branch || guardedBranch(input.cwd, branch))
20120
+ return null;
20121
+ const ledger = readLedger(input.cwd);
20122
+ if (!ledger.ok)
20123
+ return null;
20124
+ const all = ledger.value.rows;
20125
+ const rows = all.filter((row) => row.branch === branch);
20126
+ const mine = rows.filter((row) => sessionOf(row) === session);
20127
+ 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"));
20128
+ const delivered = mine.some((row) => row.observer === "workit_cli" && (row.type === "pr.created" || row.type === "delivery.verified"));
20129
+ if (commits.size === 0 && !delivered)
20130
+ return null;
20131
+ const head = gitLine(input.cwd, ["rev-parse", "HEAD"]);
20132
+ if (landed(input.cwd, branch, rows, head, commits))
20133
+ return null;
20134
+ const endpoint = resolveAutonomy(input.cwd).effectiveEndpoint;
20135
+ if (endpoint !== "commit") {
20136
+ const left = unpushed(input.cwd, branch, endpoint, commits, mergedHeads(rows));
20137
+ if (left)
20138
+ return left;
20139
+ }
20140
+ const pr = openPr(rows);
20141
+ if ((endpoint === "green" || endpoint === "merged") && pr !== null && head) {
20142
+ const left = redChecks(rows, pr, head, endpoint);
20143
+ if (left)
20144
+ return left;
20145
+ }
20146
+ return delivered ? unverified(input.cwd, branch, all, descriptor) : null;
20147
+ } catch {
20148
+ return null;
20149
+ }
20150
+ }
20151
+ function stopDecision(input, descriptor) {
20152
+ const event = input.event;
20153
+ if (event.kind !== "stop" || event.stopHookActive)
20154
+ return NONE4;
20155
+ if (!event.lastMessage?.trim() || asksUser(event.lastMessage))
20156
+ return NONE4;
20157
+ const left = stopObligation(input, descriptor);
20158
+ return left ? {
20159
+ kind: "continue",
20160
+ reason: `Workit: before stopping, ${left.reason} If this cannot be done now, say so to the user and stop.`
20161
+ } : NONE4;
20162
+ }
19922
20163
  // packages/workit-core/src/core/skill-manifests.ts
19923
20164
  var WORKIT_METHOD_SKILLS = [
19924
20165
  "workit-shape",
@@ -20027,27 +20268,6 @@ var promptTrigger = (prompt) => {
20027
20268
  }
20028
20269
  return null;
20029
20270
  };
20030
- var markerFile2 = (input, session) => {
20031
- const location = resolveStore(input.cwd);
20032
- if (location instanceof Error || !location.shared)
20033
- return null;
20034
- const name = createHash12("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
20035
- return path23.join(location.dir, "hooks", `skills-${name}.json`);
20036
- };
20037
- var readMarker = (file) => {
20038
- try {
20039
- const value = JSON.parse(fs16.readFileSync(file, "utf8"));
20040
- return { nudged: value.nudged ?? [], loaded: value.loaded ?? [] };
20041
- } catch {
20042
- return { nudged: [], loaded: [] };
20043
- }
20044
- };
20045
- var writeMarker = (file, marker) => {
20046
- try {
20047
- fs16.mkdirSync(path23.dirname(file), { recursive: true });
20048
- fs16.writeFileSync(file, JSON.stringify(marker));
20049
- } catch {}
20050
- };
20051
20271
  var loadedSkills = (cwd, session) => {
20052
20272
  const ledger = readLedger(cwd);
20053
20273
  const loaded = new Set;
@@ -20079,13 +20299,13 @@ function recordSkillLoad(input, name, via) {
20079
20299
  return;
20080
20300
  if (!inWorkitWorkspace(input.cwd))
20081
20301
  return;
20082
- const file = markerFile2(input, session);
20302
+ const file = sessionMarkerFile(input, session);
20083
20303
  if (!file)
20084
20304
  return;
20085
- const marker = readMarker(file);
20305
+ const marker = readSessionMarker(file);
20086
20306
  if (marker.loaded.includes(skill))
20087
20307
  return;
20088
- writeMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
20308
+ writeSessionMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
20089
20309
  if (loadedSkills(input.cwd, session).has(skill))
20090
20310
  return;
20091
20311
  appendHookObserved(input.cwd, {
@@ -20097,23 +20317,24 @@ function recordSkillLoad(input, name, via) {
20097
20317
  } catch {}
20098
20318
  }
20099
20319
  var nudgeOnce = (input, session, skill, line) => {
20100
- const file = markerFile2(input, session);
20320
+ const file = sessionMarkerFile(input, session);
20101
20321
  if (!file)
20102
20322
  return null;
20103
- const marker = readMarker(file);
20323
+ const marker = readSessionMarker(file);
20104
20324
  if (marker.nudged.includes(skill) || marker.loaded.includes(skill))
20105
20325
  return null;
20106
20326
  if (loadedSkills(input.cwd, session).has(skill))
20107
20327
  return null;
20108
- writeMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
20328
+ writeSessionMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
20109
20329
  return line;
20110
20330
  };
20111
20331
  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.`;
20112
20332
  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;
20333
+ var relayedMessage = (prompt) => /^\s*(?:Another Claude session sent a message\b|<agent-message\s+from=|\[SYSTEM NOTIFICATION\b|<task-notification>)/.test(prompt);
20113
20334
  function promptNudge(input, prompt) {
20114
20335
  try {
20115
20336
  const session = input.session.id;
20116
- if (!prompt || !session || input.session.agentId)
20337
+ if (!prompt || !session || input.session.agentId || relayedMessage(prompt))
20117
20338
  return null;
20118
20339
  const loads = promptLoads(prompt);
20119
20340
  if (loads) {
@@ -20129,32 +20350,7 @@ function promptNudge(input, prompt) {
20129
20350
  }
20130
20351
  }
20131
20352
  var WORKIT_DELIVERY = /^(?:pr (?:create|merge)|ci wait|git push)\b/;
20132
- var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
20133
- var LAUNCHERS = new Set([
20134
- "npx",
20135
- "bunx",
20136
- "pnpx",
20137
- "-y",
20138
- "--yes",
20139
- "pnpm",
20140
- "dlx",
20141
- "exec",
20142
- "env",
20143
- "sudo",
20144
- "command"
20145
- ]);
20146
- var runsWorkitDelivery = (command, dialect, depth = 0) => segmentsOf(command, dialect).some(({ words }) => {
20147
- const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
20148
- if (at >= 0 && /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/.test(words[at])) {
20149
- const args = words.slice(at + 1);
20150
- if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run"))
20151
- return false;
20152
- return WORKIT_DELIVERY.test(args.join(" "));
20153
- }
20154
- const shell = SHELLS2.has(words[at]?.split(/[\\/]/).at(-1) ?? "");
20155
- const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
20156
- return depth < 3 && script !== undefined && runsWorkitDelivery(script, "posix", depth + 1);
20157
- });
20353
+ var runsWorkitDelivery = (command, dialect) => workitInvocations(command, dialect).some((args) => !args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run") && WORKIT_DELIVERY.test(args.join(" ")));
20158
20354
  var isDeliveryCommand = (cwd, command, dialect = "posix") => rawDelivery(cwd, command, dialect) || runsWorkitDelivery(command, dialect) && inWorkitWorkspace(cwd);
20159
20355
  function shipNudge(input, command) {
20160
20356
  try {
@@ -20181,7 +20377,7 @@ ${line}` };
20181
20377
  };
20182
20378
  var shellNudge = (input, command) => withContextLine(rawGitPre(input, command), shipNudge(input, command));
20183
20379
  // packages/workit-core/src/hooks/hosts/fields.ts
20184
- var SHELLS3 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
20380
+ var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
20185
20381
  var WRITE_TOOLS = new Set([
20186
20382
  "write",
20187
20383
  "edit",
@@ -20219,6 +20415,7 @@ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
20219
20415
  var SOURCES2 = new Set(["startup", "resume", "clear", "compact", "fork"]);
20220
20416
 
20221
20417
  // packages/workit-core/src/hooks/hosts/cursor.ts
20418
+ var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
20222
20419
  var BLOCKING2 = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
20223
20420
 
20224
20421
  // packages/workit-core/src/hooks/run.ts
@@ -20241,7 +20438,7 @@ var OPENCODE_DESCRIPTOR = {
20241
20438
  "subagent.stop": { support: "native", native: 'tool.hook("execute.after") subagent' },
20242
20439
  "prompt.submit": { support: "undocumented", native: null },
20243
20440
  "compact.pre": { support: "native", native: 'session.hook("compaction")' },
20244
- stop: { support: "none", native: null }
20441
+ stop: { support: "partial", native: 'event("session.idle")' }
20245
20442
  },
20246
20443
  shellPolicy: { deny: "native", channel: "effect", failClosed: false },
20247
20444
  context: {
@@ -20409,7 +20606,7 @@ var ensureHygieneFiles = (root, opts) => {
20409
20606
  // packages/workit-core/src/core/init.ts
20410
20607
  import fs17 from "node:fs";
20411
20608
  import path27 from "node:path";
20412
- import { spawnSync as spawnSync16 } from "node:child_process";
20609
+ import { spawnSync as spawnSync17 } from "node:child_process";
20413
20610
 
20414
20611
  // packages/workit-core/src/core/workspace-branch-policy.ts
20415
20612
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
@@ -20532,7 +20729,7 @@ var resolveInitProvider = (cwd) => {
20532
20729
  return env.toLowerCase();
20533
20730
  const root = cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
20534
20731
  try {
20535
- const out = spawnSync16("git", ["remote", "get-url", "origin"], {
20732
+ const out = spawnSync17("git", ["remote", "get-url", "origin"], {
20536
20733
  cwd: root,
20537
20734
  encoding: "utf8"
20538
20735
  });
@@ -21442,7 +21639,7 @@ var evaluateShellPermission = (root, event, workdirs = new Map) => {
21442
21639
  permissionMode: null,
21443
21640
  transcriptPath: null,
21444
21641
  event: { kind: "shell.pre", command: resource, toolUseId: null }
21445
- }, resource);
21642
+ }, resource, { nudge: false });
21446
21643
  if (raw.kind === "deny")
21447
21644
  return deny(raw.reason);
21448
21645
  const writes = shellWrites(resource);
@@ -21547,6 +21744,56 @@ var injectSkillNudge = (root, session, messages, system) => {
21547
21744
  } catch {}
21548
21745
  };
21549
21746
 
21747
+ // packages/workit-opencode/src/v2/stop.ts
21748
+ var record3 = (value) => typeof value === "object" && value !== null ? value : {};
21749
+ var lastAssistantText = (messages) => {
21750
+ for (const message of messages.toReversed()) {
21751
+ if (record3(message).type !== "assistant")
21752
+ continue;
21753
+ const content = record3(message).content;
21754
+ if (!Array.isArray(content))
21755
+ return null;
21756
+ const text = content.filter((part) => record3(part).type === "text" && typeof record3(part).text === "string").map((part) => record3(part).text).join(`
21757
+ `);
21758
+ return text || null;
21759
+ }
21760
+ return null;
21761
+ };
21762
+ var createStopControl = (root, port) => {
21763
+ const continued = new Set;
21764
+ return {
21765
+ onIdle: async (sessionID) => {
21766
+ try {
21767
+ if (continued.delete(sessionID))
21768
+ return false;
21769
+ const session = await port.getSession(sessionID);
21770
+ if (!session || session.parentID !== undefined)
21771
+ return false;
21772
+ if (session.outcome !== "succeeded")
21773
+ return false;
21774
+ if (!sameWorkspace(root, session.directory))
21775
+ return false;
21776
+ const lastMessage = lastAssistantText(await port.messages(sessionID));
21777
+ const decision = stopDecision({
21778
+ host: "opencode",
21779
+ cwd: root,
21780
+ session: { id: sessionID, agentId: null, agentType: null, parentId: null },
21781
+ permissionMode: null,
21782
+ transcriptPath: null,
21783
+ event: { kind: "stop", lastMessage, stopHookActive: false }
21784
+ }, OPENCODE_DESCRIPTOR);
21785
+ if (decision.kind !== "continue")
21786
+ return false;
21787
+ continued.add(sessionID);
21788
+ await port.resume(sessionID, decision.reason);
21789
+ return true;
21790
+ } catch {
21791
+ return false;
21792
+ }
21793
+ }
21794
+ };
21795
+ };
21796
+
21550
21797
  // packages/workit-opencode/src/v2/registry.ts
21551
21798
  import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
21552
21799
  import path33 from "node:path";
@@ -21827,11 +22074,18 @@ var setup = async (ctx) => {
21827
22074
  });
21828
22075
  const registeredSkills = await registerSkills(ctx);
21829
22076
  await registerCommands(ctx, registeredSkills);
22077
+ const stopControl = createStopControl(root, {
22078
+ getSession: (sessionID) => sessionFacts(ctx, sessionID),
22079
+ messages: async (sessionID) => await ctx.session.context({ sessionID }),
22080
+ resume: (sessionID, text) => ctx.session.synthetic({ sessionID, text, delivery: "queue", resume: true })
22081
+ });
21830
22082
  const subscription = new AbortController;
21831
22083
  (async () => {
21832
22084
  try {
21833
22085
  for await (const event of ctx.event.subscribe({ signal: subscription.signal })) {
21834
22086
  await lifecycle.handleEvent(event);
22087
+ if (event.type === "session.idle")
22088
+ stopControl.onIdle(String(event.data.sessionID));
21835
22089
  }
21836
22090
  } catch {}
21837
22091
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "8.7.0",
3
+ "version": "8.9.0",
4
4
  "private": false,
5
5
  "description": "Workit OpenCode plugin — native tools, adaptive policy, and sixteen method skills",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "typecheck": "tsc --noEmit"
40
40
  },
41
41
  "dependencies": {
42
- "@brainervirus/workit-core": "^8.7.0"
42
+ "@brainervirus/workit-core": "^8.9.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@opencode/plugin": "2.0.18"