@brainervirus/workit-pi 8.4.1 → 8.5.2

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.
package/dist/workit.js CHANGED
@@ -18965,6 +18965,8 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
18965
18965
  };
18966
18966
  });
18967
18967
  // packages/workit-core/src/hooks/context.ts
18968
+ import { createHash as createHash11 } from "node:crypto";
18969
+ import fs15 from "node:fs";
18968
18970
  import path20 from "node:path";
18969
18971
  var MIGRATION_PENDING_NOTE = "workit migration pending — run `workit task status`";
18970
18972
  var migrationPending = (result) => !result.ok && result.code === "needs_input" && (result.error ?? "").startsWith(MIGRATION_PENDING);
@@ -19112,6 +19114,75 @@ var turnContextText = (input, descriptor) => {
19112
19114
  return null;
19113
19115
  }
19114
19116
  };
19117
+ var TURN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
19118
+ var digestOf = (text) => createHash11("sha256").update(text).digest("hex");
19119
+ var turnMarker = (input) => {
19120
+ if (!input.session.id)
19121
+ return null;
19122
+ try {
19123
+ const location = resolveStore(input.cwd);
19124
+ if (location instanceof Error)
19125
+ return null;
19126
+ const session = [input.host, input.session.id, input.session.agentId ?? ""].join("\x00");
19127
+ return path20.join(location.dir, "hooks", `turn-${digestOf(session).slice(0, 32)}.json`);
19128
+ } catch {
19129
+ return null;
19130
+ }
19131
+ };
19132
+ var writeTurnDigest = (file, digest) => {
19133
+ try {
19134
+ fs15.mkdirSync(path20.dirname(file), { recursive: true });
19135
+ fs15.writeFileSync(file, `${JSON.stringify({ digest })}
19136
+ `);
19137
+ } catch {}
19138
+ };
19139
+ var clearTurnDigest = (file) => {
19140
+ try {
19141
+ fs15.rmSync(file, { force: true });
19142
+ } catch {}
19143
+ };
19144
+ var pruneTurnDigests = (dir, now) => {
19145
+ try {
19146
+ for (const name of fs15.readdirSync(dir)) {
19147
+ if (!name.startsWith("turn-"))
19148
+ continue;
19149
+ const file = path20.join(dir, name);
19150
+ try {
19151
+ if (now - fs15.statSync(file).mtimeMs > TURN_TTL_MS)
19152
+ fs15.rmSync(file, { force: true });
19153
+ } catch {}
19154
+ }
19155
+ } catch {}
19156
+ };
19157
+ var changedTurnContext = (input, text) => {
19158
+ const file = turnMarker(input);
19159
+ if (!text) {
19160
+ if (file)
19161
+ clearTurnDigest(file);
19162
+ return null;
19163
+ }
19164
+ if (!file)
19165
+ return text;
19166
+ const digest = digestOf(text);
19167
+ try {
19168
+ const previous = JSON.parse(fs15.readFileSync(file, "utf8"));
19169
+ if (previous.digest === digest)
19170
+ return null;
19171
+ } catch {}
19172
+ writeTurnDigest(file, digest);
19173
+ return text;
19174
+ };
19175
+ var seedTurnContext = (input, descriptor) => {
19176
+ const file = turnMarker(input);
19177
+ if (!file)
19178
+ return;
19179
+ pruneTurnDigests(path20.dirname(file), Date.now());
19180
+ const text = turnContextText(input, descriptor);
19181
+ if (text)
19182
+ writeTurnDigest(file, digestOf(text));
19183
+ else
19184
+ clearTurnDigest(file);
19185
+ };
19115
19186
  // packages/workit-core/src/hooks/policy.ts
