@brainervirus/workit-pi 8.22.3 → 8.23.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
@@ -16200,6 +16200,24 @@ function workitInvocations(command, dialect = "posix", depth = 0) {
16200
16200
  }
16201
16201
  return found;
16202
16202
  }
16203
+ var WORKIT_GLOBAL = /^--(?:cwd|session|token|as)(=|$)/;
16204
+ function skipWorkitGlobals(args) {
16205
+ let cwd = null;
16206
+ let at = 0;
16207
+ for (;at < args.length; at++) {
16208
+ const match = WORKIT_GLOBAL.exec(args[at]);
16209
+ if (match?.[1] === "=") {
16210
+ if (args[at].startsWith("--cwd="))
16211
+ cwd = args[at].slice(6);
16212
+ } else if (match && args[at + 1] !== undefined) {
16213
+ if (args[at] === "--cwd")
16214
+ cwd = args[at + 1];
16215
+ at++;
16216
+ } else if (args[at] !== "--json")
16217
+ break;
16218
+ }
16219
+ return { rest: args.slice(at), cwd };
16220
+ }
16203
16221
  var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
16204
16222
  var CURRENT_BRANCH = /^\$\(\s*git\s+(?:branch\s+--show-current|rev-parse\s+--abbrev-ref\s+HEAD|symbolic-ref\s+--short\s+HEAD)\s*\)$/;
16205
16223
  var pushDestination = (destination) => {
@@ -21372,7 +21390,7 @@ var splitDenial = (plan) => ({
21372
21390
  unblock: plan.unblock
21373
21391
  });
21374
21392
  // packages/workit-core/src/hooks/write-gate.ts
21375
- import { realpathSync as realpathSync6 } from "node:fs";
21393
+ import { existsSync as existsSync9, realpathSync as realpathSync6 } from "node:fs";
21376
21394
  import os10 from "node:os";
21377
21395
  import path31 from "node:path";
21378
21396
 
@@ -21601,7 +21619,7 @@ var exempt = (cwd, root, file, plans) => {
21601
21619
  const segments = posix.split("/");
21602
21620
  return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
21603
21621
  };
21604
- var PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
21622
+ var PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs", "busybox"]);
21605
21623
  var operands = (words) => words.filter((item) => !item.startsWith("-"));
21606
21624
  var SCRIPT_SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
21607
21625
  var wrappedScript = (input) => {
@@ -21613,9 +21631,9 @@ var wrappedScript = (input) => {
21613
21631
  return null;
21614
21632
  if ("script" in tail)
21615
21633
  return tail.script;
21616
- const [shell, flag, script] = tail.argv;
21617
- if (SCRIPT_SHELLS.has((shell.split(/[\\/]/).at(-1) ?? "").toLowerCase()) && /^-[a-z]*c$/.test(flag ?? ""))
21618
- return script ?? null;
21634
+ const script = shellScript2(tail.argv);
21635
+ if (SCRIPT_SHELLS.has((tail.argv[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase()) && script)
21636
+ return script;
21619
21637
  return tail.argv.map(quoteWord2).join(" ");
21620
21638
  };
21621
21639
  var editsOf = (input) => {
@@ -21694,6 +21712,94 @@ var editsOf = (input) => {
21694
21712
  return [];
21695
21713
  }
21696
21714
  };
21715
+ var shellScript2 = (words) => {
21716
+ if (!SCRIPT_SHELLS.has((words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase()))
21717
+ return null;
21718
+ for (let index = 1;index < words.length; index++) {
21719
+ const word = words[index];
21720
+ if (/^-[a-z]*c$/.test(word))
21721
+ return words[index + 1] ?? null;
21722
+ if (/^[-+][oO]$/.test(word))
21723
+ index++;
21724
+ else if (!/^[-+]/.test(word))
21725
+ return null;
21726
+ }
21727
+ return null;
21728
+ };
21729
+ var XARGS_VALUE = /^-[IinLPdsEa]$/;
21730
+ var runsWriter = (words) => {
21731
+ const filled = [...words.filter((word) => word !== "{}" && word !== ";" && word !== "+"), "x"];
21732
+ const edits = editsOf(filled);
21733
+ return edits === null || edits.length > 0 || opaqueWrites(filled) !== null;
21734
+ };
21735
+ var opaqueWrites = (input) => {
21736
+ let words = input;
21737
+ while (words.length && (PREFIXES2.has(words[0]) || /^\w+=/.test(words[0]))) {
21738
+ if (words[0] === "xargs") {
21739
+ let at = 1;
21740
+ while (words[at]?.startsWith("-"))
21741
+ at += XARGS_VALUE.test(words[at]) ? 2 : 1;
21742
+ return runsWriter(words.slice(at)) ? ["."] : null;
21743
+ }
21744
+ words = words.slice(1);
21745
+ }
21746
+ const [verb, ...args] = words;
21747
+ const name = (verb?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
21748
+ const files = operands(args);
21749
+ const has = (pattern) => args.some((arg) => pattern.test(arg));
21750
+ const listed = (named) => named.length ? named : ["."];
21751
+ if (name === "unlink" || name === "shred")
21752
+ return listed(files);
21753
+ if (/^(?:chmod|chown|chgrp|chattr)$/.test(name))
21754
+ return listed(has(/^--reference/) ? files : files.slice(1));
21755
+ if (name === "rsync") {
21756
+ const dest = files.at(-1);
21757
+ const remote = /^[\w.@-]*:/.test(dest ?? "") && !/^[A-Za-z]:[\\/]/.test(dest ?? "");
21758
+ return dest === undefined ? ["."] : remote ? [] : [dest];
21759
+ }
21760
+ if (name === "find") {
21761
+ const exec = args.findIndex((arg) => /^-(?:exec|execdir|ok|okdir)$/.test(arg));
21762
+ const writes = has(/^-(?:delete|fprint\w*|fls)$/) || exec >= 0 && runsWriter(args.slice(exec + 1));
21763
+ const end = args.findIndex((arg) => /^[-(!]/.test(arg));
21764
+ return writes ? listed(end < 0 ? args : args.slice(0, end)) : null;
21765
+ }
21766
+ if (/^g?awk$/.test(name)) {
21767
+ if (!has(/^-i|^--include/))
21768
+ return null;
21769
+ const named = [];
21770
+ let program = !has(/^-f$|^--file/);
21771
+ for (let index = 0;index < args.length; index++)
21772
+ if (/^(?:-[ifvF]|--(?:include|file|assign|field-separator))$/.test(args[index]))
21773
+ index++;
21774
+ else if (args[index].startsWith("-"))
21775
+ continue;
21776
+ else if (program)
21777
+ program = false;
21778
+ else
21779
+ named.push(args[index]);
21780
+ return listed(named);
21781
+ }
21782
+ if (name === "perl")
21783
+ return has(/^-[a-zA-Z]*[eE]/) ? ["."] : null;
21784
+ if (/^(?:python[\d.]*|node|bun|deno|ruby|php)$/.test(name))
21785
+ return has(/^-[a-zA-Z]*[ce]$|^--eval$|^-p$/) ? ["."] : null;
21786
+ return shellScript2(words) === null ? null : ["."];
21787
+ };
21788
+ var linkSources = (args, at) => {
21789
+ const names = operands([...args]);
21790
+ const into = args.indexOf("-t");
21791
+ const dest = into >= 0 ? args[into + 1] : names.length > 1 ? names.at(-1) : ".";
21792
+ const sources = names.filter((name, index) => into >= 0 ? name !== dest : index < names.length - 1 || names.length === 1);
21793
+ const target = dest === undefined ? null : at(dest);
21794
+ const symbolic = args.some((arg) => /^-[a-zA-Z]*s|^--symbolic$/.test(arg));
21795
+ const base = target === null ? null : into >= 0 || sources.length > 1 || existsSync9(target) || /[\\/]$/.test(dest ?? "") ? target : path31.dirname(target);
21796
+ return sources.flatMap((source) => {
21797
+ if (symbolic && base !== null && !path31.isAbsolute(homeExpanded(source)))
21798
+ return [path31.resolve(base, source)];
21799
+ const resolved = at(source);
21800
+ return resolved ? [resolved] : [];
21801
+ });
21802
+ };
21697
21803
  var shellWrites = (command, depth = 0) => {
21698
21804
  const targets = [];
21699
21805
  let unknown = false;
@@ -21803,11 +21909,15 @@ var gitMutationDirs = (cwd, command, dialect) => {
21803
21909
  var CD2 = new Set(["cd", "chdir", "set-location", "sl"]);
21804
21910
  var PUSHD2 = new Set(["pushd", "push-location"]);
21805
21911
  var POPD2 = new Set(["popd", "pop-location"]);
21806
- var cdInto = (dir, words) => {
21912
+ var UNKNOWN_DIR = path31.resolve(path31.parse(process.cwd()).root, ".workit-unknown-dir");
21913
+ var cdInto = (dir, words, strict = false) => {
21914
+ const lost = strict ? UNKNOWN_DIR : undefined;
21807
21915
  const named = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
21808
21916
  const target = named === undefined ? undefined : homeExpanded(named);
21809
- if (dir === undefined || target === "-" || target && /^~|[$`*?]/.test(target))
21810
- return;
21917
+ if (target === "-" || target && /^~|[$`*?]/.test(target))
21918
+ return lost;
21919
+ if (dir === undefined)
21920
+ return lost;
21811
21921
  return target === undefined ? os10.homedir() : path31.resolve(dir, target);
21812
21922
  };
21813
21923
  var quoteWord2 = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
@@ -21815,7 +21925,7 @@ var homeExpanded = (file) => {
21815
21925
  const match = /^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/.exec(file);
21816
21926
  return match ? (process.env.HOME || os10.homedir()) + file.slice(match[0].length) : file;
21817
21927
  };
21818
- var segmentTargets = (cwd, command, dialect, depth = 0) => {
21928
+ var segmentTargets = (cwd, command, dialect, depth = 0, strict = false) => {
21819
21929
  const targets = [];
21820
21930
  let dir = cwd;
21821
21931
  let stack = [];
@@ -21839,21 +21949,35 @@ var segmentTargets = (cwd, command, dialect, depth = 0) => {
21839
21949
  const words = lead < 0 ? [] : segment.words.slice(lead);
21840
21950
  const name = (words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
21841
21951
  if (CD2.has(name)) {
21842
- dir = cdInto(dir, words);
21952
+ dir = cdInto(dir, words, strict);
21843
21953
  continue;
21844
21954
  }
21845
21955
  if (PUSHD2.has(name)) {
21846
21956
  stack.push(dir);
21847
- dir = cdInto(dir, words);
21957
+ dir = cdInto(dir, words, strict);
21848
21958
  continue;
21849
21959
  }
21850
21960
  if (POPD2.has(name)) {
21851
- dir = stack.length ? stack.pop() : undefined;
21961
+ dir = stack.length ? stack.pop() : strict ? UNKNOWN_DIR : undefined;
21852
21962
  continue;
21853
21963
  }
21854
- const inner = depth < 4 ? wrappedScript(words) : null;
21964
+ const inner = depth < 4 ? wrappedScript(words) ?? (strict ? shellScript2(words) : null) : null;
21855
21965
  if (inner !== null && dir !== undefined)
21856
- targets.push(...segmentTargets(dir, inner, "posix", depth + 1));
21966
+ targets.push(...segmentTargets(dir, inner, "posix", depth + 1, strict));
21967
+ if (strict && dir !== undefined) {
21968
+ const runIn = dir;
21969
+ for (const sub of depth < 4 ? segment.subs ?? [] : [])
21970
+ targets.push(...segmentTargets(runIn, sub, dialect, depth + 1, true));
21971
+ const opaque = segment.unanalyzable ? ["."] : inner === null ? opaqueWrites(words) : null;
21972
+ for (const file of opaque ?? []) {
21973
+ const resolved = at(file);
21974
+ if (resolved)
21975
+ targets.push(file === "." ? path31.join(resolved, DIR_TARGET) : resolved);
21976
+ }
21977
+ const ln = words.findIndex((word) => /^ln$/i.test(word.split(/[\\/]/).at(-1) ?? ""));
21978
+ if (ln >= 0 && words.slice(0, ln).every((word) => PREFIXES2.has(word)))
21979
+ targets.push(...linkSources(words.slice(ln + 1), at));
21980
+ }
21857
21981
  for (const file of segment.redirects)
21858
21982
  if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
21859
21983
  const resolved = at(file);
@@ -21870,11 +21994,29 @@ var segmentTargets = (cwd, command, dialect, depth = 0) => {
21870
21994
  if (resolved)
21871
21995
  targets.push(resolved);
21872
21996
  }
21873
- if (dir !== undefined && workitInvocations(segment.words.map(quoteWord2).join(" "), "posix").some((args) => args[0] === "git" && MUTATING_WORKIT_GIT.has(args[1] ?? "") && !inert(args.slice(2), "")))
21874
- targets.push(path31.join(dir, DIR_TARGET));
21997
+ if (dir !== undefined)
21998
+ for (const invocation of workitInvocations(segment.words.map(quoteWord2).join(" "), "posix")) {
21999
+ const { at: runsIn, args } = workitCwd(dir, invocation);
22000
+ if (args[0] === "git" && MUTATING_WORKIT_GIT.has(args[1] ?? "") && !inert(args.slice(2), ""))
22001
+ targets.push(path31.join(runsIn, DIR_TARGET));
22002
+ }
21875
22003
  }
21876
22004
  return targets;
21877
22005
  };
22006
+ var workitCwd = (dir, args) => {
22007
+ const { rest, cwd } = skipWorkitGlobals(args);
22008
+ return { at: cwd === null ? dir : path31.resolve(dir, homeExpanded(cwd)), args: rest };
22009
+ };
22010
+ var shellMutationTargets = (cwd, command, dialect = "posix", strict = false) => {
22011
+ try {
22012
+ return [
22013
+ ...segmentTargets(cwd, command, dialect, 0, strict),
22014
+ ...gitMutationDirs(cwd, command, dialect).map((dir) => path31.join(dir, DIR_TARGET))
22015
+ ];
22016
+ } catch {
22017
+ return [];
22018
+ }
22019
+ };
21878
22020
  var overridden = (command, dialect) => {
21879
22021
  const segments = segmentsOf(command, dialect);
21880
22022
  if (segments.length !== 1 || segments[0].group || segments[0].subs?.length)
@@ -21980,16 +22122,8 @@ function shellGate(cwd, command, owner, dialect = "posix", deps) {
21980
22122
  } catch {}
21981
22123
  const writes = shellWrites(command);
21982
22124
  let targets = [];
21983
- if (owner) {
21984
- try {
21985
- targets = [
21986
- ...segmentTargets(cwd, command, dialect),
21987
- ...gitMutationDirs(cwd, command, dialect).map((dir) => path31.join(dir, DIR_TARGET))
21988
- ];
21989
- } catch {
21990
- targets = [];
21991
- }
21992
- }
22125
+ if (owner)
22126
+ targets = shellMutationTargets(cwd, command, dialect);
21993
22127
  if (owner && targets.length) {
21994
22128
  const live = liveGate(owner, targets, deps);
21995
22129
  if (live.kind !== "none") {
@@ -22628,9 +22762,266 @@ var cwdRewrite = (command) => {
22628
22762
  return `workit --cwd ${dir} ${rest.slice("workit".length).trimStart()}`.trimEnd();
22629
22763
  };
22630
22764
 
22765
+ // packages/workit-core/src/hooks/worktree-confine.ts
22766
+ import { createHash as createHash17 } from "node:crypto";
22767
+ import fs25 from "node:fs";
22768
+ import path33 from "node:path";
22769
+ var FANOUT_TOKEN_FILE = "workit-fanout-token";
22770
+ var NONE5 = { kind: "none" };
22771
+ var CASE_INSENSITIVE2 = process.platform === "darwin" || process.platform === "win32";
22772
+ var canonical3 = (file) => {
22773
+ let rest = "";
22774
+ for (let dir = path33.resolve(file);; dir = path33.dirname(dir)) {
22775
+ try {
22776
+ return path33.join(fs25.realpathSync.native(dir), rest);
22777
+ } catch {
22778
+ if (path33.dirname(dir) === dir)
22779
+ return path33.resolve(file);
22780
+ rest = rest ? path33.join(path33.basename(dir), rest) : path33.basename(dir);
22781
+ }
22782
+ }
22783
+ };
22784
+ var within = (child, parent) => {
22785
+ const fold = (value) => CASE_INSENSITIVE2 ? value.toLowerCase() : value;
22786
+ const relative = path33.relative(fold(parent), fold(child));
22787
+ return relative === "" || !relative.startsWith("..") && !path33.isAbsolute(relative);
22788
+ };
22789
+ var checkoutOf2 = (file) => {
22790
+ for (let dir = canonical3(file);; dir = path33.dirname(dir)) {
22791
+ const dotGit = path33.join(dir, ".git");
22792
+ if (fs25.existsSync(dotGit)) {
22793
+ if (!fs25.statSync(dotGit).isFile())
22794
+ return { root: dir, workit: false };
22795
+ const admin = /^gitdir:\s*(.+)$/mu.exec(fs25.readFileSync(dotGit, "utf8"))?.[1]?.trim();
22796
+ return {
22797
+ root: dir,
22798
+ workit: Boolean(admin) && fs25.existsSync(path33.join(path33.resolve(dir, admin), FANOUT_TOKEN_FILE))
22799
+ };
22800
+ }
22801
+ if (path33.dirname(dir) === dir)
22802
+ return null;
22803
+ }
22804
+ };
22805
+ var workitWorktreeOf = (file) => {
22806
+ try {
22807
+ const checkout = checkoutOf2(file);
22808
+ return checkout?.workit ? checkout.root : null;
22809
+ } catch {
22810
+ return null;
22811
+ }
22812
+ };
22813
+ var bindingDir = () => path33.join(resolveStateDir(), "worktree-bind");
22814
+ var bindingFile = (session, agentId) => path33.join(bindingDir(), `${createHash17("sha256").update(`${session}\x00${agentId}`).digest("hex").slice(0, 32)}.json`);
22815
+ var readBinding = (file) => {
22816
+ try {
22817
+ const value = JSON.parse(fs25.readFileSync(file, "utf8"));
22818
+ return typeof value.worktree === "string" ? value : null;
22819
+ } catch {
22820
+ return null;
22821
+ }
22822
+ };
22823
+ var boundWorktree = (input) => {
22824
+ const agentId = input.session.agentId;
22825
+ if (!agentId)
22826
+ return null;
22827
+ const binding = readBinding(bindingFile(input.session.id, agentId));
22828
+ return binding && workitWorktreeOf(binding.worktree) === binding.worktree ? binding.worktree : null;
22829
+ };
22830
+ var bind = (input, worktree) => {
22831
+ const agentId = input.session.agentId;
22832
+ const file = bindingFile(input.session.id, agentId);
22833
+ const binding = {
22834
+ worktree,
22835
+ session: input.session.id,
22836
+ agentId,
22837
+ at: new Date().toISOString()
22838
+ };
22839
+ fs25.mkdirSync(path33.dirname(file), { recursive: true });
22840
+ fs25.writeFileSync(file, JSON.stringify(binding));
22841
+ try {
22842
+ if (inWorkitWorkspace(input.cwd))
22843
+ appendHookObserved(input.cwd, {
22844
+ type: "worktree.bound",
22845
+ actor: { host: input.host, session: input.session.id, agentId },
22846
+ agentType: input.session.agentType,
22847
+ worktree
22848
+ });
22849
+ } catch {}
22850
+ };
22851
+ var hostIsolated = (cwd, agentId) => {
22852
+ try {
22853
+ if (agentId === null)
22854
+ return false;
22855
+ const checkout = checkoutOf2(cwd);
22856
+ if (!checkout)
22857
+ return false;
22858
+ const dotGit = path33.join(checkout.root, ".git");
22859
+ if (!fs25.statSync(dotGit).isFile())
22860
+ return false;
22861
+ const admin = /^gitdir:\s*(.+)$/mu.exec(fs25.readFileSync(dotGit, "utf8"))?.[1]?.trim();
22862
+ if (!admin)
22863
+ return false;
22864
+ const adminDir = path33.resolve(checkout.root, admin);
22865
+ const back = fs25.readFileSync(path33.join(adminDir, "gitdir"), "utf8").trim();
22866
+ if (path33.relative(canonical3(path33.resolve(adminDir, back)), canonical3(dotGit)) !== "")
22867
+ return false;
22868
+ const common = path33.join(adminDir, "commondir");
22869
+ const gitDir = fs25.existsSync(common) ? path33.resolve(adminDir, fs25.readFileSync(common, "utf8").trim()) : adminDir;
22870
+ const expected = path33.join(path33.dirname(canonical3(gitDir)), ".claude", "worktrees", `agent-${agentId}`);
22871
+ return path33.relative(canonical3(expected), checkout.root) === "";
22872
+ } catch {
22873
+ return false;
22874
+ }
22875
+ };
22876
+ var shown = (target) => path33.basename(target) === ".workit-live-target" ? path33.dirname(target) : target;
22877
+ var unboundDeny = (input, target) => {
22878
+ const unblock = "Work only in the worktree your brief names, which the lead made with `workit fanout worktree create <slice>`: edit files under it and run one verb per call against it (`workit --cwd <worktree> git commit -m … -- <paths>`, `workit --cwd <worktree> check <name>`). If your brief names no worktree, stop and report that to the lead.";
22879
+ return {
22880
+ kind: "deny",
22881
+ reason: `workit worktree confinement: implementer ${input.session.agentId} is not working in a Workit worktree yet, and ${shown(target)} is outside every worktree \`workit fanout worktree create\` made, so it would change the lead's checkout. ${unblock}`,
22882
+ unblock
22883
+ };
22884
+ };
22885
+ var outsideDeny = (input, worktree, target) => {
22886
+ const unblock = `Edit only under ${worktree} and run git there, one verb per call: \`workit --cwd ${worktree} git commit -m … -- <paths>\`, \`workit --cwd ${worktree} check <name>\` (or \`git -C ${worktree} …\`); a command whose writes Workit cannot see is judged by the directory it runs in. Anything outside it is a follow-up for your report.`;
22887
+ return {
22888
+ kind: "deny",
22889
+ reason: `workit worktree confinement: implementer ${input.session.agentId} is bound to the worktree ${worktree}, and ${shown(target)} is outside it. ${unblock}`,
22890
+ unblock
22891
+ };
22892
+ };
22893
+ var unknownDirDeny = (input, worktree) => {
22894
+ const wt = worktree ?? "<worktree>";
22895
+ const unblock = `Workit cannot resolve \`cd -\`, \`cd "$X"\` or \`popd\`: write by absolute path, \`git -C ${wt}\` or \`workit --cwd ${wt}\`.`;
22896
+ return {
22897
+ kind: "deny",
22898
+ reason: `workit worktree confinement: implementer ${input.session.agentId} writes after an unknown directory change. ${unblock}`,
22899
+ unblock
22900
+ };
22901
+ };
22902
+ function confineImplementer(input, targets) {
22903
+ try {
22904
+ if (!input.session.agentId || hostIsolated(input.cwd, input.session.agentId))
22905
+ return NONE5;
22906
+ if (targets.some((target) => within(path33.resolve(target), UNKNOWN_DIR)))
22907
+ return unknownDirDeny(input, boundWorktree(input));
22908
+ const bindings = canonical3(bindingDir());
22909
+ const judged = targets.map((named) => ({ named, real: canonical3(named) })).filter(({ real }) => within(real, bindings) || checkoutOf2(real) !== null);
22910
+ if (judged.length === 0)
22911
+ return NONE5;
22912
+ let worktree = boundWorktree(input);
22913
+ if (!worktree) {
22914
+ for (const { real } of judged) {
22915
+ worktree = within(real, bindings) ? null : workitWorktreeOf(real);
22916
+ if (worktree)
22917
+ break;
22918
+ }
22919
+ if (!worktree)
22920
+ return unboundDeny(input, judged[0].named);
22921
+ bind(input, worktree);
22922
+ }
22923
+ const root = canonical3(worktree);
22924
+ const outside = judged.find(({ real }) => !within(real, root) || within(real, bindings));
22925
+ return outside ? outsideDeny(input, worktree, outside.named) : NONE5;
22926
+ } catch {
22927
+ return NONE5;
22928
+ }
22929
+ }
22930
+ var MUTATING_REMOTE = new Set([
22931
+ "add",
22932
+ "remove",
22933
+ "rm",
22934
+ "rename",
22935
+ "set-url",
22936
+ "set-head",
22937
+ "set-branches",
22938
+ "prune"
22939
+ ]);
22940
+ var CONFIG_READ = /^(?:--get(?:-all|-regexp|-urlmatch)?|--list|-l|get|list)$/;
22941
+ var CONFIG_WRITE = /^(?:--(?:unset(?:-all)?|add|replace-all|rename-section|remove-section|edit)|-e|set|unset|rename-section|remove-section|edit)$/;
22942
+ var CONFIG_VALUE = /^(?:--file|-f|--blob|--type|--default|--comment)$/;
22943
+ var leadOwnedGit = (args) => {
22944
+ const [sub = "", ...rest] = args;
22945
+ if (rest.some((arg) => arg === "--help" || arg === "-h"))
22946
+ return false;
22947
+ const names = rest.filter((arg) => !arg.startsWith("-"));
22948
+ const flag = (pattern) => rest.some((arg) => pattern.test(arg));
22949
+ switch (sub) {
22950
+ case "worktree":
22951
+ return names[0] !== undefined && names[0] !== "list";
22952
+ case "update-ref":
22953
+ return true;
22954
+ case "symbolic-ref":
22955
+ return names.length > 1 || flag(/^(?:-d|--delete)$/);
22956
+ case "branch":
22957
+ return flag(/^-[a-zA-Z]*[dDfmMcC]|^--(?:delete|force|move|copy)$/);
22958
+ case "tag":
22959
+ return flag(/^-[a-zA-Z]*[df]|^--(?:delete|force)$/);
22960
+ case "push": {
22961
+ if (flag(/^(?:-d|--delete|--mirror|--prune)$/))
22962
+ return true;
22963
+ const refspecs = names.slice(1);
22964
+ return refspecs.some((name) => name.includes(":") || name.startsWith("+")) || refspecs.length > 0 && flag(/^-[a-zA-Z]*f|^--force/);
22965
+ }
22966
+ case "replace":
22967
+ case "filter-branch":
22968
+ case "gc":
22969
+ case "prune":
22970
+ case "repack":
22971
+ case "sparse-checkout":
22972
+ return true;
22973
+ case "notes":
22974
+ case "reflog":
22975
+ case "submodule":
22976
+ return !/^(?:list|show|status|exists)$/.test(names[0] ?? "list");
22977
+ case "stash":
22978
+ return names[0] === "drop" || names[0] === "clear";
22979
+ case "remote":
22980
+ return MUTATING_REMOTE.has(names[0] ?? "");
22981
+ case "config": {
22982
+ if (rest.some((arg) => CONFIG_READ.test(arg)))
22983
+ return false;
22984
+ if (rest.some((arg) => CONFIG_WRITE.test(arg)))
22985
+ return true;
22986
+ let operands = 0;
22987
+ for (let index = 0;index < rest.length; index++)
22988
+ if (CONFIG_VALUE.test(rest[index]))
22989
+ index++;
22990
+ else if (!rest[index].startsWith("-"))
22991
+ operands++;
22992
+ return operands > 1;
22993
+ }
22994
+ default:
22995
+ return false;
22996
+ }
22997
+ };
22998
+ var leadOwnedWorkit = (args) => !args.includes("--help") && args.some((_, start) => {
22999
+ if (start > 0 && !/(?:^|[\\/])workit(?:\.exe|\.cmd)?$/.test(args[start - 1]))
23000
+ return false;
23001
+ const [verb, sub] = skipWorkitGlobals(args.slice(start)).rest;
23002
+ return verb === "fanout" && /^(?:plan|worktree)$/.test(sub ?? "");
23003
+ });
23004
+ var leadOwnedDeny = (input, command) => {
23005
+ const unblock = "Worktrees, branches other than your own, refs, tags, remotes, git config, the shared stash and the fan-out plan belong to the lead: report what you need changed as a follow-up instead.";
23006
+ return {
23007
+ kind: "deny",
23008
+ reason: `workit worktree confinement: implementer ${input.session.agentId} may not run \`${command.slice(0, 160)}\`. ${unblock}`,
23009
+ unblock
23010
+ };
23011
+ };
23012
+ function confineImplementerShell(input, command, dialect = "posix") {
23013
+ try {
23014
+ if (!input.session.agentId || hostIsolated(input.cwd, input.session.agentId))
23015
+ return NONE5;
23016
+ if (rawInvocations(command, dialect).some((invocation) => invocation.tool === "git" && leadOwnedGit(invocation.args)) || workitInvocations(command, dialect).some(leadOwnedWorkit))
23017
+ return leadOwnedDeny(input, command);
23018
+ } catch {}
23019
+ return confineImplementer(input, shellMutationTargets(input.cwd, command, dialect, true));
23020
+ }
23021
+
22631
23022
  // packages/workit-core/src/hooks/handle.ts
22632
23023
  var usable2 = (value) => value === "native" || value === "partial";
22633
- var NONE5 = { kind: "none" };
23024
+ var NONE6 = { kind: "none" };
22634
23025
  var resendsOnChange = (descriptor) => descriptor.context.turnResend === "on-change" && usable2(descriptor.context.perTurn);
22635
23026
  var offered = new Set;
22636
23027
  var ROLES = {
@@ -22643,6 +23034,7 @@ var workitRole = (descriptor, agentType) => {
22643
23034
  const name = agentType.startsWith(prefix) ? agentType.slice(prefix.length) : "";
22644
23035
  return Object.hasOwn(ROLES, name) ? ROLES[name] : null;
22645
23036
  };
23037
+ var implementerAgent = (input, descriptor) => Boolean(input.session.agentId) && input.session.agentType !== null && workitRole(descriptor, input.session.agentType) === "implementer";
22646
23038
  var subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128);
22647
23039
  var CLAUDE_WORKTREE = /[\\/]\.claude[\\/]worktrees[\\/]/;
22648
23040
  var isolatedGit = (cwd) => `Run git as plain, separate commands inside your worktree (\`git switch -c <branch> <base>\`, ${workspaceTrailers(cwd).enabled ? '`git commit --trailer "Workit-Session=<your session>"`, `git push`; `workit doctor --fix` can install a prepare-commit-msg hook that adds the trailer' : "`git commit` (this workspace sets trailers: false: add no Workit-Session trailer), `git push`"}; add Co-Authored-By with \`git commit --trailer\` in the same commit, never by amending), never \`workit git …\`: Claude Code's worktree isolation refuses a command that wraps git and cannot be turned off (https://code.claude.com/docs/en/worktrees#how-claude-code-enforces-isolation). Never dodge that check by renaming or re-wrapping git. Use workit for the non-git verbs (\`workit check\`, \`workit ledger\`, \`workit pr\`).`;
@@ -22650,7 +23042,7 @@ var subagentStartText = (input, descriptor, event) => {
22650
23042
  const session = input.session.id ?? null;
22651
23043
  const role = workitRole(descriptor, event.agentType);
22652
23044
  const who = `${descriptor.label} subagent ${event.agentId} (${event.agentType})`;
22653
- const isolated = descriptor.subagents.worktreeIsolation === "native" && (role === "implementer" || CLAUDE_WORKTREE.test(input.cwd));
23045
+ const isolated = descriptor.subagents.worktreeIsolation === "native" && (role === "implementer" ? hostIsolated(input.cwd, event.agentId) : CLAUDE_WORKTREE.test(input.cwd));
22654
23046
  const guide = role === "implementer" ? branchGuidance(input.cwd) : null;
22655
23047
  if (role === "implementer" && isolated)
22656
23048
  return `Workit observed ${who} working in its own git worktree: it may edit and commit there, within its brief's scope. ${isolatedGit(input.cwd)} Plain git is expected here, and the branch name is still policy-checked: a raw \`git switch -c\`, \`git checkout -b\`, \`git branch\` or \`git worktree add -b\` onto a protected or non-compliant name is denied, so use a policy-compliant branch.${guide ? ` ${guide}` : " (e.g. feature/<slug>)"} Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
@@ -22658,7 +23050,7 @@ var subagentStartText = (input, descriptor, event) => {
22658
23050
  return `${isolatedGit(input.cwd)}
22659
23051
  ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
22660
23052
  if (role === "implementer")
22661
- return `Workit observed ${who} as an implementer: it edits and commits only in its own git worktree (the lead makes one with \`workit fanout worktree create <slice>\`), never in the lead's checkout, within its brief's scope. Before the first commit, switch to a policy-compliant branch with \`workit git branch <branch> --base <base>\` (or \`--kind <kind> --slug <slug>\`, which picks the base); branch policy hooks still deny protected or non-compliant branches, raw git included.${guide ? ` ${guide}` : " (e.g. feature/<slug>)"} Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
23053
+ return `Workit observed ${who} as an implementer: it edits and commits only in its own git worktree, the one its brief names (the lead makes it with \`workit fanout worktree create <slice>\` before spawning), never in the lead's checkout, within its brief's scope. Workit binds it to the first Workit worktree it changes: from then on an edit or a mutating git command outside that worktree is denied, and before then so is one outside every Workit worktree. Run Workit against the worktree one verb per call, never \`cd … &&\` chains or \`$VAR\` paths: \`workit --cwd <worktree> git commit -m "<message>" -- <paths>\`, \`workit --cwd <worktree> check <name>\`. The worktree is on the brief's branch once the lead made it; otherwise switch with \`workit --cwd <worktree> git branch <branch> --base <base>\` (or \`--kind <kind> --slug <slug>\`, which picks the base); branch policy hooks still deny protected or non-compliant branches, raw git included.${guide ? ` ${guide}` : " (e.g. feature/<slug>)"} Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
22662
23054
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
22663
23055
  if (role !== "judge" || !event.agentId)
22664
23056
  return readOnly;
@@ -22689,7 +23081,7 @@ function decide(input, deps) {
22689
23081
  switch (event.kind) {
22690
23082
  case "session.start": {
22691
23083
  if (!usable2(descriptor.context.sessionStart))
22692
- return NONE5;
23084
+ return NONE6;
22693
23085
  const key = input.session.id ? `${input.host}\x00${input.session.id}` : null;
22694
23086
  const offer = event.source === "startup" && (key === null || !offered.has(key));
22695
23087
  if (offer && key !== null)
@@ -22704,26 +23096,31 @@ function decide(input, deps) {
22704
23096
  }
22705
23097
  case "context.turn": {
22706
23098
  if (!usable2(descriptor.context.perTurn))
22707
- return NONE5;
23099
+ return NONE6;
22708
23100
  const current = turnContextText(input, descriptor);
22709
23101
  const text = resendsOnChange(descriptor) ? changedTurnContext(input, current) : current;
22710
- return withContextLine(text ? { kind: "context", text } : NONE5, promptNudge(input, event.prompt));
23102
+ return withContextLine(text ? { kind: "context", text } : NONE6, promptNudge(input, event.prompt));
22711
23103
  }
22712
23104
  case "shell.pre": {
22713
23105
  const skillRead = skillReadIn(event.command, event.dialect);
22714
23106
  if (skillRead)
22715
23107
  recordSkillLoad(input, skillRead, "read");
22716
23108
  const canDeny = usable2(descriptor.shellPolicy.deny);
22717
- const policy = canDeny ? shellPolicy(input.cwd, event.command, event.dialect) : NONE5;
23109
+ const policy = canDeny ? shellPolicy(input.cwd, event.command, event.dialect) : NONE6;
22718
23110
  if (policy.kind !== "none")
22719
23111
  return policy;
22720
23112
  const postTool = usable2(descriptor.events["shell.post"].support);
22721
23113
  if (!postTool)
22722
23114
  settlePendingCommit(input);
22723
- const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
23115
+ const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE6;
22724
23116
  if (raw.kind === "deny")
22725
23117
  return raw;
22726
- const gate = usable2(descriptor.events["write.pre"].support) ? shellGate(input.cwd, event.command, liveOwner(input, descriptor.transport), event.dialect) : NONE5;
23118
+ if (canDeny && implementerAgent(input, descriptor)) {
23119
+ const confined = confineImplementerShell(input, event.command, event.dialect);
23120
+ if (confined.kind === "deny")
23121
+ return confined;
23122
+ }
23123
+ const gate = usable2(descriptor.events["write.pre"].support) ? shellGate(input.cwd, event.command, liveOwner(input, descriptor.transport), event.dialect) : NONE6;
22727
23124
  if (gate.kind === "deny")
22728
23125
  return gate;
22729
23126
  const plan = heavyGate(input.cwd, event.command, {
@@ -22746,10 +23143,10 @@ function decide(input, deps) {
22746
23143
  command: rewrittenTo,
22747
23144
  input: event.toolInput ? { ...event.toolInput, command: rewrittenTo } : null,
22748
23145
  text
22749
- } : NONE5;
23146
+ } : NONE6;
22750
23147
  if (descriptor.transport !== "hook-process")
22751
23148
  return rewritten(null);
22752
- const nudged = withContextLine(withContextLine(canDeny ? rawGitPre(input, event.command) : NONE5, aside), shipNudge(input, event.command));
23149
+ const nudged = withContextLine(withContextLine(canDeny ? rawGitPre(input, event.command) : NONE6, aside), shipNudge(input, event.command));
22753
23150
  if (!lease)
22754
23151
  return cwdCommand && nudged.kind !== "deny" ? rewritten(nudged.kind === "context" ? nudged.text : null) : nudged;
22755
23152
  return rewritten([rewriteNote(lease), nudged.kind === "context" ? nudged.text : null].filter(Boolean).join(`
@@ -22758,9 +23155,17 @@ function decide(input, deps) {
22758
23155
  case "shell.post":
22759
23156
  if (usable2(descriptor.events["shell.post"].support))
22760
23157
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
22761
- return NONE5;
22762
- case "write.pre":
22763
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths, liveOwner(input, descriptor.transport)) : NONE5;
23158
+ return NONE6;
23159
+ case "write.pre": {
23160
+ if (!usable2(descriptor.events["write.pre"].support))
23161
+ return NONE6;
23162
+ if (implementerAgent(input, descriptor)) {
23163
+ const confined = confineImplementer(input, liveTargets(input.cwd, event.paths));
23164
+ if (confined.kind === "deny")
23165
+ return confined;
23166
+ }
23167
+ return writeGate(input.cwd, event.paths, liveOwner(input, descriptor.transport));
23168
+ }
22764
23169
  case "subagent.start":
22765
23170
  if (workitRole(descriptor, event.agentType) === "judge")
22766
23171
  trackJudge(input, event.agentId, true);
@@ -22777,14 +23182,14 @@ function decide(input, deps) {
22777
23182
  case "tool.pre":
22778
23183
  if (event.skill)
22779
23184
  recordSkillLoad(input, event.skill, "tool");
22780
- return NONE5;
23185
+ return NONE6;
22781
23186
  case "stop":
22782
- return usable2(descriptor.events.stop.support) ? stopDecision(input, descriptor) : NONE5;
23187
+ return usable2(descriptor.events.stop.support) ? stopDecision(input, descriptor) : NONE6;
22783
23188
  case "subagent.stop":
22784
23189
  trackJudge(input, event.agentId, false);
22785
- return NONE5;
23190
+ return NONE6;
22786
23191
  case "prompt.submit":
22787
- return NONE5;
23192
+ return NONE6;
22788
23193
  }
22789
23194
  }
22790
23195
  // packages/workit-core/src/hooks/deadline.ts
@@ -23093,12 +23498,12 @@ var skillPromptNudge = (prompt, ctx) => process.env.WORKIT_PI_WORKER_SESSION ? n
23093
23498
  // packages/workit-pi/src/launches.ts
23094
23499
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
23095
23500
  import os11 from "node:os";
23096
- import path33 from "node:path";
23501
+ import path34 from "node:path";
23097
23502
  var launchDir = (root) => {
23098
23503
  const location = resolveStore(root);
23099
- return location instanceof Error ? null : path33.join(location.dir, "pi-launches");
23504
+ return location instanceof Error ? null : path34.join(location.dir, "pi-launches");
23100
23505
  };
23101
- var fileFor = (dir, workerId) => path33.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
23506
+ var fileFor = (dir, workerId) => path34.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
23102
23507
  var recordLaunch = (root, attempt) => {
23103
23508
  const dir = launchDir(root);
23104
23509
  if (!dir)
@@ -23144,7 +23549,7 @@ var abandonedLaunches = (root) => {
23144
23549
  const found = [];
23145
23550
  for (const name of names) {
23146
23551
  try {
23147
- const attempt = JSON.parse(readFileSync6(path33.join(dir, name), "utf8"));
23552
+ const attempt = JSON.parse(readFileSync6(path34.join(dir, name), "utf8"));
23148
23553
  if (typeof attempt.workerId === "string" && typeof attempt.session === "string") {
23149
23554
  if (!alive(attempt))
23150
23555
  found.push(attempt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-pi",
3
- "version": "8.22.3",
3
+ "version": "8.23.0",
4
4
  "description": "Workit native extension for Pi — tools, method skills, and session continuity",
5
5
  "keywords": [
6
6
  "agentic-coding",
@@ -20,14 +20,14 @@ units of ~100+ lines each. Coupled work stays with one owner. Workit never spawn
20
20
  PR (`references/orchestration.md`).
21
21
  2. **Record standing orders** (`workit ledger standing add "<order>"`): every
22
22
  user directive all workers share. Directives decay across resumes.
23
- 3. **Brief each worker verbatim** with `workit fanout brief <slice>`: plan
24
- fields, standing orders, SCRATCH, session id, fan-in and heavy-lease rules.
23
+ 3. **Brief each worker verbatim**: `workit fanout worktree create <slice>`
24
+ (every host), then `workit fanout brief <slice>` (WORKTREE, SCRATCH, rules).
25
25
  Never hand-edit it; change the plan or the standing orders and render again.
26
26
  4. **Spawn in a rolling window**, in the background, each in its own worktree:
27
27
  keep 4-6 in flight and refill from `spawnable` in `workit fanout status` as
28
28
  each one finishes. Model: the brief's `model:` line, else TIER
29
- (`references/orchestration.md`). Claude Code: the `implementer` agent;
30
- elsewhere first `workit fanout worktree create <slice>`.
29
+ (`references/orchestration.md`). Claude Code: the `implementer` agent, never
30
+ with `isolation: "worktree"`; Workit's hooks confine it to its WORKTREE.
31
31
  5. **Judge liveness by side effects only:** `workit fanout status` (head age,
32
32
  PR, CI, verdict, landed; STUCK past the TIMEBOX). Stop a stuck worker and
33
33
  observe that it exited (a timeout is not proof). Removing a worktree drops
@@ -25,6 +25,7 @@ REPORT: branch, head SHA, files changed, each VERIFY command with its exit code,
25
25
  each ACCEPTANCE line met / not met, rulings you made (`workit ledger ruling`),
26
26
  anything out of scope as a follow-up, not a diff.
27
27
  STANDING: <the standing orders, verbatim: `workit ledger standing add` records each one>
28
+ WORKTREE: <the path `workit fanout worktree create <slice>` made; the worker runs `workit --cwd <it> …` there>
28
29
  <identity line, per the lead's host: see "Identity and git rules per host" below>
29
30
  ```
30
31
 
@@ -48,6 +49,7 @@ FORBIDDEN: no edits outside SCOPE; no schema migration; no new packages
48
49
  FAN-IN: one PR per slice. Never rebase, retarget, merge or force-push: the lead owns topology.
49
50
  REPORT: as in the template
50
51
  STANDING: conventional commits; no comments that restate code; ask nothing, record rulings instead
52
+ WORKTREE: ../app-wt/usage-endpoint (made by `workit fanout worktree create usage-endpoint`; yours alone)
51
53
  ```
52
54
 
53
55
  ## Plan file
@@ -106,25 +108,29 @@ lead's host (`WORKIT_HOST`, or the host's own session variable):
106
108
 
107
109
  | Lead's host | Identity line | RULES 1 and 3 |
108
110
  | --- | --- | --- |
109
- | Claude Code | `Session: your env already has WORKIT_SESSION_ID (the lead's id)`; no `export`, no hand-minted id (SubagentStart issues `<lead>:<agent id>` only to verifiers and reviewers, never to an implementer, so the worker's commits carry the lead's id) | plain git: `git fetch origin && git switch -c <branch> origin/<base>` and `git commit -m <msg> --trailer "Workit-Session: <the env WORKIT_SESSION_ID>" --trailer "<host attribution>" -- <paths in SCOPE>`, because worktree isolation refuses every `workit git` form |
111
+ | Claude Code | `Session: your env already has WORKIT_SESSION_ID (the lead's id)`; no `export`, no hand-minted id (SubagentStart issues `<lead>:<agent id>` only to verifiers and reviewers, never to an implementer, so the worker's commits carry the lead's id) | `workit git branch <branch> --base <base>`, `workit git commit -m <msg> -- <paths in SCOPE>`: the implementer runs without Claude Code's worktree isolation, which refused every `workit git` form |
110
112
  | OpenCode, Pi, Codex | `Session: your host already sets WORKIT_SESSION_ID in your shell`; export nothing (the host derives the id and an export cannot replace it) | `workit git branch <branch> --base <base>`, `workit git commit -m <msg> -- <paths in SCOPE>` |
111
113
  | Cursor, CI, a plain terminal | `export WORKIT_SESSION_ID=<lead>-w-<slice>` (`+try<n>` for a race attempt): declared, not checked | the same `workit git` forms |
112
114
 
113
- `MODE: resume` is `git switch <branch>` on every host (the branch exists; the
114
- lead removed the dead worker's worktree after its exit, so the switch
115
- succeeds), then continue from its head. A race attempt (`--attempt <n>`)
116
- follows its host's row with the branch `<branch>-try<n>`.
115
+ Once `workit fanout worktree create <slice>` made the worktree, the brief's
116
+ WORKTREE line names it and RULES 1 and 3 run against it on every host:
117
+ `workit --cwd <worktree> git commit …`, `workit --cwd <worktree> check <name>`,
118
+ one verb per call. The worktree is already on the branch (MODE new or resume),
119
+ so no branch command comes first. Without one, `MODE: resume` is
120
+ `git switch <branch>` (the branch exists), then continue from its head. A race
121
+ attempt (`--attempt <n>`) follows its host's row with the branch
122
+ `<branch>-try<n>`.
117
123
 
118
124
  ## Worker rules (`workit fanout brief` appends them as RULES)
119
125
 
120
- 1. First command: create the branch (`MODE: new`; the worktree may start on a
121
- name that breaks branch policy) or `git switch` to it (`MODE: resume`),
122
- as the table above says for your host.
126
+ 1. Work only in WORKTREE, already on the branch, one verb per call against it
127
+ (`workit --cwd <worktree> …`); Workit's hooks deny an implementer's change
128
+ outside it. With no worktree made yet: create the branch (`MODE: new`) or
129
+ `git switch` to it (`MODE: resume`), as the table above says.
123
130
  2. Decide ambiguities yourself and record them:
124
131
  `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`.
125
132
  Stop only for an irreversible action, a security-sensitive one, or a side
126
133
  effect outside the worktree.
127
- 3. Commit as the table says (`workit git commit -m "<msg>" -- <paths in SCOPE>`,
128
- or plain git with the trailers on Claude Code); push only if the brief says
129
- so.
134
+ 3. Commit with `workit --cwd <worktree> git commit -m "<msg>" -- <paths in SCOPE>`;
135
+ push only if the brief says so.
130
136
  4. Never record a verdict on your own work.
@@ -35,8 +35,8 @@ parent is verified, appears there by itself.
35
35
 
36
36
  A slice is retried when it is STUCK, its worker exits without meeting
37
37
  ACCEPTANCE, or its verdict fails. Stop the worker and observe that it exited.
38
- Release its worktree: `workit fanout worktree release <slice>` (native
39
- worktrees: record `git -C <wt> status --short` first). Then spawn one fresh
38
+ Release its worktree: `workit fanout worktree release <slice>` (it records
39
+ `git status` first), and create it again for the retry. Then spawn one fresh
40
40
  worker with `workit fanout brief <slice> --mode resume`: same branch, and the
41
41
  brief points at its head and its ledger rows (last report, verdicts,
42
42
  rulings). If the retry fails too, escalate instead of a third worker:
@@ -69,9 +69,9 @@ list. The fixer applies that list once, then one focused recheck follows.
69
69
  When a hard slice has an uncertain approach and a cheap VERIFY, run 2-3
70
70
  attempts at once instead of retrying in series. `workit fanout brief <slice>
71
71
  --attempt <n>` gives each attempt its own branch (`<branch>-try<n>`), session
72
- and scratch dir. Each attempt needs its own worktree: Claude Code's
73
- `implementer` isolates natively; on other hosts `fanout worktree create` makes
74
- only the slice's own worktree, so run the attempts one after another there.
72
+ and scratch dir. Each attempt needs its own worktree, and `fanout worktree
73
+ create` makes only the slice's own one, so run the attempts one after another
74
+ there on every host.
75
75
  Verify every attempt, keep the one with the best verdict (accepted first,
76
76
  then the fewest changed files, then the earliest), point the slice branch at
77
77
  it (`git branch <branch> <winner>`), have the verifier record the verdict on
@@ -109,7 +109,7 @@ Which session id a verdict carries, per host:
109
109
  `WORKIT_SESSION_ID=<lead>:verifier-<random hex>`, a fresh suffix per
110
110
  verifier. Cursor cannot check it: it is declared, not derived.
111
111
  - A worker's commits: on Claude Code the lead's `WORKIT_SESSION_ID` already in
112
- its env (SubagentStart issues no implementer id; plain git, no export, so the
112
+ its env (SubagentStart issues no implementer id; no export, so the
113
113
  lead authored those commits: a verifier or reviewer records the verdict, not
114
114
  the lead); on OpenCode, Pi and Codex the id the host already
115
115
  puts in its shell; on Cursor, CI and a plain terminal the brief's
@@ -47,6 +47,8 @@ description: Build a requested change in small verified steps - follow local pat
47
47
  `workit verify-delivery push`).
48
48
 
49
49
  Independent slices that could run in parallel: fan them out (read the `workit-fanout` skill's SKILL.md and follow it).
50
+ One `implementer` slice: `workit fanout worktree create <slice>` before spawning,
51
+ then `workit fanout brief <slice>`; no host isolation (Workit's hooks confine it).
50
52
 
51
53
  Stay inline by default (workspace `delegation: inline-first`). Hand work off
52
54
  only past the thresholds: an explorer after about 3 tool calls or 10k tokens