@brainervirus/workit-core 8.16.0 → 8.17.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/package.json +1 -1
- package/skills/workit-fanout/SKILL.md +7 -7
- package/skills/workit-fanout/references/brief.md +3 -1
- package/skills/workit-implement/SKILL.md +8 -1
- package/skills/workit-review/SKILL.md +2 -1
- package/skills/workit-verify-app/SKILL.md +2 -0
- package/src/check-config.ts +21 -1
- package/src/core/heavy-rules.ts +373 -0
- package/src/core/heavy-scope.ts +131 -0
- package/src/core/heavy-words.ts +92 -0
- package/src/core/heavy.ts +542 -0
- package/src/core/resources.ts +32 -76
- package/src/core/workspaces.ts +22 -0
- package/src/fanout-brief.ts +2 -0
- package/src/fanout.ts +26 -2
- package/src/hooks/descriptor.ts +2 -0
- package/src/hooks/handle.ts +32 -3
- package/src/hooks/heavy-gate.ts +91 -0
- package/src/hooks/hosts/claude-code.ts +34 -3
- package/src/hooks/hosts/codex.ts +6 -1
- package/src/hooks/hosts/cursor.ts +1 -1
- package/src/hooks/hosts/opencode.ts +1 -1
- package/src/hooks/hosts/pi.ts +1 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/protocol.ts +8 -0
- package/src/hooks/raw-git.ts +104 -2
- package/src/hooks/write-gate.ts +42 -2
package/src/hooks/raw-git.ts
CHANGED
|
@@ -544,7 +544,29 @@ export function rawInvocations(
|
|
|
544
544
|
/** The invocations of the last few commands parsed (pure: same text, same answer). */
|
|
545
545
|
const RECENT = new Map<string, readonly RawInvocation[]>();
|
|
546
546
|
|
|
547
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Every command line a shell command runs, past the same wrappers, nesting
|
|
549
|
+
* and chains rawInvocations reads (`bash -c '…'`, `flock f -c '…'`, `xargs`,
|
|
550
|
+
* `timeout 30`, env prefixes): the words after the prefixes, outermost first.
|
|
551
|
+
*/
|
|
552
|
+
export function commandLines(command: string, dialect: ShellDialect = "posix"): string[][] {
|
|
553
|
+
const lines: string[][] = [];
|
|
554
|
+
try {
|
|
555
|
+
walk(command, dialect, 0, lines);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
throw error instanceof ShellAnalysisError
|
|
558
|
+
? error
|
|
559
|
+
: new ShellAnalysisError(error instanceof Error ? error.message : String(error));
|
|
560
|
+
}
|
|
561
|
+
return lines;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function walk(
|
|
565
|
+
command: string,
|
|
566
|
+
dialect: ShellDialect,
|
|
567
|
+
depth: number,
|
|
568
|
+
seen?: string[][],
|
|
569
|
+
): RawInvocation[] {
|
|
548
570
|
const found: RawInvocation[] = [];
|
|
549
571
|
let dir: Dir = null;
|
|
550
572
|
const groups: Array<{ dir: Dir; stack: Dir[] }> = [];
|
|
@@ -556,7 +578,10 @@ function walk(command: string, dialect: ShellDialect, depth: number): RawInvocat
|
|
|
556
578
|
const sessionConfig: string[] = [];
|
|
557
579
|
const nested = (script: string | null | undefined, inner: ShellDialect) => {
|
|
558
580
|
if (!script || depth >= MAX_DEPTH) return;
|
|
559
|
-
|
|
581
|
+
const inside = seen
|
|
582
|
+
? walk(script, inner, depth + 1, seen)
|
|
583
|
+
: rawInvocations(script, inner, depth + 1);
|
|
584
|
+
for (const invocation of inside)
|
|
560
585
|
found.push({ ...invocation, dir: rebase(dir, invocation.dir) });
|
|
561
586
|
};
|
|
562
587
|
const visit = (input: string[], stdin: string | undefined, hops: number) => {
|
|
@@ -567,9 +592,18 @@ function walk(command: string, dialect: ShellDialect, depth: number): RawInvocat
|
|
|
567
592
|
const { words, env } = stripPrefixes(line);
|
|
568
593
|
const name = commandName(words[0]);
|
|
569
594
|
if (!name) return;
|
|
595
|
+
seen?.push(words);
|
|
596
|
+
// `flock <file> -c '…'`: the wrapper's script.
|
|
597
|
+
if (name === "-c" || name === "--command") return nested(words[1], "posix");
|
|
570
598
|
const runs = (lines: string[][]) => {
|
|
571
599
|
if (hops < MAX_DEPTH) for (const next of lines) visit(next, undefined, hops + 1);
|
|
572
600
|
};
|
|
601
|
+
// The gates judge what `workit check … -- <cmd>` runs as if it ran raw.
|
|
602
|
+
// (The heavy-command reader, `seen`, stops at workit: a wrapped command
|
|
603
|
+
// already runs under the lease.)
|
|
604
|
+
const wrapped = seen ? null : workitCheckTail(words);
|
|
605
|
+
if (wrapped)
|
|
606
|
+
return "script" in wrapped ? nested(wrapped.script, "posix") : runs([wrapped.argv]);
|
|
573
607
|
if (CD.has(name)) {
|
|
574
608
|
const target = cdTarget(words);
|
|
575
609
|
dir = target === null ? os.homedir() : joinDir(dir, target);
|
|
@@ -659,6 +693,74 @@ const LAUNCHERS = new Set([
|
|
|
659
693
|
"command",
|
|
660
694
|
]);
|
|
661
695
|
const WORKIT_BIN = /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/;
|
|
696
|
+
const WORKIT_PACKAGE = /^@brainervirus\/workit-cli(?:@\S*)?$/;
|
|
697
|
+
const PACKAGE_RUNNERS = new Set(["npx", "bunx", "pnpx"]);
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* What a `workit check [flags] -- <cmd…>` (or `workit lock run -- <cmd…>`,
|
|
701
|
+
* also through `npx @brainervirus/workit-cli`) runs: the script of `--shell`,
|
|
702
|
+
* else the argv. Null for any other command.
|
|
703
|
+
*/
|
|
704
|
+
export function workitCheckTail(
|
|
705
|
+
words: readonly string[],
|
|
706
|
+
): { script: string } | { argv: string[] } | null {
|
|
707
|
+
let at = 0;
|
|
708
|
+
if (PACKAGE_RUNNERS.has(commandName(words[0]))) {
|
|
709
|
+
at = words.findIndex((word, index) => index > 0 && !word.startsWith("-"));
|
|
710
|
+
if (at < 0 || !WORKIT_PACKAGE.test(words[at])) return null;
|
|
711
|
+
} else if (!/^workit(?:\.exe|\.cmd)?$/.test(commandName(words[0]))) return null;
|
|
712
|
+
const args = words.slice(at + 1);
|
|
713
|
+
if (args[0] !== "check" && !(args[0] === "lock" && args[1] === "run")) return null;
|
|
714
|
+
const split = args.indexOf("--");
|
|
715
|
+
const tail = split < 0 ? [] : args.slice(split + 1);
|
|
716
|
+
if (!tail.length) return null;
|
|
717
|
+
return args.slice(0, split).includes("--shell") ? { script: tail.join(" ") } : { argv: tail };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* The one command `command` runs after its wrappers (env prefixes, `timeout`,
|
|
722
|
+
* `nice`, `flock`, `bash -c '<one command>'`), or null when it is a chain,
|
|
723
|
+
* pipe, redirect, substitution, group or heredoc anywhere.
|
|
724
|
+
*/
|
|
725
|
+
export function simpleCommand(
|
|
726
|
+
command: string,
|
|
727
|
+
dialect: ShellDialect = "posix",
|
|
728
|
+
depth = 0,
|
|
729
|
+
): string[] | null {
|
|
730
|
+
let segments: Segment[];
|
|
731
|
+
try {
|
|
732
|
+
segments = segmentsOf(command, dialect);
|
|
733
|
+
} catch {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
if (segments.length !== 1) return null;
|
|
737
|
+
const [segment] = segments;
|
|
738
|
+
if (
|
|
739
|
+
segment.group ||
|
|
740
|
+
segment.pipe ||
|
|
741
|
+
segment.unanalyzable ||
|
|
742
|
+
segment.redirects.length ||
|
|
743
|
+
segment.subs?.length ||
|
|
744
|
+
segment.stdin?.length
|
|
745
|
+
)
|
|
746
|
+
return null;
|
|
747
|
+
const { words } = stripPrefixes(segment.words);
|
|
748
|
+
const name = commandName(words[0]);
|
|
749
|
+
if (!name || depth >= MAX_DEPTH) return null;
|
|
750
|
+
if (SHELLS.has(name)) {
|
|
751
|
+
const script = shellScript(words, undefined);
|
|
752
|
+
return script ? simpleCommand(script, "posix", depth + 1) : null;
|
|
753
|
+
}
|
|
754
|
+
if (name === "-c" || name === "--command")
|
|
755
|
+
return words[1] ? simpleCommand(words[1], "posix", depth + 1) : null;
|
|
756
|
+
if (
|
|
757
|
+
EVALS.has(name) ||
|
|
758
|
+
POWERSHELLS.has(name) ||
|
|
759
|
+
["xargs", "find", "parallel", "script", "cmd", "source", "."].includes(name)
|
|
760
|
+
)
|
|
761
|
+
return null;
|
|
762
|
+
return words;
|
|
763
|
+
}
|
|
662
764
|
|
|
663
765
|
/**
|
|
664
766
|
* The arguments of every workit CLI invocation in a shell command: past env
|
package/src/hooks/write-gate.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { HookDecision } from "./protocol";
|
|
|
23
23
|
import {
|
|
24
24
|
inWorkitWorkspace,
|
|
25
25
|
rawInvocations,
|
|
26
|
+
workitCheckTail,
|
|
26
27
|
workitInvocations,
|
|
27
28
|
type RawInvocation,
|
|
28
29
|
} from "./raw-git";
|
|
@@ -70,6 +71,28 @@ const exempt = (cwd: string, root: string, file: string, plans: ReadonlySet<stri
|
|
|
70
71
|
const PREFIXES = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
|
|
71
72
|
const operands = (words: string[]) => words.filter((item) => !item.startsWith("-"));
|
|
72
73
|
|
|
74
|
+
const SCRIPT_SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The script `workit check … -- <cmd>` runs, so the gates judge it as if it
|
|
78
|
+
* ran raw: the `--shell` string, a `bash -c` script, or the argv. Other
|
|
79
|
+
* workit verbs stay exempt (null).
|
|
80
|
+
*/
|
|
81
|
+
const wrappedScript = (input: string[]): string | null => {
|
|
82
|
+
let words = input;
|
|
83
|
+
while (words.length && (PREFIXES.has(words[0]) || /^\w+=/.test(words[0]))) words = words.slice(1);
|
|
84
|
+
const tail = workitCheckTail(words);
|
|
85
|
+
if (!tail) return null;
|
|
86
|
+
if ("script" in tail) return tail.script;
|
|
87
|
+
const [shell, flag, script] = tail.argv;
|
|
88
|
+
if (
|
|
89
|
+
SCRIPT_SHELLS.has((shell.split(/[\\/]/).at(-1) ?? "").toLowerCase()) &&
|
|
90
|
+
/^-[a-z]*c$/.test(flag ?? "")
|
|
91
|
+
)
|
|
92
|
+
return script ?? null;
|
|
93
|
+
return tail.argv.map(quoteWord).join(" ");
|
|
94
|
+
};
|
|
95
|
+
|
|
73
96
|
/** The files one simple command edits: [] none, null unknown. */
|
|
74
97
|
const editsOf = (input: string[]): string[] | null => {
|
|
75
98
|
let words = input;
|
|
@@ -149,10 +172,19 @@ const editsOf = (input: string[]): string[] | null => {
|
|
|
149
172
|
* known, else null. Interpreters and formatters are not detected; Workit's
|
|
150
173
|
* own commands never count.
|
|
151
174
|
*/
|
|
152
|
-
export const shellWrites = (
|
|
175
|
+
export const shellWrites = (
|
|
176
|
+
command: string,
|
|
177
|
+
depth = 0,
|
|
178
|
+
): { writes: boolean; targets: string[] | null } => {
|
|
153
179
|
const targets: string[] = [];
|
|
154
180
|
let unknown = false;
|
|
155
181
|
for (const segment of segmentsOf(command)) {
|
|
182
|
+
const inner = depth < 4 ? wrappedScript(segment.words) : null;
|
|
183
|
+
if (inner !== null) {
|
|
184
|
+
const nested = shellWrites(inner, depth + 1);
|
|
185
|
+
if (nested.writes && nested.targets === null) unknown = true;
|
|
186
|
+
else if (nested.targets) targets.push(...nested.targets);
|
|
187
|
+
}
|
|
156
188
|
targets.push(
|
|
157
189
|
...segment.redirects.filter((item) => !/^\/dev\/(?:null|stdout|stderr|tty)$/.test(item)),
|
|
158
190
|
);
|
|
@@ -309,7 +341,12 @@ const quoteWord = (word: string): string =>
|
|
|
309
341
|
* directory that segment runs in (`cd`, `pushd`/`popd`, `( … )` groups). A
|
|
310
342
|
* relative target in an unknown directory is skipped.
|
|
311
343
|
*/
|
|
312
|
-
const segmentTargets = (
|
|
344
|
+
const segmentTargets = (
|
|
345
|
+
cwd: string,
|
|
346
|
+
command: string,
|
|
347
|
+
dialect: ShellDialect,
|
|
348
|
+
depth = 0,
|
|
349
|
+
): string[] => {
|
|
313
350
|
const targets: string[] = [];
|
|
314
351
|
let dir: string | undefined = cwd;
|
|
315
352
|
let stack: Array<string | undefined> = [];
|
|
@@ -342,6 +379,9 @@ const segmentTargets = (cwd: string, command: string, dialect: ShellDialect): st
|
|
|
342
379
|
dir = stack.length ? stack.pop() : undefined;
|
|
343
380
|
continue;
|
|
344
381
|
}
|
|
382
|
+
const inner = depth < 4 ? wrappedScript(words) : null;
|
|
383
|
+
if (inner !== null && dir !== undefined)
|
|
384
|
+
targets.push(...segmentTargets(dir, inner, "posix", depth + 1));
|
|
345
385
|
for (const file of segment.redirects)
|
|
346
386
|
if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
|
|
347
387
|
const resolved = at(file);
|