19116
19187
  var shellPolicy = (cwd, command) => {
19117
19188
  const policy = shellBranchPolicyViolation(cwd, command);
@@ -19405,7 +19476,7 @@ function writeGate(cwd, paths) {
19405
19476
  const gate = gateFor(cwd);
19406
19477
  if (!gate || gate.blockers.length === 0)
19407
19478
  return NONE2;
19408
- if (paths && paths.every((file) => exempt(cwd, gate.root, file, gate.plans)))
19479
+ if (paths?.length && paths.every((file) => exempt(cwd, gate.root, file, gate.plans)))
19409
19480
  return NONE2;
19410
19481
  const reasons = gate.blockers.map((item) => `${item.ruleId}: ${item.reason}`).join("; ");
19411
19482
  return {
@@ -19419,8 +19490,8 @@ function writeGate(cwd, paths) {
19419
19490
  }
19420
19491
  // packages/workit-core/src/hooks/raw-git.ts
19421
19492
  import { spawnSync as spawnSync15 } from "node:child_process";
19422
- import { createHash as createHash11 } from "node:crypto";
19423
- import fs15 from "node:fs";
19493
+ import { createHash as createHash12 } from "node:crypto";
19494
+ import fs16 from "node:fs";
19424
19495
  import os4 from "node:os";
19425
19496
  import path22 from "node:path";
19426
19497
  var NONE3 = { kind: "none" };
@@ -19666,7 +19737,7 @@ var repoAt = (dir) => {
19666
19737
  return { location, dir, branch: resolveTaskKey(dir, location).branch };
19667
19738
  };
19668
19739
  var isWorkitWorkspace = (repo) => {
19669
- if (fs15.existsSync(repo.location.dir))
19740
+ if (fs16.existsSync(repo.location.dir))
19670
19741
  return true;
19671
19742
  try {
19672
19743
  return resolveRuntimeWorkspaceVcs(repo.location.top ?? repo.dir) !== null;
@@ -19697,7 +19768,7 @@ var headSha2 = (repo) => {
19697
19768
  if (!git || git.reftable)
19698
19769
  return null;
19699
19770
  try {
19700
- const head = fs15.readFileSync(path22.join(git.gitDir, "HEAD"), "utf8").trim();
19771
+ const head = fs16.readFileSync(path22.join(git.gitDir, "HEAD"), "utf8").trim();
19701
19772
  if (/^[0-9a-f]{40,64}$/.test(head))
19702
19773
  return head;
19703
19774
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
@@ -19705,9 +19776,9 @@ var headSha2 = (repo) => {
19705
19776
  return null;
19706
19777
  const common = path22.dirname(repo.location.dir);
19707
19778
  try {
19708
- return fs15.readFileSync(path22.join(common, ref), "utf8").trim() || null;
19779
+ return fs16.readFileSync(path22.join(common, ref), "utf8").trim() || null;
19709
19780
  } catch {}
19710
- const packed = fs15.readFileSync(path22.join(common, "packed-refs"), "utf8");
19781
+ const packed = fs16.readFileSync(path22.join(common, "packed-refs"), "utf8");
19711
19782
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
19712
19783
  } catch {
19713
19784
  return null;
@@ -19755,6 +19826,11 @@ var actionsIn = (cwd, command, dialect) => {
19755
19826
  }
19756
19827
  return out;
19757
19828
  };
19829
+ var inWorkitWorkspace = (dir) => {
19830
+ const repo = repoAt(dir);
19831
+ return repo !== null && isWorkitWorkspace(repo);
19832
+ };
19833
+ var rawDelivery = (cwd, command, dialect) => mentionsRawTool(command) && actionsIn(cwd, command, dialect).some(({ action }) => action.kind !== "commit" && action.kind !== "pr-read");
19758
19834
  var mentionsRawTool = (command) => /\b(?:git|gh|glab)\b/i.test(command);
19759
19835
  var dialectOf = (input) => (input.event.kind === "shell.pre" || input.event.kind === "shell.post" ? input.event.dialect : undefined) ?? "posix";
19760
19836
  function rawGitPre(input, command) {
@@ -19784,8 +19860,8 @@ function rawGitPre(input, command) {
19784
19860
  var deny = (reason) => ({ kind: "deny", reason, unblock: null });
19785
19861
  var NEXT_COMMAND = "next";
19786
19862
  var MARKER_TTL_SECONDS = 3600;
19787
- var markerFile = (repo, session, key) => path22.join(repo.location.dir, "hooks", `commit-${createHash11("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
19788
- var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash11("sha256").update(command).digest("hex")}`;
19863
+ var markerFile = (repo, session, key) => path22.join(repo.location.dir, "hooks", `commit-${createHash12("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
19864
+ var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash12("sha256").update(command).digest("hex")}`;
19789
19865
  function noteRawCommit(input, command, key) {
19790
19866
  try {
19791
19867
  if (!input.session.id || !/\bcommit\b/.test(command))
@@ -19795,8 +19871,8 @@ function noteRawCommit(input, command, key) {
19795
19871
  if (action.kind !== "commit")
19796
19872
  continue;
19797
19873
  const file = markerFile(repo, input.session.id, key);
19798
- fs15.mkdirSync(path22.dirname(file), { recursive: true });
19799
- fs15.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
19874
+ fs16.mkdirSync(path22.dirname(file), { recursive: true });
19875
+ fs16.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
19800
19876
  `);
19801
19877
  }
19802
19878
  } catch {}
@@ -19819,11 +19895,11 @@ var headCommit = (dir) => {
19819
19895
  var settle2 = (input, repo, file) => {
19820
19896
  let marker;
19821
19897
  try {
19822
- marker = JSON.parse(fs15.readFileSync(file, "utf8"));
19898
+ marker = JSON.parse(fs16.readFileSync(file, "utf8"));
19823
19899
  } catch {
19824
19900
  return;
19825
19901
  }
19826
- fs15.rmSync(file, { force: true });
19902
+ fs16.rmSync(file, { force: true });
19827
19903
  const session = input.session.id;
19828
19904
  const now = Date.now() / 1000;
19829
19905
  if (typeof marker.at !== "number" || now - marker.at > MARKER_TTL_SECONDS)
@@ -19872,14 +19948,294 @@ function settlePendingCommit(input) {
19872
19948
  if (!repo)
19873
19949
  return;
19874
19950
  const file = markerFile(repo, input.session.id, NEXT_COMMAND);
19875
- if (fs15.existsSync(file))
19951
+ if (fs16.existsSync(file))
19876
19952
  settle2(input, repo, file);
19877
19953
  } catch {}
19878
19954
  }
19955
+ // packages/workit-core/src/hooks/skill-nudge.ts
19956
+ import { createHash as createHash13 } from "node:crypto";
19957
+ import fs17 from "node:fs";
19958
+ import path23 from "node:path";
19959
+
19960
+ // packages/workit-core/src/core/skill-manifests.ts
19961
+ var WORKIT_METHOD_SKILLS = [
19962
+ "workit-shape",
19963
+ "workit-implement",
19964
+ "workit-review",
19965
+ "workit-debug",
19966
+ "workit-ship",
19967
+ "workit-continue",
19968
+ "workit-bdd",
19969
+ "workit-test-audit",
19970
+ "workit-deslop",
19971
+ "workit-fanout",
19972
+ "workit-verify-app",
19973
+ "workit-retro",
19974
+ "workit-architecture"
19975
+ ];
19976
+ var WORKIT_SKILL_ALIASES = {
19977
+ "wk-shape": "workit-shape",
19978
+ "wk-implement": "workit-implement",
19979
+ "wk-review": "workit-review",
19980
+ "wk-debug": "workit-debug",
19981
+ "wk-ship": "workit-ship",
19982
+ "wk-continue": "workit-continue",
19983
+ "wk-bdd": "workit-bdd",
19984
+ "wk-test-audit": "workit-test-audit",
19985
+ "wk-deslop": "workit-deslop",
19986
+ "wk-fanout": "workit-fanout",
19987
+ "wk-verify-app": "workit-verify-app",
19988
+ "wk-retro": "workit-retro",
19989
+ "wk-architecture": "workit-architecture"
19990
+ };
19991
+ var skillLoadWording = (host, skill) => {
19992
+ switch (host) {
19993
+ case "claude-code":
19994
+ return `call the Skill tool with \`workit:${skill.replace(/^workit-/, "")}\``;
19995
+ case "opencode":
19996
+ return `call the skill tool with \`${skill}\``;
19997
+ default:
19998
+ return `read the \`${skill}\` skill's SKILL.md and follow it`;
19999
+ }
20000
+ };
20001
+ if (false) {}
20002
+
20003
+ // packages/workit-core/src/hooks/skill-nudge.ts
20004
+ var USER_INVOKED = new Set(["workit-retro", "workit-architecture"]);
20005
+ var SKILL_HOST = {
20006
+ claude_code: "claude-code",
20007
+ opencode: "opencode",
20008
+ codex_cli: "codex",
20009
+ codex_desktop: "codex",
20010
+ cursor: "cursor",
20011
+ pi: "pi"
20012
+ };
20013
+ var PROMPT_INTENTS = [
20014
+ [
20015
+ "workit-debug",
20016
+ /\b(flaky|regressed|(?:is|introduced) a regression|regression (?:in|after|since|from)|(?:is|are|got|looks) broken|debug (?:this|it|the|why)|fix (?:the|this|a) bug)\b/i
20017
+ ],
20018
+ [
20019
+ "workit-continue",
20020
+ /\b(pick up where|where (?:were|did) we|resume (?:the|this|my|our|work|where)|hand ?off)\b/i
20021
+ ],
20022
+ [
20023
+ "workit-review",
20024
+ /\b(blast radius|code review|review (?:the|this|my|PR|pr|#\d+|branch|diff|changes))\b/i
20025
+ ],
20026
+ [
20027
+ "workit-ship",
20028
+ /\b(ship (?:it|this|the)|babysit|open (?:a|the) (?:PR|pr|MR|mr)(?=\s*$|\s+(?:for|to|on|with|and|against|into|from|after|once)\b|[.!,;])|merge (?:the|this|my) (?:PR|pr|MR|mr|branch)|(?:get|until|make) CI (?:is )?green)\b/i
20029
+ ],
20030
+ ["workit-bdd", /\b(BDD|TDD|acceptance criteria|Given\/When\/Then)(?![\w/])/],
20031
+ ["workit-test-audit", /\b(test audit|audit the tests|tautolog\w*|weak tests)\b/i],
20032
+ ["workit-deslop", /\b(deslop|slop|dead code)\b/i],
20033
+ [
20034
+ "workit-fanout",
20035
+ /\b(fan(?: |-)?out|parallelize|parallel agents|agent swarm|swarm of agents)\b/i
20036
+ ],
20037
+ ["workit-verify-app", /\b(verify the app|smoke test|prove it works)\b/i],
20038
+ ["workit-retro", /\b((?:a|the) retro(?![-\w]|\.\w)|retrospective)/i],
20039
+ ["workit-architecture", /\b(deepen (?:the )?modules|architecture (?:review|pass|audit))\b/i],
20040
+ [
20041
+ "workit-shape",
20042
+ /\b(brainstorm\w*|grill me|should we|let'?s plan|plan (?:out|how|the|a|for)|(?:write|draft) (?:a|the) spec)\b/i
20043
+ ],
20044
+ [
20045
+ "workit-implement",
20046
+ /\b(implement (?:the|this|a|it)|add (?:a|the) feature|build (?:a|an|the) (?:new )?(?:feature|command|verb|page|endpoint|component|cli|tool))\b/i
20047
+ ]
20048
+ ];
20049
+ var workitSkillOf = (name) => {
20050
+ const bare = name.trim().replace(/^\//, "").toLowerCase();
20051
+ const alias = WORKIT_SKILL_ALIASES[bare];
20052
+ if (alias)
20053
+ return alias;
20054
+ const skill = bare.replace(/^workit:/, "workit-");
20055
+ return WORKIT_METHOD_SKILLS.includes(skill) ? skill : null;
20056
+ };
20057
+ var promptTrigger = (prompt) => {
20058
+ const text = prompt.replace(/```[\s\S]*?```/g, " ").slice(0, 4000);
20059
+ for (const [skill, pattern] of PROMPT_INTENTS) {
20060
+ const match = pattern.exec(text);
20061
+ if (match)
20062
+ return { skill, trigger: match[1] ?? match[0] };
20063
+ }
20064
+ return null;
20065
+ };
20066
+ var markerFile2 = (input, session) => {
20067
+ const location = resolveStore(input.cwd);
20068
+ if (location instanceof Error || !location.shared)
20069
+ return null;
20070
+ const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
20071
+ return path23.join(location.dir, "hooks", `skills-${name}.json`);
20072
+ };
20073
+ var readMarker = (file) => {
20074
+ try {
20075
+ const value = JSON.parse(fs17.readFileSync(file, "utf8"));
20076
+ return { nudged: value.nudged ?? [], loaded: value.loaded ?? [] };
20077
+ } catch {
20078
+ return { nudged: [], loaded: [] };
20079
+ }
20080
+ };
20081
+ var writeMarker = (file, marker) => {
20082
+ try {
20083
+ fs17.mkdirSync(path23.dirname(file), { recursive: true });
20084
+ fs17.writeFileSync(file, JSON.stringify(marker));
20085
+ } catch {}
20086
+ };
20087
+ var loadedSkills = (cwd, session) => {
20088
+ const ledger = readLedger(cwd);
20089
+ const loaded = new Set;
20090
+ if (!ledger.ok)
20091
+ return loaded;
20092
+ for (const row of ledger.value.rows)
20093
+ if (row.type === "skill.loaded" && row.actor.session === session && typeof row.skill === "string")
20094
+ loaded.add(row.skill);
20095
+ return loaded;
20096
+ };
20097
+ var SKILL_FILE = /(?:^|[\\/])(workit-[a-z-]+)[\\/]SKILL\.md$/;
20098
+ var skillFileIn = (file) => SKILL_FILE.exec(file)?.[1] ?? null;
20099
+ var READERS = new Set([
20100
+ "cat",
20101
+ "sed",
20102
+ "head",
20103
+ "tail",
20104
+ "less",
20105
+ "more",
20106
+ "bat",
20107
+ "nl",
20108
+ "get-content",
20109
+ "gc",
20110
+ "type"
20111
+ ]);
20112
+ var skillReadIn = (command, dialect = "posix") => {
20113
+ for (const { words } of segmentsOf(command, dialect)) {
20114
+ if (!READERS.has((words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase()))
20115
+ continue;
20116
+ if (words.some((word) => /^(?:-i(?:\.\w+)?|--in-place(?:=.*)?)$/.test(word)))
20117
+ continue;
20118
+ for (const word of words.slice(1)) {
20119
+ const skill = skillFileIn(word);
20120
+ if (skill)
20121
+ return skill;
20122
+ }
20123
+ }
20124
+ return null;
20125
+ };
20126
+ function recordSkillLoad(input, name, via) {
20127
+ try {
20128
+ const skill = workitSkillOf(name);
20129
+ const session = input.session.id;
20130
+ if (!skill || !session)
20131
+ return;
20132
+ if (!inWorkitWorkspace(input.cwd))
20133
+ return;
20134
+ const file = markerFile2(input, session);
20135
+ if (!file)
20136
+ return;
20137
+ const marker = readMarker(file);
20138
+ if (marker.loaded.includes(skill))
20139
+ return;
20140
+ writeMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
20141
+ if (loadedSkills(input.cwd, session).has(skill))
20142
+ return;
20143
+ appendHookObserved(input.cwd, {
20144
+ type: "skill.loaded",
20145
+ actor: { host: input.host, session, agentId: input.session.agentId },
20146
+ skill,
20147
+ via
20148
+ });
20149
+ } catch {}
20150
+ }
20151
+ var nudgeOnce = (input, session, skill, line) => {
20152
+ const file = markerFile2(input, session);
20153
+ if (!file)
20154
+ return null;
20155
+ const marker = readMarker(file);
20156
+ if (marker.nudged.includes(skill) || marker.loaded.includes(skill))
20157
+ return null;
20158
+ if (loadedSkills(input.cwd, session).has(skill))
20159
+ return null;
20160
+ writeMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
20161
+ return line;
20162
+ };
20163
+ 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.`;
20164
+ 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;
20165
+ function promptNudge(input, prompt) {
20166
+ try {
20167
+ const session = input.session.id;
20168
+ if (!prompt || !session || input.session.agentId)
20169
+ return null;
20170
+ const loads = promptLoads(prompt);
20171
+ if (loads) {
20172
+ recordSkillLoad(input, loads, "slash");
20173
+ return null;
20174
+ }
20175
+ const hit = promptTrigger(prompt);
20176
+ if (!hit || !inWorkitWorkspace(input.cwd))
20177
+ return null;
20178
+ return nudgeOnce(input, session, hit.skill, nudgeText(input, hit.skill, `this request ("${hit.trigger}")`));
20179
+ } catch {
20180
+ return null;
20181
+ }
20182
+ }
20183
+ var WORKIT_DELIVERY = /^(?:pr (?:create|merge)|ci wait|git push)\b/;
20184
+ var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
20185
+ var LAUNCHERS = new Set([
20186
+ "npx",
20187
+ "bunx",
20188
+ "pnpx",
20189
+ "-y",
20190
+ "--yes",
20191
+ "pnpm",
20192
+ "dlx",
20193
+ "exec",
20194
+ "env",
20195
+ "sudo",
20196
+ "command"
20197
+ ]);
20198
+ var runsWorkitDelivery = (command, dialect, depth = 0) => segmentsOf(command, dialect).some(({ words }) => {
20199
+ const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
20200
+ if (at >= 0 && /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/.test(words[at])) {
20201
+ const args = words.slice(at + 1);
20202
+ if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run"))
20203
+ return false;
20204
+ return WORKIT_DELIVERY.test(args.join(" "));
20205
+ }
20206
+ const shell = SHELLS2.has(words[at]?.split(/[\\/]/).at(-1) ?? "");
20207
+ const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
20208
+ return depth < 3 && script !== undefined && runsWorkitDelivery(script, "posix", depth + 1);
20209
+ });
20210
+ var isDeliveryCommand = (cwd, command, dialect = "posix") => rawDelivery(cwd, command, dialect) || runsWorkitDelivery(command, dialect) && inWorkitWorkspace(cwd);
20211
+ function shipNudge(input, command) {
20212
+ try {
20213
+ const session = input.session.id;
20214
+ if (!session || input.session.agentId)
20215
+ return null;
20216
+ const dialect = input.event.kind === "shell.pre" ? input.event.dialect ?? "posix" : "posix";
20217
+ if (!isDeliveryCommand(input.cwd, command, dialect))
20218
+ return null;
20219
+ return nudgeOnce(input, session, "workit-ship", nudgeText(input, "workit-ship", "this delivery step"));
20220
+ } catch {
20221
+ return null;
20222
+ }
20223
+ }
20224
+ var withContextLine = (decision, line) => {
20225
+ if (!line)
20226
+ return decision;
20227
+ if (decision.kind === "none")
20228
+ return { kind: "context", text: line };
20229
+ if (decision.kind === "context")
20230
+ return { kind: "context", text: `${decision.text}
20231
+ ${line}` };
20232
+ return decision;
20233
+ };
20234
+ var shellNudge = (input, command) => withContextLine(rawGitPre(input, command), shipNudge(input, command));
19879
20235
  // packages/workit-core/src/hooks/hosts/fields.ts
19880
20236
  var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
19881
20237
  var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
19882
- var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
20238
+ var SHELLS3 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
19883
20239
  var WRITE_TOOLS = new Set([
19884
20240
  "write",
19885
20241
  "edit",
@@ -19900,30 +20256,45 @@ var WRITE_TOOLS = new Set([
19900
20256
  "writefile"
19901
20257
  ]);
19902
20258
  var PATH_KEYS = ["file_path", "filePath", "notebook_path", "path", "target_file", "file"];
20259
+ var PATCH_HEADER = /^[ \t]*\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/gm;
19903
20260
  var writePaths = (input) => {
19904
20261
  if (!isRecord4(input))
19905
20262
  return [];
19906
20263
  const paths = PATH_KEYS.map((key) => input[key]).filter(nonEmpty2);
19907
20264
  for (const value of Object.values(input))
19908
20265
  if (typeof value === "string")
19909
- for (const match of value.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm))
20266
+ for (const match of value.matchAll(PATCH_HEADER))
19910
20267
  paths.push(match[1].trim());
19911
20268
  return [...new Set(paths)];
19912
20269
  };
19913
20270
  // packages/workit-core/src/hooks/handle.ts
19914
20271
  var usable2 = (value) => value === "native" || value === "partial";
19915
20272
  var NONE4 = { kind: "none" };
20273
+ var resendsOnChange = (descriptor) => descriptor.context.turnResend === "on-change" && usable2(descriptor.context.perTurn);
19916
20274
  var offered = new Set;
19917
- var CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer";
19918
- var CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
20275
+ var ROLES = {
20276
+ implementer: "implementer",
20277
+ verifier: "judge",
20278
+ reviewer: "judge"
20279
+ };
20280
+ var workitRole = (descriptor, agentType) => {
20281
+ const prefix = descriptor.subagents.agentPrefix;
20282
+ const name = agentType.startsWith(prefix) ? agentType.slice(prefix.length) : "";
20283
+ return Object.hasOwn(ROLES, name) ? ROLES[name] : null;
20284
+ };
19919
20285
  var subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128);
19920
- var subagentStartText = (host, session, descriptor, event) => {
19921
- if (host === "claude_code" && event.agentType === CLAUDE_WORKTREE_IMPLEMENTER)
19922
- return `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) working in its own git worktree: it may edit and commit there, 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.`;
19923
- const readOnly = `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided.`;
19924
- if (host === "claude_code" && event.agentType && CLAUDE_JUDGES.has(event.agentType) && event.agentId)
19925
- return `${readOnly} Its own Workit session is ${subagentSession(session, event.agentId)}: record verdicts with \`workit ledger verdict <result> --session ${subagentSession(session, event.agentId)} ...\` so the ledger tells it apart from the author.`;
19926
- return readOnly;
20286
+ var subagentStartText = (session, descriptor, event) => {
20287
+ const role = workitRole(descriptor, event.agentType);
20288
+ const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
20289
+ if (role === "implementer") {
20290
+ 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";
20291
+ 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.`;
20292
+ }
20293
+ const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
20294
+ if (role !== "judge" || !event.agentId)
20295
+ return readOnly;
20296
+ const own = subagentSession(session, event.agentId);
20297
+ 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.`;
19927
20298
  };
19928
20299
  function handleHook(input, deps) {
19929
20300
  const { descriptor } = deps;
@@ -19939,18 +20310,22 @@ function handleHook(input, deps) {
19939
20310
  const advisory = usable2(descriptor.events["write.pre"].support) ? null : `Workit cannot gate file writes on ${descriptor.label} (no pre-write hook): before-write requirements (an open product choice, a missing plan) are advisory here, so settle them before editing.`;
19940
20311
  const addendum = [deps.addendum, advisory].filter(Boolean).join(`
19941
20312
  `) || null;
19942
- return {
19943
- kind: "context",
19944
- text: sessionContextText(input, descriptor, { offer, addendum })
19945
- };
20313
+ const text = sessionContextText(input, descriptor, { offer, addendum });
20314
+ if (resendsOnChange(descriptor))
20315
+ seedTurnContext(input, descriptor);
20316
+ return { kind: "context", text };
19946
20317
  }
19947
20318
  case "context.turn": {
19948
20319
  if (!usable2(descriptor.context.perTurn))
19949
20320
  return NONE4;
19950
- const text = turnContextText(input, descriptor);
19951
- return text ? { kind: "context", text } : NONE4;
20321
+ const current = turnContextText(input, descriptor);
20322
+ const text = resendsOnChange(descriptor) ? changedTurnContext(input, current) : current;
20323
+ return withContextLine(text ? { kind: "context", text } : NONE4, promptNudge(input, event.prompt));
19952
20324
  }
19953
20325
  case "shell.pre": {
20326
+ const skillRead = skillReadIn(event.command, event.dialect);
20327
+ if (skillRead)
20328
+ recordSkillLoad(input, skillRead, "read");
19954
20329
  const canDeny = usable2(descriptor.shellPolicy.deny);
19955
20330
  const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE4;
19956
20331
  if (policy.kind !== "none")
@@ -19966,7 +20341,7 @@ function handleHook(input, deps) {
19966
20341
  if (gate.kind === "deny")
19967
20342
  return gate;
19968
20343
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
19969
- return raw;
20344
+ return withContextLine(raw, shipNudge(input, event.command));
19970
20345
  }
19971
20346
  case "shell.post":
19972
20347
  if (usable2(descriptor.events["shell.post"].support))
@@ -19977,7 +20352,7 @@ function handleHook(input, deps) {
19977
20352
  case "subagent.start":
19978
20353
  return {
19979
20354
  kind: "context",
19980
- text: subagentStartText(input.host, input.session.id ?? null, descriptor, event)
20355
+ text: subagentStartText(input.session.id ?? null, descriptor, event)
19981
20356
  };
19982
20357
  case "compact.pre":
19983
20358
  return {
@@ -19985,6 +20360,9 @@ function handleHook(input, deps) {
19985
20360
  userMessage: "Workit context may be stale after compaction; re-run inspection or resume before acting."
19986
20361
  };
19987
20362
  case "tool.pre":
20363
+ if (event.skill)
20364
+ recordSkillLoad(input, event.skill, "tool");
20365
+ return NONE4;
19988
20366
  case "subagent.stop":
19989
20367
  case "prompt.submit":
19990
20368
  case "stop":
@@ -20004,7 +20382,7 @@ var CONTEXT_EVENTS = new Set([
20004
20382
 
20005
20383
  // packages/workit-core/src/hooks/hosts/codex.ts
20006
20384
  var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
20007
- var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
20385
+ var SOURCES2 = new Set(["startup", "resume", "clear", "compact", "fork"]);
20008
20386
 
20009
20387
  // packages/workit-core/src/hooks/hosts/cursor.ts
20010
20388
  var BLOCKING2 = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
@@ -20034,16 +20412,18 @@ var PI_DESCRIPTOR = {
20034
20412
  shellPolicy: { deny: "native", channel: "block", failClosed: false },
20035
20413
  context: {
20036
20414
  sessionStart: "native",
20037
- perTurn: "native",
20415
+ perTurn: "partial",
20038
20416
  afterCompact: "native",
20039
- task: "session-bound"
20417
+ task: "session-bound",
20418
+ turnResend: "per-session"
20040
20419
  },
20041
20420
  subagents: {
20042
20421
  identity: "none",
20043
20422
  parentBinding: "none",
20044
20423
  blockStart: "none",
20045
20424
  worktreeIsolation: "none",
20046
- maxConcurrency: "undocumented"
20425
+ maxConcurrency: "undocumented",
20426
+ agentPrefix: "workit-"
20047
20427
  },
20048
20428
  provenance: { sessionId: "native", agentIdOnTool: "none", postToolObserve: "native" },
20049
20429
  interaction: { questions: "native", writeBoundary: "native" },
@@ -20120,24 +20500,6 @@ var unfinishedTaskOffer2 = (ctx) => {
20120
20500
  }
20121
20501
  };
20122
20502
 
20123
- // packages/workit-core/src/core/skill-manifests.ts
20124
- var WORKIT_SKILL_ALIASES = {
20125
- "wk-shape": "workit-shape",
20126
- "wk-implement": "workit-implement",
20127
- "wk-review": "workit-review",
20128
- "wk-debug": "workit-debug",
20129
- "wk-ship": "workit-ship",
20130
- "wk-continue": "workit-continue",
20131
- "wk-bdd": "workit-bdd",
20132
- "wk-test-audit": "workit-test-audit",
20133
- "wk-deslop": "workit-deslop",
20134
- "wk-fanout": "workit-fanout",
20135
- "wk-verify-app": "workit-verify-app",
20136
- "wk-retro": "workit-retro",
20137
- "wk-architecture": "workit-architecture"
20138
- };
20139
- if (false) {}
20140
-
20141
20503
  // packages/workit-pi/src/tools.ts
20142
20504
  var readOnlyActions = new Set(["list", "inspect", "preview", "explain", "export"]);
20143
20505
  var output = (result) => ({
@@ -20227,6 +20589,13 @@ var enforceToolPolicy = (event, ctx) => {
20227
20589
  const command = event.input?.command;
20228
20590
  return typeof command === "string" ? gate({ kind: "shell.pre", command, toolUseId: event.toolCallId ?? null }) : undefined;
20229
20591
  }
20592
+ if (event.toolName === "read") {
20593
+ const file = event.input?.path;
20594
+ const skill = typeof file === "string" ? skillFileIn(file) : null;
20595
+ if (skill)
20596
+ recordSkillLoad(hookInput(ctx, { kind: "tool.pre", tool: "read", toolUseId: null }), skill, "read");
20597
+ return;
20598
+ }
20230
20599
  if (event.toolName !== "write" && event.toolName !== "edit")
20231
20600
  return;
20232
20601
  if (!ctx.isProjectTrusted())
@@ -20253,22 +20622,23 @@ var observeToolResult = (event, ctx) => {
20253
20622
  toolUseId: event.toolCallId
20254
20623
  });
20255
20624
  handleHook(post, { descriptor: PI_DESCRIPTOR, addendum: null });
20256
- const nudge = rawGitPre(post, command);
20625
+ const nudge = process.env.WORKIT_PI_WORKER_SESSION ? rawGitPre(post, command) : shellNudge(post, command);
20257
20626
  return nudge.kind === "context" ? { content: [...event.content, { type: "text", text: nudge.text }] } : undefined;
20258
20627
  } catch {
20259
20628
  return;
20260
20629
  }
20261
20630
  };
20631
+ var skillPromptNudge = (prompt, ctx) => process.env.WORKIT_PI_WORKER_SESSION ? null : promptNudge(hookInput(ctx, { kind: "context.turn", prompt }), prompt);
20262
20632
 
20263
20633
  // packages/workit-pi/src/launches.ts
20264
20634
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
20265
20635
  import os5 from "node:os";
20266
- import path23 from "node:path";
20636
+ import path24 from "node:path";
20267
20637
  var launchDir = (root) => {
20268
20638
  const location = resolveStore(root);
20269
- return location instanceof Error ? null : path23.join(location.dir, "pi-launches");
20639
+ return location instanceof Error ? null : path24.join(location.dir, "pi-launches");
20270
20640
  };
20271
- var fileFor = (dir, workerId) => path23.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
20641
+ var fileFor = (dir, workerId) => path24.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
20272
20642
  var recordLaunch = (root, attempt) => {
20273
20643
  const dir = launchDir(root);
20274
20644
  if (!dir)
@@ -20314,7 +20684,7 @@ var abandonedLaunches = (root) => {
20314
20684
  const found = [];
20315
20685
  for (const name of names) {
20316
20686
  try {
20317
- const attempt = JSON.parse(readFileSync6(path23.join(dir, name), "utf8"));
20687
+ const attempt = JSON.parse(readFileSync6(path24.join(dir, name), "utf8"));
20318
20688
  if (typeof attempt.workerId === "string" && typeof attempt.session === "string") {
20319
20689
  if (!alive(attempt))
20320
20690
  found.push(attempt);
@@ -21142,14 +21512,18 @@ function extension(pi) {
21142
21512
  sessions.delete(ctx.sessionManager.getSessionId());
21143
21513
  reconcileLostWorkers(ctx);
21144
21514
  });
21145
- pi.on("before_agent_start", (_event, ctx) => {
21515
+ pi.on("before_agent_start", (event, ctx) => {
21516
+ const nudge = skillPromptNudge(event.prompt, ctx);
21146
21517
  const id = ctx.sessionManager.getSessionId();
21147
21518
  if (sessions.has(id))
21148
- return;
21519
+ return nudge ? { message: { customType: "workit-skill", content: nudge, display: false } } : undefined;
21149
21520
  sessions.add(id);
21150
21521
  const offer = historyOfferSessions.has(id) ? null : unfinishedTaskOffer2(ctx);
21151
21522
  historyOfferSessions.add(id);
21152
- return { message: contextMessage(ctx, offer) };
21523
+ const message = contextMessage(ctx, offer);
21524
+ return { message: nudge ? { ...message, content: `${message.content}
21525
+
21526
+ ${nudge}` } : message };
21153
21527
  });
21154
21528
  pi.on("session_before_compact", () => {
21155
21529
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-pi",
3
- "version": "8.4.1",
3
+ "version": "8.5.2",
4
4
  "description": "Workit native extension for Pi — tools, method skills, and session continuity",
5
5
  "keywords": [
6
6
  "agentic-coding",
@@ -15,8 +15,8 @@ proposes and stops. Nothing changes until the user approves.
15
15
  `retro:` row in `workit ledger list --type decision`), else the last ~10
16
16
  sessions. State the window in one line.
17
17
  2. **Read through workit, cheapest first.** Every finding cites these:
18
- - `workit ledger list --last 200`: rulings (ambiguities the agent had to
19
- settle), failed or self verdicts, handoffs, check runs, CI reruns.
18
+ - `workit ledger list --last 200`: rulings, failed/self verdicts, handoffs,
19
+ check runs, CI reruns, `skill.loaded` rows (skipped skills: routing).
20
20
  - `workit pr status --pr <n>` for each recent PR: threads, failing checks.
21
21
  - `git log --oneline -50`, plus reverts and fixups after review.
22
22
  - `workit knowledge lint`: today's AGENTS.md and need-based files.