@hasna/hooks 0.3.11 → 0.4.1

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.
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import {
4
+ canonicalRepoIdentity,
5
+ canonicalWorktreeTemplate,
4
6
  classifyDangerousOperation,
5
7
  claimCommand,
6
8
  gitCommandInfo,
@@ -10,7 +12,6 @@ import {
10
12
  managedWorktreeInfo,
11
13
  readInput,
12
14
  respond,
13
- runIdFrom,
14
15
  taskIdFrom,
15
16
  warn,
16
17
  type CodewithHookInput,
@@ -40,27 +41,63 @@ export async function evaluate(input: CodewithHookInput): Promise<{ output: Reco
40
41
  const managed = managedWorktreeInfo(targetCwd);
41
42
  if (managed.managed) return { output: { continue: true }, warnings };
42
43
 
44
+ // Migration shim, temporary and deliberately narrow: worktrees created under the
45
+ // pre-rule-8 station-id lease layout are non-compliant, but they are real, active
46
+ // worktrees. Hard-blocking their git work on day one would strand in-flight tasks
47
+ // with no way to land, so this one recognised layout warns instead of blocking.
48
+ // Everything else non-canonical is still blocked. Remove once they are re-homed.
49
+ if (managed.layout === "legacy-station-lease") {
50
+ const message = `${managed.reason}. This layout will stop being tolerated; re-home this worktree.`;
51
+ warnings.push(message);
52
+ return {
53
+ output: {
54
+ continue: true,
55
+ hookSpecificOutput: {
56
+ hookEventName: "PreToolUse",
57
+ additionalContext: `[worktree-guard] ${message}`,
58
+ },
59
+ },
60
+ warnings,
61
+ };
62
+ }
63
+
43
64
  const repoRoot = await gitRepoRoot(targetCwd);
44
- const repo = await gitRemoteSlug(targetCwd) || (repoRoot ? repoRoot.split("/").filter(Boolean).pop() || null : null);
65
+ // Rule 8 resolution order: the repos CLI is the source of truth for <repo-name>.
66
+ // The local checkout directory is the next best guess, and the remote slug is the
67
+ // last resort — for most repos the remote basename is NOT the canonical repo name.
68
+ const identity = await canonicalRepoIdentity(targetCwd);
69
+ const repo = identity.name
70
+ || (repoRoot ? repoRoot.split("/").filter(Boolean).pop() || null : null)
71
+ || (await gitRemoteSlug(targetCwd))?.split("/").filter(Boolean).pop()
72
+ || null;
45
73
  const taskId = taskIdFrom(input);
46
- const runId = runIdFrom(input);
47
- const recommended = claimCommand(repo, taskId, runId);
74
+ const recommended = claimCommand(repo, taskId, identity.defaultBranch);
48
75
  const action = gitInfo?.action || null;
76
+ const canonical = canonicalWorktreeTemplate(managed.root);
77
+ // Rule 8 requires an exact repos-CLI lookup for <repo-name>. When that lookup did
78
+ // not resolve, the name below is a local guess, so say so rather than imply it is
79
+ // authoritative — the repo directory name and the remote basename often differ.
80
+ const unverifiedRepoName = identity.name
81
+ ? null
82
+ : `Confirm <repo-name> with an exact repos CLI lookup first: repos repo ${repo || "<name>"} --json`;
49
83
 
50
84
  if (action) {
51
85
  const reason = [
52
- `Blocked git ${action} outside a managed repos worktree (${managed.reason || "not under managed root"}).`,
86
+ `Blocked git ${action} outside a canonical task worktree (${managed.reason || "not under managed root"}).`,
53
87
  `Command target cwd: ${targetCwd}`,
54
- `Managed root: ${managed.root}`,
55
- `Claim a task worktree first: ${recommended}`,
88
+ `Canonical worktree path (Agent Operating Rules rule 8): ${canonical}`,
89
+ `Create one first: ${recommended}`,
90
+ ...(unverifiedRepoName ? [unverifiedRepoName] : []),
56
91
  ].join(" ");
57
92
  return { output: { decision: "block", reason }, warnings };
58
93
  }
59
94
 
60
95
  if (isFeatureWork(input, command)) {
61
96
  warnings.push([
62
- `Feature work appears to be outside a managed repos worktree (${managed.reason || "not under managed root"}).`,
97
+ `Feature work appears to be outside a canonical task worktree (${managed.reason || "not under managed root"}).`,
98
+ `Canonical worktree path (Agent Operating Rules rule 8): ${canonical}.`,
63
99
  `Use: ${recommended}`,
100
+ ...(unverifiedRepoName ? [unverifiedRepoName] : []),
64
101
  ].join(" "));
65
102
  return {
66
103
  output: {
@@ -91,4 +128,9 @@ export async function run(): Promise<void> {
91
128
 
92
129
  if (import.meta.main) {
93
130
  await run();
131
+ // The verdict is written, so the hook is done. Exit rather than waiting for the
132
+ // event loop to drain: an optional CLI consulted during evaluation may have left a
133
+ // grandchild holding a pipe open, and a hook that has already answered must never
134
+ // keep the caller's PreToolUse path waiting on it.
135
+ process.exit(0);
94
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.3.11",
3
+ "version": "0.4.1",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {