@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
|
|
21617
|
-
if (SCRIPT_SHELLS.has((
|
|
21618
|
-
return script
|
|
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
|
|
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 (
|
|
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
|
|
21874
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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 } :
|
|
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) :
|
|
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 }) :
|
|
23115
|
+
const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE6;
|
|
22724
23116
|
if (raw.kind === "deny")
|
|
22725
23117
|
return raw;
|
|
22726
|
-
|
|
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
|
-
} :
|
|
23146
|
+
} : NONE6;
|
|
22750
23147
|
if (descriptor.transport !== "hook-process")
|
|
22751
23148
|
return rewritten(null);
|
|
22752
|
-
const nudged = withContextLine(withContextLine(canDeny ? rawGitPre(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
|
|
22762
|
-
case "write.pre":
|
|
22763
|
-
|
|
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
|
|
23185
|
+
return NONE6;
|
|
22781
23186
|
case "stop":
|
|
22782
|
-
return usable2(descriptor.events.stop.support) ? stopDecision(input, descriptor) :
|
|
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
|
|
23190
|
+
return NONE6;
|
|
22786
23191
|
case "prompt.submit":
|
|
22787
|
-
return
|
|
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
|
|
23501
|
+
import path34 from "node:path";
|
|
23097
23502
|
var launchDir = (root) => {
|
|
23098
23503
|
const location = resolveStore(root);
|
|
23099
|
-
return location instanceof Error ? null :
|
|
23504
|
+
return location instanceof Error ? null : path34.join(location.dir, "pi-launches");
|
|
23100
23505
|
};
|
|
23101
|
-
var fileFor = (dir, workerId) =>
|
|
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(
|
|
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
|
@@ -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
|
|
24
|
-
|
|
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
|
-
|
|
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) |
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
128
|
-
|
|
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>` (
|
|
39
|
-
|
|
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
|
|
73
|
-
`
|
|
74
|
-
|
|
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;
|
|
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
|