@brainervirus/workit-core 8.11.0 → 8.12.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.
@@ -28,6 +28,12 @@
28
28
  // it would commit a trailer-only message instead of aborting); for a
29
29
  // template (git aborts an unedited one); for a comment-only message; and in a
30
30
  // rebase or cherry-pick replaying commits that keep their own trailers.
31
+ // - A workspace with `trailers: false`: the helper adds nothing when
32
+ // WORKIT_TRAILERS=off (set by `workit git commit`) or `workit git trailers`
33
+ // prints `off` (asked with a timeout when workit is on PATH). Without
34
+ // workit it fails open and adds the trailer, so doctor warns about any
35
+ // Workit hook or helper left there and `--fix` removes them
36
+ // (removeSessionHook); another tool's hook and its manual line stay.
31
37
  // - `--if-exists addIfDifferent`: the same session's trailer is never repeated;
32
38
  // another session that amends adds its own, as `workit git commit --amend` does.
33
39
  import { spawnSync } from "node:child_process";
@@ -80,6 +86,19 @@ fi
80
86
  [ -n "$session" ] || exit 0
81
87
  [ \${#session} -le 128 ] || exit 0
82
88
  case "$session" in *[!A-Za-z0-9_.:@/+-]*) exit 0 ;; esac
89
+ # A workspace with \`trailers: false\` keeps authorship in the ledger only.
90
+ # \`workit git commit\` decides it already (WORKIT_TRAILERS); otherwise ask
91
+ # workit, bounded, and add the trailer when it cannot answer.
92
+ case "\${WORKIT_TRAILERS-}" in
93
+ off) exit 0 ;;
94
+ on) ;;
95
+ *)
96
+ if command -v workit >/dev/null 2>&1; then
97
+ t=; ! command -v timeout >/dev/null 2>&1 || t="timeout 5"
98
+ [ "$($t workit git trailers 2>/dev/null)" != off ] || exit 0
99
+ fi
100
+ ;;
101
+ esac
83
102
  add_trailer() {
84
103
  [ -f "$1" ] && [ -r "$1" ] || return 0
85
104
  body=$(sed '/^# -\\{24\\} >8 -\\{24\\}$/,$d' "$1" | git stripspace --strip-comments)
@@ -519,3 +538,52 @@ export function installSessionHook(
519
538
  detail: `${verb} the Workit ${SESSION_HOOK_NAME} hook at ${status.hookPath}${helper}`,
520
539
  };
521
540
  }
541
+
542
+ export type SessionHookRemoval = {
543
+ /** Whether Workit's own hook or helper was deleted. */
544
+ action: "removed" | "unchanged" | "skipped";
545
+ /** The files deleted: Workit's hook (only one carrying its marker) and the helper. */
546
+ removed: string[];
547
+ status: SessionHookStatus;
548
+ detail: string;
549
+ };
550
+
551
+ /** Whether `status` finds Workit's own hook (any version) in the slot. */
552
+ export const workitHookIn = (status: SessionHookStatus): boolean =>
553
+ status.hookPath !== null && (readText(status.hookPath)?.includes(SESSION_HOOK_MARKER) ?? false);
554
+
555
+ /** Whether the helper exists for `status`, current or not. */
556
+ export const helperIn = (status: SessionHookStatus): boolean =>
557
+ status.helperPath !== null && exists(status.helperPath);
558
+
559
+ /**
560
+ * For a workspace with `trailers: false`: delete Workit's own hook (only a
561
+ * slot holding its marker) and the helper. Another tool's hook, a husky
562
+ * script or a lefthook job that runs the helper is never edited; without the
563
+ * helper its manual line runs nothing.
564
+ */
565
+ export function removeSessionHook(
566
+ cwd: string,
567
+ env: NodeJS.ProcessEnv = process.env,
568
+ ): SessionHookRemoval {
569
+ const status = inspectSessionHook(cwd, env);
570
+ if (status.state === "not_git")
571
+ return { action: "skipped", removed: [], status, detail: "not inside a git work tree" };
572
+ const removed: string[] = [];
573
+ if (workitHookIn(status)) {
574
+ rmSync(status.hookPath!, { force: true });
575
+ removed.push(status.hookPath!);
576
+ }
577
+ if (helperIn(status)) {
578
+ rmSync(status.helperPath!, { force: true });
579
+ removed.push(status.helperPath!);
580
+ }
581
+ return {
582
+ action: removed.length ? "removed" : "unchanged",
583
+ removed,
584
+ status: inspectSessionHook(cwd, env),
585
+ detail: removed.length
586
+ ? `removed ${removed.join(" and ")}`
587
+ : "no Workit session hook or helper to remove",
588
+ };
589
+ }
@@ -7,6 +7,7 @@ import {
7
7
  } from "./context";
8
8
  import type { HostDescriptor, Support } from "./descriptor";
9
9
  import { branchGuidance } from "../core/branch";
10
+ import { workspaceTrailers } from "../core/workspaces";
10
11
  import { recordIssuedSession } from "../host-session";
11
12
  import { shellPolicy } from "./policy";
12
13
  import {
@@ -78,8 +79,12 @@ const CLAUDE_WORKTREE = /[\\/]\.claude[\\/]worktrees[\\/]/;
78
79
  * (`workit git …` included) and cannot be turned off, so an isolated agent
79
80
  * runs git itself and workit for everything else.
80
81
  */
81
- const ISOLATED_GIT =
82
- 'Run git as plain, separate commands inside your worktree (`git switch -c <branch> <base>`, `git commit --trailer "Workit-Session=<your session>"`, `git push`; `workit doctor --fix` can install a commit-msg hook that adds the trailer), 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`).';
82
+ const isolatedGit = (cwd: string): string =>
83
+ `Run git as plain, separate commands inside your worktree (\`git switch -c <branch> <base>\`, ${
84
+ workspaceTrailers(cwd).enabled
85
+ ? '`git commit --trailer "Workit-Session=<your session>"`, `git push`; `workit doctor --fix` can install a prepare-commit-msg hook that adds the trailer'
86
+ : "`git commit` (this workspace sets trailers: false: add no Workit-Session trailer), `git push`"
87
+ }; 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\`).`;
83
88
 
84
89
  const subagentStartText = (
85
90
  input: HookInput,
@@ -94,9 +99,9 @@ const subagentStartText = (
94
99
  (role === "implementer" || CLAUDE_WORKTREE.test(input.cwd));
95
100
  const guide = role === "implementer" ? branchGuidance(input.cwd) : null;
96
101
  if (role === "implementer" && isolated)
97
- return `Workit observed ${who} working in its own git worktree: it may edit and commit there, within its brief's scope. ${ISOLATED_GIT} 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.`;
102
+ 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.`;
98
103
  if (isolated)
99
- return `${ISOLATED_GIT}\n${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
104
+ return `${isolatedGit(input.cwd)}\n${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
100
105
  if (role === "implementer")
101
106
  // No host isolation: the lead makes the worktree, and workit git works there.
102
107
  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.`;