@brainervirus/workit-pi 8.4.1 → 8.5.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.
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,289 @@ 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|regression|(?: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)|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
+ ["workit-fanout", /\b(fan(?: |-)?out|parallelize|parallel agents|swarm)\b/i],
20034
+ ["workit-verify-app", /\b(verify the app|smoke test|prove it works)\b/i],
20035
+ ["workit-retro", /\b(retro|retrospective)\b(?!\.\w)/i],
20036
+ ["workit-architecture", /\b(deepen (?:the )?modules|architecture (?:review|pass|audit))\b/i],
20037
+ [
20038
+ "workit-shape",
20039
+ /\b(brainstorm\w*|grill me|should we|let'?s plan|plan (?:out|how|the|a|for)|(?:write|draft) (?:a|the) spec)\b/i
20040
+ ],
20041
+ [
20042
+ "workit-implement",
20043
+ /\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
20044
+ ]
20045
+ ];
20046
+ var workitSkillOf = (name) => {
20047
+ const bare = name.trim().replace(/^\//, "").toLowerCase();
20048
+ const alias = WORKIT_SKILL_ALIASES[bare];
20049
+ if (alias)
20050
+ return alias;
20051
+ const skill = bare.replace(/^workit:/, "workit-");
20052
+ return WORKIT_METHOD_SKILLS.includes(skill) ? skill : null;
20053
+ };
20054
+ var promptTrigger = (prompt) => {
20055
+ const text = prompt.replace(/```[\s\S]*?```/g, " ").slice(0, 4000);
20056
+ for (const [skill, pattern] of PROMPT_INTENTS) {
20057
+ const match = pattern.exec(text);
20058
+ if (match)
20059
+ return { skill, trigger: match[1] ?? match[0] };
20060
+ }
20061
+ return null;
20062
+ };
20063
+ var markerFile2 = (input, session) => {
20064
+ const location = resolveStore(input.cwd);
20065
+ if (location instanceof Error || !location.shared)
20066
+ return null;
20067
+ const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
20068
+ return path23.join(location.dir, "hooks", `skills-${name}.json`);
20069
+ };
20070
+ var readMarker = (file) => {
20071
+ try {
20072
+ const value = JSON.parse(fs17.readFileSync(file, "utf8"));
20073
+ return { nudged: value.nudged ?? [], loaded: value.loaded ?? [] };
20074
+ } catch {
20075
+ return { nudged: [], loaded: [] };
20076
+ }
20077
+ };
20078
+ var writeMarker = (file, marker) => {
20079
+ try {
20080
+ fs17.mkdirSync(path23.dirname(file), { recursive: true });
20081
+ fs17.writeFileSync(file, JSON.stringify(marker));
20082
+ } catch {}
20083
+ };
20084
+ var loadedSkills = (cwd, session) => {
20085
+ const ledger = readLedger(cwd);
20086
+ const loaded = new Set;
20087
+ if (!ledger.ok)
20088
+ return loaded;
20089
+ for (const row of ledger.value.rows)
20090
+ if (row.type === "skill.loaded" && row.actor.session === session && typeof row.skill === "string")
20091
+ loaded.add(row.skill);
20092
+ return loaded;
20093
+ };
20094
+ var SKILL_FILE = /(?:^|[\\/])(workit-[a-z-]+)[\\/]SKILL\.md$/;
20095
+ var skillFileIn = (file) => SKILL_FILE.exec(file)?.[1] ?? null;
20096
+ var READERS = new Set([
20097
+ "cat",
20098
+ "sed",
20099
+ "head",
20100
+ "tail",
20101
+ "less",
20102
+ "more",
20103
+ "bat",
20104
+ "nl",
20105
+ "get-content",
20106
+ "gc",
20107
+ "type"
20108
+ ]);
20109
+ var skillReadIn = (command, dialect = "posix") => {
20110
+ for (const { words } of segmentsOf(command, dialect)) {
20111
+ if (!READERS.has((words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase()))
20112
+ continue;
20113
+ for (const word of words.slice(1)) {
20114
+ const skill = skillFileIn(word);
20115
+ if (skill)
20116
+ return skill;
20117
+ }
20118
+ }
20119
+ return null;
20120
+ };
20121
+ function recordSkillLoad(input, name, via) {
20122
+ try {
20123
+ const skill = workitSkillOf(name);
20124
+ const session = input.session.id;
20125
+ if (!skill || !session)
20126
+ return;
20127
+ if (!inWorkitWorkspace(input.cwd))
20128
+ return;
20129
+ const file = markerFile2(input, session);
20130
+ if (!file)
20131
+ return;
20132
+ const marker = readMarker(file);
20133
+ if (marker.loaded.includes(skill))
20134
+ return;
20135
+ writeMarker(file, { ...marker, loaded: [...marker.loaded, skill] });
20136
+ if (loadedSkills(input.cwd, session).has(skill))
20137
+ return;
20138
+ appendHookObserved(input.cwd, {
20139
+ type: "skill.loaded",
20140
+ actor: { host: input.host, session, agentId: input.session.agentId },
20141
+ skill,
20142
+ via
20143
+ });
20144
+ } catch {}
20145
+ }
20146
+ var nudgeOnce = (input, session, skill, line) => {
20147
+ const file = markerFile2(input, session);
20148
+ if (!file)
20149
+ return null;
20150
+ const marker = readMarker(file);
20151
+ if (marker.nudged.includes(skill) || marker.loaded.includes(skill))
20152
+ return null;
20153
+ if (loadedSkills(input.cwd, session).has(skill))
20154
+ return null;
20155
+ writeMarker(file, { ...marker, nudged: [...marker.nudged, skill] });
20156
+ return line;
20157
+ };
20158
+ 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.`;
20159
+ 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;
20160
+ function promptNudge(input, prompt) {
20161
+ try {
20162
+ const session = input.session.id;
20163
+ if (!prompt || !session || input.session.agentId)
20164
+ return null;
20165
+ const loads = promptLoads(prompt);
20166
+ if (loads) {
20167
+ recordSkillLoad(input, loads, "slash");
20168
+ return null;
20169
+ }
20170
+ const hit = promptTrigger(prompt);
20171
+ if (!hit || !inWorkitWorkspace(input.cwd))
20172
+ return null;
20173
+ return nudgeOnce(input, session, hit.skill, nudgeText(input, hit.skill, `this request ("${hit.trigger}")`));
20174
+ } catch {
20175
+ return null;
20176
+ }
20177
+ }
20178
+ var WORKIT_DELIVERY = /^(?:pr (?:create|merge)|ci wait|git push)\b/;
20179
+ var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
20180
+ var LAUNCHERS = new Set([
20181
+ "npx",
20182
+ "bunx",
20183
+ "pnpx",
20184
+ "-y",
20185
+ "--yes",
20186
+ "pnpm",
20187
+ "dlx",
20188
+ "exec",
20189
+ "env",
20190
+ "sudo",
20191
+ "command"
20192
+ ]);
20193
+ var runsWorkitDelivery = (command, dialect, depth = 0) => segmentsOf(command, dialect).some(({ words }) => {
20194
+ const at = words.findIndex((word) => !/^\w+=/.test(word) && !LAUNCHERS.has(word));
20195
+ if (at >= 0 && /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/.test(words[at])) {
20196
+ const args = words.slice(at + 1);
20197
+ if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run"))
20198
+ return false;
20199
+ return WORKIT_DELIVERY.test(args.join(" "));
20200
+ }
20201
+ const shell = SHELLS2.has(words[at]?.split(/[\\/]/).at(-1) ?? "");
20202
+ const script = shell && words[at + 1] === "-c" ? words[at + 2] : undefined;
20203
+ return depth < 3 && script !== undefined && runsWorkitDelivery(script, "posix", depth + 1);
20204
+ });
20205
+ var isDeliveryCommand = (cwd, command, dialect = "posix") => rawDelivery(cwd, command, dialect) || runsWorkitDelivery(command, dialect) && inWorkitWorkspace(cwd);
20206
+ function shipNudge(input, command) {
20207
+ try {
20208
+ const session = input.session.id;
20209
+ if (!session || input.session.agentId)
20210
+ return null;
20211
+ const dialect = input.event.kind === "shell.pre" ? input.event.dialect ?? "posix" : "posix";
20212
+ if (!isDeliveryCommand(input.cwd, command, dialect))
20213
+ return null;
20214
+ return nudgeOnce(input, session, "workit-ship", nudgeText(input, "workit-ship", "this delivery step"));
20215
+ } catch {
20216
+ return null;
20217
+ }
20218
+ }
20219
+ var withContextLine = (decision, line) => {
20220
+ if (!line)
20221
+ return decision;
20222
+ if (decision.kind === "none")
20223
+ return { kind: "context", text: line };
20224
+ if (decision.kind === "context")
20225
+ return { kind: "context", text: `${decision.text}
20226
+ ${line}` };
20227
+ return decision;
20228
+ };
20229
+ var shellNudge = (input, command) => withContextLine(rawGitPre(input, command), shipNudge(input, command));
19879
20230
  // packages/workit-core/src/hooks/hosts/fields.ts
19880
20231
  var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
19881
20232
  var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
19882
- var SHELLS2 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
20233
+ var SHELLS3 = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
19883
20234
  var WRITE_TOOLS = new Set([
19884
20235
  "write",
19885
20236
  "edit",
@@ -19900,30 +20251,45 @@ var WRITE_TOOLS = new Set([
19900
20251
  "writefile"
19901
20252
  ]);
19902
20253
  var PATH_KEYS = ["file_path", "filePath", "notebook_path", "path", "target_file", "file"];
20254
+ var PATCH_HEADER = /^[ \t]*\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/gm;
19903
20255
  var writePaths = (input) => {
19904
20256
  if (!isRecord4(input))
19905
20257
  return [];
19906
20258
  const paths = PATH_KEYS.map((key) => input[key]).filter(nonEmpty2);
19907
20259
  for (const value of Object.values(input))
19908
20260
  if (typeof value === "string")
19909
- for (const match of value.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm))
20261
+ for (const match of value.matchAll(PATCH_HEADER))
19910
20262
  paths.push(match[1].trim());
19911
20263
  return [...new Set(paths)];
19912
20264
  };
19913
20265
  // packages/workit-core/src/hooks/handle.ts
19914
20266
  var usable2 = (value) => value === "native" || value === "partial";
19915
20267
  var NONE4 = { kind: "none" };
20268
+ var resendsOnChange = (descriptor) => descriptor.context.turnResend === "on-change" && usable2(descriptor.context.perTurn);
19916
20269
  var offered = new Set;
19917
- var CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer";
19918
- var CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
20270
+ var ROLES = {
20271
+ implementer: "implementer",
20272
+ verifier: "judge",
20273
+ reviewer: "judge"
20274
+ };
20275
+ var workitRole = (descriptor, agentType) => {
20276
+ const prefix = descriptor.subagents.agentPrefix;
20277
+ const name = agentType.startsWith(prefix) ? agentType.slice(prefix.length) : "";
20278
+ return Object.hasOwn(ROLES, name) ? ROLES[name] : null;
20279
+ };
19919
20280
  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;
20281
+ var subagentStartText = (session, descriptor, event) => {
20282
+ const role = workitRole(descriptor, event.agentType);
20283
+ const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
20284
+ if (role === "implementer") {
20285
+ 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";
20286
+ 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.`;
20287
+ }
20288
+ const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
20289
+ if (role !== "judge" || !event.agentId)
20290
+ return readOnly;
20291
+ const own = subagentSession(session, event.agentId);
20292
+ 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
20293
  };
