@brainervirus/workit-core 8.21.2 → 8.22.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/src/core/methods.ts +3 -0
- package/src/hooks/cwd-normalize.ts +45 -0
- package/src/hooks/handle.ts +14 -4
- package/src/stack.ts +1 -1
package/package.json
CHANGED
package/src/core/methods.ts
CHANGED
|
@@ -125,6 +125,9 @@ Autonomy contract:
|
|
|
125
125
|
CLI first: if a step has one right answer, use the \`workit\` verb instead of
|
|
126
126
|
hand-running git, gh or glab (\`workit git|pr|ci|check|help\`;
|
|
127
127
|
else \`npx -y @brainervirus/workit-cli\`).
|
|
128
|
+
Other checkout: \`workit --cwd <dir> <verb>\` (Codex: \`<verb> --cwd <dir>\`); one workit
|
|
129
|
+
verb per shell call, never \`cd <dir> && workit\`. zsh-safe: no \`echo =====\`
|
|
130
|
+
(\`printf '%s\\n' ---\`), quote globs and URLs, no variable named \`status\` or \`path\`.
|
|
128
131
|
Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
|
|
129
132
|
an amend.
|
|
130
133
|
The task families (task, policy, evidence, finding, decision, worker, state)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// `cd <dir> && workit <args>` → `workit --cwd <dir> <args>` (issue #388).
|
|
2
|
+
// On Claude Code the compound does not match a `Bash(workit:*)` allow rule
|
|
3
|
+
// (every subcommand must match, and a `cd` outside the working dirs breaks
|
|
4
|
+
// it), so the auto-mode classifier judged it. The rewrite is exact and
|
|
5
|
+
// conservative: one literal directory, one workit call, and only read-only
|
|
6
|
+
// filters after it. Anything else is left alone. Pure string work.
|
|
7
|
+
import { segmentsOf } from "./shell-words";
|
|
8
|
+
|
|
9
|
+
// A directory spelled as one literal word: bare, '…' or "…" with no expansion.
|
|
10
|
+
const COMPOUND =
|
|
11
|
+
/^\s*cd\s+([A-Za-z0-9_./@%+:,=-]+|'[^'$`\\\n]*'|"[^"$`\\!\n]*")\s*&&\s*(workit(?:\s[\s\S]*)?)$/u;
|
|
12
|
+
const FILTERS = new Set(["tail", "head", "grep"]);
|
|
13
|
+
|
|
14
|
+
// A filter that reads only stdin, so moving the cwd cannot change what it
|
|
15
|
+
// reads: flags and counts for tail/head, one pattern for grep (no -r/-R/-f).
|
|
16
|
+
const stdinOnly = (words: readonly string[]): boolean => {
|
|
17
|
+
const [name, ...args] = words;
|
|
18
|
+
const operands = args.filter((w) => !w.startsWith("-"));
|
|
19
|
+
if (name === "grep")
|
|
20
|
+
return operands.length <= 1 && !args.some((w) => /^-[^-]*[rRf]|^--(recursive|file)/u.test(w));
|
|
21
|
+
return operands.every((w) => /^\d+$/u.test(w)) && !args.some((w) => w.startsWith("--output"));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The command rewritten to `workit --cwd <dir> …`, or null when it is not
|
|
26
|
+
* exactly `cd <literal dir> && workit … [2>&1] [| tail|head|grep …]*`.
|
|
27
|
+
*/
|
|
28
|
+
export const cwdRewrite = (command: string): string | null => {
|
|
29
|
+
const match = COMPOUND.exec(command);
|
|
30
|
+
const dir = match?.[1];
|
|
31
|
+
const rest = match?.[2];
|
|
32
|
+
if (!dir || !rest || dir.startsWith("-")) return null;
|
|
33
|
+
// `2>&1` is the only redirect, `|` the only operator: no `&`, `;`, `<`, `>`, backtick or newline.
|
|
34
|
+
if (/[&;<>`\n]/u.test(rest.replaceAll("2>&1", ""))) return null;
|
|
35
|
+
const segments = segmentsOf(rest);
|
|
36
|
+
const [head, ...filters] = segments;
|
|
37
|
+
if (!head || head.words[0] !== "workit") return null;
|
|
38
|
+
if (head.words.some((w) => w === "--cwd" || w.startsWith("--cwd="))) return null;
|
|
39
|
+
const clean = (s: (typeof segments)[number]) =>
|
|
40
|
+
!s.unanalyzable && !s.group && !s.subs?.length && !s.stdin?.length && !s.redirects.length;
|
|
41
|
+
if (!segments.every(clean)) return null;
|
|
42
|
+
if (!segments.slice(0, -1).every((s) => s.pipe)) return null;
|
|
43
|
+
if (!filters.every((s) => FILTERS.has(s.words[0] ?? "") && stdinOnly(s.words))) return null;
|
|
44
|
+
return `workit --cwd ${dir} ${rest.slice("workit".length).trimStart()}`.trimEnd();
|
|
45
|
+
};
|
package/src/hooks/handle.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { HostDescriptor, Support } from "./descriptor";
|
|
|
9
9
|
import { branchGuidance } from "../core/branch";
|
|
10
10
|
import { workspaceTrailers } from "../core/workspaces";
|
|
11
11
|
import { judgeBranch, recordIssuedSession, verdictCommand } from "../host-session";
|
|
12
|
+
import { cwdRewrite } from "./cwd-normalize";
|
|
12
13
|
import { heavyDenial, heavyGate, rewriteNote, splitDenial } from "./heavy-gate";
|
|
13
14
|
import { shellPolicy } from "./policy";
|
|
14
15
|
import {
|
|
@@ -204,6 +205,10 @@ function decide(input: HookInput, deps: HookDeps): HookDecision {
|
|
|
204
205
|
const rewrites = usable(descriptor.shellPolicy.rewrite);
|
|
205
206
|
if (plan.kind === "rewrite" && !rewrites && canDeny) return heavyDenial(event.command, plan);
|
|
206
207
|
const lease = plan.kind === "rewrite" && rewrites ? plan : null;
|
|
208
|
+
// `cd <dir> && workit …` becomes `workit --cwd <dir> …` (issue #388). A
|
|
209
|
+
// workit command is never heavy itself, so the lease plan wins when set.
|
|
210
|
+
const cwdCommand =
|
|
211
|
+
!lease && rewrites && input.host === "claude_code" ? cwdRewrite(event.command) : null;
|
|
207
212
|
const aside = plan.kind === "passthrough" ? plan.note : null;
|
|
208
213
|
// The command will run: note HEAD so the post-tool hook (or the next
|
|
209
214
|
// command, without one) can tell whether a raw commit moved it.
|
|
@@ -212,12 +217,13 @@ function decide(input: HookInput, deps: HookDeps): HookDecision {
|
|
|
212
217
|
event.command,
|
|
213
218
|
postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND,
|
|
214
219
|
);
|
|
220
|
+
const rewrittenTo = lease?.command ?? cwdCommand;
|
|
215
221
|
const rewritten = (text: string | null): HookDecision =>
|
|
216
|
-
|
|
222
|
+
rewrittenTo
|
|
217
223
|
? {
|
|
218
224
|
kind: "rewrite",
|
|
219
|
-
command:
|
|
220
|
-
input: event.toolInput ? { ...event.toolInput, command:
|
|
225
|
+
command: rewrittenTo,
|
|
226
|
+
input: event.toolInput ? { ...event.toolInput, command: rewrittenTo } : null,
|
|
221
227
|
text,
|
|
222
228
|
}
|
|
223
229
|
: NONE;
|
|
@@ -228,7 +234,11 @@ function decide(input: HookInput, deps: HookDeps): HookDecision {
|
|
|
228
234
|
withContextLine(canDeny ? rawGitPre(input, event.command) : NONE, aside),
|
|
229
235
|
shipNudge(input, event.command),
|
|
230
236
|
);
|
|
231
|
-
|
|
237
|
+
// A deny always wins over the cwd rewrite.
|
|
238
|
+
if (!lease)
|
|
239
|
+
return cwdCommand && nudged.kind !== "deny"
|
|
240
|
+
? rewritten(nudged.kind === "context" ? nudged.text : null)
|
|
241
|
+
: nudged;
|
|
232
242
|
return rewritten(
|
|
233
243
|
[rewriteNote(lease), nudged.kind === "context" ? nudged.text : null]
|
|
234
244
|
.filter(Boolean)
|
package/src/stack.ts
CHANGED
|
@@ -1494,7 +1494,7 @@ export function syncStack(
|
|
|
1494
1494
|
return stackFail(
|
|
1495
1495
|
"blocked",
|
|
1496
1496
|
`a rebase is in progress in ${owner}`,
|
|
1497
|
-
`
|
|
1497
|
+
`git -C ${owner} rebase --continue # or --abort; then workit --cwd ${owner} stack sync`,
|
|
1498
1498
|
{ conflict: entry.branch, worktree: owner, progress: outcome },
|
|
1499
1499
|
);
|
|
1500
1500
|
if (owner && trackedDirt(owner))
|