19928
20294
  function handleHook(input, deps) {
19929
20295
  const { descriptor } = deps;
@@ -19939,18 +20305,22 @@ function handleHook(input, deps) {
19939
20305
  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
20306
  const addendum = [deps.addendum, advisory].filter(Boolean).join(`
19941
20307
  `) || null;
19942
- return {
19943
- kind: "context",
19944
- text: sessionContextText(input, descriptor, { offer, addendum })
19945
- };
20308
+ const text = sessionContextText(input, descriptor, { offer, addendum });
20309
+ if (resendsOnChange(descriptor))
20310
+ seedTurnContext(input, descriptor);
20311
+ return { kind: "context", text };
19946
20312
  }
19947
20313
  case "context.turn": {
19948
20314
  if (!usable2(descriptor.context.perTurn))
19949
20315
  return NONE4;
19950
- const text = turnContextText(input, descriptor);
19951
- return text ? { kind: "context", text } : NONE4;
20316
+ const current = turnContextText(input, descriptor);
20317
+ const text = resendsOnChange(descriptor) ? changedTurnContext(input, current) : current;
20318
+ return withContextLine(text ? { kind: "context", text } : NONE4, promptNudge(input, event.prompt));
19952
20319
  }
19953
20320
  case "shell.pre": {
20321
+ const skillRead = skillReadIn(event.command, event.dialect);
20322
+ if (skillRead)
20323
+ recordSkillLoad(input, skillRead, "read");
19954
20324
  const canDeny = usable2(descriptor.shellPolicy.deny);
19955
20325
  const policy = canDeny ? shellPolicy(input.cwd, event.command) : NONE4;
19956
20326
  if (policy.kind !== "none")
@@ -19966,7 +20336,7 @@ function handleHook(input, deps) {
19966
20336
  if (gate.kind === "deny")
19967
20337
  return gate;
19968
20338
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
19969
- return raw;
20339
+ return withContextLine(raw, shipNudge(input, event.command));
19970
20340
  }
19971
20341
  case "shell.post":
19972
20342
  if (usable2(descriptor.events["shell.post"].support))
@@ -19977,7 +20347,7 @@ function handleHook(input, deps) {
19977
20347
  case "subagent.start":
19978
20348
  return {
19979
20349
  kind: "context",
19980
- text: subagentStartText(input.host, input.session.id ?? null, descriptor, event)
20350
+ text: subagentStartText(input.session.id ?? null, descriptor, event)
19981
20351
  };
19982
20352
  case "compact.pre":
19983
20353
  return {
@@ -19985,6 +20355,9 @@ function handleHook(input, deps) {
19985
20355
  userMessage: "Workit context may be stale after compaction; re-run inspection or resume before acting."
19986
20356
  };
19987
20357
  case "tool.pre":
20358
+ if (event.skill)
20359
+ recordSkillLoad(input, event.skill, "tool");
20360
+ return NONE4;
19988
20361
  case "subagent.stop":
19989
20362
  case "prompt.submit":
19990
20363
  case "stop":
@@ -20004,7 +20377,7 @@ var CONTEXT_EVENTS = new Set([
20004
20377
 
20005
20378
  // packages/workit-core/src/hooks/hosts/codex.ts
20006
20379
  var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
20007
- var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
20380
+ var SOURCES2 = new Set(["startup", "resume", "clear", "compact", "fork"]);
20008
20381
 
20009
20382
  // packages/workit-core/src/hooks/hosts/cursor.ts
20010
20383
  var BLOCKING2 = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
@@ -20034,16 +20407,18 @@ var PI_DESCRIPTOR = {
20034
20407
  shellPolicy: { deny: "native", channel: "block", failClosed: false },
20035
20408
  context: {
20036
20409
  sessionStart: "native",
20037
- perTurn: "native",
20410
+ perTurn: "partial",
20038
20411
  afterCompact: "native",
20039
- task: "session-bound"
20412
+ task: "session-bound",
20413
+ turnResend: "per-session"
20040
20414
  },
20041
20415
  subagents: {
20042
20416
  identity: "none",
20043
20417
  parentBinding: "none",
20044
20418
  blockStart: "none",
20045
20419
  worktreeIsolation: "none",
20046
- maxConcurrency: "undocumented"
20420
+ maxConcurrency: "undocumented",
20421
+ agentPrefix: "workit-"
20047
20422
  },
20048
20423
  provenance: { sessionId: "native", agentIdOnTool: "none", postToolObserve: "native" },
20049
20424
  interaction: { questions: "native", writeBoundary: "native" },
@@ -20120,24 +20495,6 @@ var unfinishedTaskOffer2 = (ctx) => {
20120
20495
  }
20121
20496
  };
20122
20497
 
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
20498
  // packages/workit-pi/src/tools.ts
20142
20499
  var readOnlyActions = new Set(["list", "inspect", "preview", "explain", "export"]);
20143
20500
  var output = (result) => ({
@@ -20227,6 +20584,13 @@ var enforceToolPolicy = (event, ctx) => {
20227
20584
  const command = event.input?.command;
20228
20585
  return typeof command === "string" ? gate({ kind: "shell.pre", command, toolUseId: event.toolCallId ?? null }) : undefined;
20229
20586
  }
20587
+ if (event.toolName === "read") {
20588
+ const file = event.input?.path;
20589
+ const skill = typeof file === "string" ? skillFileIn(file) : null;
20590
+ if (skill)
20591
+ recordSkillLoad(hookInput(ctx, { kind: "tool.pre", tool: "read", toolUseId: null }), skill, "read");
20592
+ return;
20593
+ }
20230
20594
  if (event.toolName !== "write" && event.toolName !== "edit")
20231
20595
  return;
20232
20596
  if (!ctx.isProjectTrusted())
@@ -20253,22 +20617,23 @@ var observeToolResult = (event, ctx) => {
20253
20617
  toolUseId: event.toolCallId
20254
20618
  });
20255
20619
  handleHook(post, { descriptor: PI_DESCRIPTOR, addendum: null });
20256
- const nudge = rawGitPre(post, command);
20620
+ const nudge = process.env.WORKIT_PI_WORKER_SESSION ? rawGitPre(post, command) : shellNudge(post, command);
20257
20621
  return nudge.kind === "context" ? { content: [...event.content, { type: "text", text: nudge.text }] } : undefined;
20258
20622
  } catch {
20259
20623
  return;
20260
20624
  }
20261
20625
  };
20626
+ var skillPromptNudge = (prompt, ctx) => process.env.WORKIT_PI_WORKER_SESSION ? null : promptNudge(hookInput(ctx, { kind: "context.turn", prompt }), prompt);
20262
20627
 
20263
20628
  // packages/workit-pi/src/launches.ts
20264
20629
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
20265
20630
  import os5 from "node:os";
20266
- import path23 from "node:path";
20631
+ import path24 from "node:path";
20267
20632
  var launchDir = (root) => {
20268
20633
  const location = resolveStore(root);
20269
- return location instanceof Error ? null : path23.join(location.dir, "pi-launches");
20634
+ return location instanceof Error ? null : path24.join(location.dir, "pi-launches");
20270
20635
  };
20271
- var fileFor = (dir, workerId) => path23.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
20636
+ var fileFor = (dir, workerId) => path24.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
20272
20637
  var recordLaunch = (root, attempt) => {
20273
20638
  const dir = launchDir(root);
20274
20639
  if (!dir)
@@ -20314,7 +20679,7 @@ var abandonedLaunches = (root) => {
20314
20679
  const found = [];
20315
20680
  for (const name of names) {
20316
20681
  try {
20317
- const attempt = JSON.parse(readFileSync6(path23.join(dir, name), "utf8"));
20682
+ const attempt = JSON.parse(readFileSync6(path24.join(dir, name), "utf8"));
20318
20683
  if (typeof attempt.workerId === "string" && typeof attempt.session === "string") {
20319
20684
  if (!alive(attempt))
20320
20685
  found.push(attempt);
@@ -21142,14 +21507,18 @@ function extension(pi) {
21142
21507
  sessions.delete(ctx.sessionManager.getSessionId());
21143
21508
  reconcileLostWorkers(ctx);
21144
21509
  });
21145
- pi.on("before_agent_start", (_event, ctx) => {
21510
+ pi.on("before_agent_start", (event, ctx) => {
21511
+ const nudge = skillPromptNudge(event.prompt, ctx);
21146
21512
  const id = ctx.sessionManager.getSessionId();
21147
21513
  if (sessions.has(id))
21148
- return;
21514
+ return nudge ? { message: { customType: "workit-skill", content: nudge, display: false } } : undefined;
21149
21515
  sessions.add(id);
21150
21516
  const offer = historyOfferSessions.has(id) ? null : unfinishedTaskOffer2(ctx);
21151
21517
  historyOfferSessions.add(id);
21152
- return { message: contextMessage(ctx, offer) };
21518
+ const message = contextMessage(ctx, offer);
21519
+ return { message: nudge ? { ...message, content: `${message.content}
21520
+
21521
+ ${nudge}` } : message };
21153
21522
  });
21154
21523
  pi.on("session_before_compact", () => {
21155
21524
  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.0",
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.