xtrm-tools 2.4.0 → 2.4.2

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.
Files changed (125) hide show
  1. package/README.md +23 -9
  2. package/cli/dist/index.cjs +774 -240
  3. package/cli/dist/index.cjs.map +1 -1
  4. package/cli/package.json +1 -1
  5. package/config/hooks.json +10 -0
  6. package/config/pi/extensions/core/adapter.ts +2 -14
  7. package/config/pi/extensions/core/guard-rules.ts +70 -0
  8. package/config/pi/extensions/core/session-state.ts +59 -0
  9. package/config/pi/extensions/main-guard.ts +10 -14
  10. package/config/pi/extensions/plan-mode/README.md +65 -0
  11. package/config/pi/extensions/plan-mode/index.ts +340 -0
  12. package/config/pi/extensions/plan-mode/utils.ts +168 -0
  13. package/config/pi/extensions/service-skills.ts +51 -7
  14. package/config/pi/extensions/session-flow.ts +117 -0
  15. package/hooks/beads-claim-sync.mjs +123 -2
  16. package/hooks/beads-compact-restore.mjs +41 -9
  17. package/hooks/beads-compact-save.mjs +36 -5
  18. package/hooks/beads-gate-messages.mjs +27 -1
  19. package/hooks/beads-stop-gate.mjs +58 -8
  20. package/hooks/guard-rules.mjs +86 -0
  21. package/hooks/hooks.json +28 -18
  22. package/hooks/main-guard.mjs +3 -21
  23. package/hooks/quality-check.cjs +1286 -0
  24. package/hooks/quality-check.py +345 -0
  25. package/hooks/session-state.mjs +138 -0
  26. package/package.json +2 -1
  27. package/project-skills/quality-gates/.claude/settings.json +1 -24
  28. package/skills/creating-service-skills/SKILL.md +433 -0
  29. package/skills/creating-service-skills/references/script_quality_standards.md +425 -0
  30. package/skills/creating-service-skills/references/service_skill_system_guide.md +278 -0
  31. package/skills/creating-service-skills/scripts/bootstrap.py +326 -0
  32. package/skills/creating-service-skills/scripts/deep_dive.py +304 -0
  33. package/skills/creating-service-skills/scripts/scaffolder.py +482 -0
  34. package/skills/scoping-service-skills/SKILL.md +231 -0
  35. package/skills/scoping-service-skills/scripts/scope.py +74 -0
  36. package/skills/sync-docs/SKILL.md +235 -0
  37. package/skills/sync-docs/evals/evals.json +89 -0
  38. package/skills/sync-docs/references/doc-structure.md +104 -0
  39. package/skills/sync-docs/references/schema.md +103 -0
  40. package/skills/sync-docs/scripts/context_gatherer.py +246 -0
  41. package/skills/sync-docs/scripts/doc_structure_analyzer.py +495 -0
  42. package/skills/sync-docs/scripts/validate_doc.py +365 -0
  43. package/skills/sync-docs-workspace/iteration-1/benchmark.json +293 -0
  44. package/skills/sync-docs-workspace/iteration-1/benchmark.md +13 -0
  45. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/eval_metadata.json +27 -0
  46. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/outputs/result.md +210 -0
  47. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/grading.json +28 -0
  48. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/timing.json +1 -0
  49. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/outputs/result.md +101 -0
  50. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/grading.json +28 -0
  51. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/timing.json +5 -0
  52. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/timing.json +5 -0
  53. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/eval_metadata.json +27 -0
  54. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/outputs/result.md +198 -0
  55. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/grading.json +28 -0
  56. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/timing.json +1 -0
  57. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/outputs/result.md +94 -0
  58. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/grading.json +28 -0
  59. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/timing.json +1 -0
  60. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/eval_metadata.json +27 -0
  61. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/outputs/result.md +237 -0
  62. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/grading.json +28 -0
  63. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
  64. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/outputs/result.md +134 -0
  65. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/grading.json +28 -0
  66. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/timing.json +1 -0
  67. package/skills/sync-docs-workspace/iteration-2/benchmark.json +297 -0
  68. package/skills/sync-docs-workspace/iteration-2/benchmark.md +13 -0
  69. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/eval_metadata.json +27 -0
  70. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/outputs/result.md +137 -0
  71. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/grading.json +92 -0
  72. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/timing.json +1 -0
  73. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/outputs/result.md +134 -0
  74. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/grading.json +86 -0
  75. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/timing.json +1 -0
  76. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/eval_metadata.json +27 -0
  77. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/outputs/result.md +193 -0
  78. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/grading.json +72 -0
  79. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/timing.json +1 -0
  80. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/outputs/result.md +211 -0
  81. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/grading.json +91 -0
  82. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/timing.json +5 -0
  83. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/eval_metadata.json +27 -0
  84. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/outputs/result.md +182 -0
  85. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
  86. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
  87. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/outputs/result.md +222 -0
  88. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/grading.json +88 -0
  89. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
  90. package/skills/sync-docs-workspace/iteration-3/benchmark.json +298 -0
  91. package/skills/sync-docs-workspace/iteration-3/benchmark.md +13 -0
  92. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/eval_metadata.json +27 -0
  93. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/outputs/result.md +125 -0
  94. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/grading.json +97 -0
  95. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/timing.json +5 -0
  96. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/outputs/result.md +144 -0
  97. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/grading.json +78 -0
  98. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/timing.json +5 -0
  99. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/eval_metadata.json +27 -0
  100. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/outputs/result.md +104 -0
  101. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/grading.json +91 -0
  102. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/timing.json +5 -0
  103. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/outputs/result.md +79 -0
  104. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/grading.json +82 -0
  105. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/timing.json +5 -0
  106. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/eval_metadata.json +27 -0
  107. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase1_context.json +302 -0
  108. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase2_drift.txt +33 -0
  109. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase3_analysis.json +114 -0
  110. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase4_fix.txt +118 -0
  111. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase5_validate.txt +38 -0
  112. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/result.md +158 -0
  113. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
  114. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/timing.json +5 -0
  115. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/outputs/result.md +71 -0
  116. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/grading.json +90 -0
  117. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
  118. package/skills/updating-service-skills/SKILL.md +136 -0
  119. package/skills/updating-service-skills/scripts/drift_detector.py +222 -0
  120. package/skills/using-quality-gates/SKILL.md +254 -0
  121. package/skills/using-service-skills/SKILL.md +108 -0
  122. package/skills/using-service-skills/scripts/cataloger.py +74 -0
  123. package/skills/using-service-skills/scripts/skill_activator.py +152 -0
  124. package/skills/using-service-skills/scripts/test_skill_activator.py +58 -0
  125. package/skills/using-xtrm/SKILL.md +34 -38
@@ -31,7 +31,7 @@ export const COMMIT_NEXT_STEPS =
31
31
 
32
32
  // ── Edit gate messages ───────────────────────────────────────────
33
33
 
34
- export function editBlockMessage(sessionId) {
34
+ export function editBlockMessage(_sessionId) {
35
35
  return (
36
36
  '🚫 No active claim — claim an issue first.\n' +
37
37
  ' bd update <id> --claim\n'
@@ -68,6 +68,32 @@ export function stopBlockMessage(summary, claimed) {
68
68
  );
69
69
  }
70
70
 
71
+ export function stopBlockWaitingMergeMessage(state) {
72
+ const pr = state.prNumber != null ? `#${state.prNumber}` : '(PR pending)';
73
+ const prUrl = state.prUrl ? `\nPR: ${state.prUrl}` : '';
74
+ return (
75
+ `🚫 PR ${pr} not yet merged. Run: xtrm finish\n` +
76
+ `${prUrl}\n` +
77
+ `Worktree: ${state.worktreePath}\n`
78
+ );
79
+ }
80
+
81
+ export function stopBlockConflictingMessage(state) {
82
+ const conflicts = Array.isArray(state.conflictFiles) && state.conflictFiles.length > 0
83
+ ? state.conflictFiles.join(', ')
84
+ : 'unknown files';
85
+ return (
86
+ `🚫 Merge conflicts in: ${conflicts}. Resolve, push, then: xtrm finish\n` +
87
+ `Worktree: ${state.worktreePath}\n`
88
+ );
89
+ }
90
+
91
+ export function stopWarnActiveWorktreeMessage(state) {
92
+ return (
93
+ `⚠ Session has an active worktree at ${state.worktreePath}. Consider running: xtrm finish\n`
94
+ );
95
+ }
96
+
71
97
  // ── Memory gate messages ─────────────────────────────────────────
72
98
 
73
99
  export function memoryPromptMessage() {
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // beads-stop-gate — Claude Code Stop hook
3
3
  // Blocks the agent from stopping when this session has an unclosed claim in bd kv.
4
- // Falls back to global in_progress check when session_id is unavailable.
4
+ // Also blocks when xtrm session state indicates unfinished closure phases.
5
5
  // Exit 0: allow stop | Exit 2: block stop (stderr shown to Claude)
6
- //
7
- // Installed by: xtrm install
8
6
 
9
7
  import {
10
8
  readHookInput,
@@ -13,11 +11,52 @@ import {
13
11
  decideStopGate,
14
12
  } from './beads-gate-core.mjs';
15
13
  import { withSafeBdContext } from './beads-gate-utils.mjs';
16
- import { stopBlockMessage } from './beads-gate-messages.mjs';
14
+ import {
15
+ stopBlockMessage,
16
+ stopBlockWaitingMergeMessage,
17
+ stopBlockConflictingMessage,
18
+ stopWarnActiveWorktreeMessage,
19
+ } from './beads-gate-messages.mjs';
20
+ import { readSessionState } from './session-state.mjs';
17
21
 
18
22
  const input = readHookInput();
19
23
  if (!input) process.exit(0);
20
24
 
25
+ function evaluateSessionState(cwd) {
26
+ const state = readSessionState(cwd);
27
+ if (!state) return { allow: true };
28
+
29
+ if (state.phase === 'cleanup-done' || state.phase === 'merged') {
30
+ return { allow: true, state };
31
+ }
32
+
33
+ if (state.phase === 'waiting-merge' || state.phase === 'pending-cleanup') {
34
+ return {
35
+ allow: false,
36
+ state,
37
+ message: stopBlockWaitingMergeMessage(state),
38
+ };
39
+ }
40
+
41
+ if (state.phase === 'conflicting') {
42
+ return {
43
+ allow: false,
44
+ state,
45
+ message: stopBlockConflictingMessage(state),
46
+ };
47
+ }
48
+
49
+ if (state.phase === 'claimed' || state.phase === 'phase1-done') {
50
+ return {
51
+ allow: true,
52
+ state,
53
+ warning: stopWarnActiveWorktreeMessage(state),
54
+ };
55
+ }
56
+
57
+ return { allow: true, state };
58
+ }
59
+
21
60
  withSafeBdContext(() => {
22
61
  const ctx = resolveSessionContext(input);
23
62
  if (!ctx || !ctx.isBeadsProject) process.exit(0);
@@ -25,9 +64,20 @@ withSafeBdContext(() => {
25
64
  const state = resolveClaimAndWorkState(ctx);
26
65
  const decision = decideStopGate(ctx, state);
27
66
 
28
- if (decision.allow) process.exit(0);
67
+ if (!decision.allow) {
68
+ process.stderr.write(stopBlockMessage(decision.summary, decision.claimed));
69
+ process.exit(2);
70
+ }
71
+
72
+ const sessionDecision = evaluateSessionState(ctx.cwd);
73
+ if (!sessionDecision.allow) {
74
+ process.stderr.write(sessionDecision.message);
75
+ process.exit(2);
76
+ }
77
+
78
+ if (sessionDecision.warning) {
79
+ process.stderr.write(sessionDecision.warning);
80
+ }
29
81
 
30
- // Block with message
31
- process.stderr.write(stopBlockMessage(decision.summary, decision.claimed));
32
- process.exit(2);
82
+ process.exit(0);
33
83
  });
@@ -0,0 +1,86 @@
1
+ // Canonical guard rule definitions shared across hooks, policies, and extensions.
2
+ // Pure data module: named exports only.
3
+
4
+ export const WRITE_TOOLS = [
5
+ 'Edit',
6
+ 'Write',
7
+ 'MultiEdit',
8
+ 'NotebookEdit',
9
+ 'mcp__serena__rename_symbol',
10
+ 'mcp__serena__replace_symbol_body',
11
+ 'mcp__serena__insert_after_symbol',
12
+ 'mcp__serena__insert_before_symbol',
13
+ ];
14
+
15
+ export const DANGEROUS_BASH_PATTERNS = [
16
+ 'sed\\s+-i',
17
+ 'echo\\s+[^\\n]*>',
18
+ 'printf\\s+[^\\n]*>',
19
+ 'cat\\s+[^\\n]*>',
20
+ 'tee\\b',
21
+ '(?:^|\\s)(?:vim|nano|vi)\\b',
22
+ '(?:^|\\s)mv\\b',
23
+ '(?:^|\\s)cp\\b',
24
+ '(?:^|\\s)rm\\b',
25
+ '(?:^|\\s)mkdir\\b',
26
+ '(?:^|\\s)touch\\b',
27
+ '(?:^|\\s)chmod\\b',
28
+ '(?:^|\\s)chown\\b',
29
+ '>>',
30
+ '(?:^|\\s)git\\s+add\\b',
31
+ '(?:^|\\s)git\\s+commit\\b',
32
+ '(?:^|\\s)git\\s+merge\\b',
33
+ '(?:^|\\s)git\\s+push\\b',
34
+ '(?:^|\\s)git\\s+reset\\b',
35
+ '(?:^|\\s)git\\s+checkout\\b',
36
+ '(?:^|\\s)git\\s+rebase\\b',
37
+ '(?:^|\\s)git\\s+stash\\b',
38
+ '(?:^|\\s)npm\\s+install\\b',
39
+ '(?:^|\\s)bun\\s+install\\b',
40
+ '(?:^|\\s)bun\\s+add\\b',
41
+ '(?:^|\\s)node\\s+(?:-e|--eval)\\b',
42
+ '(?:^|\\s)bun\\s+(?:-e|--eval)\\b',
43
+ '(?:^|\\s)python\\s+-c\\b',
44
+ '(?:^|\\s)perl\\s+-e\\b',
45
+ '(?:^|\\s)ruby\\s+-e\\b',
46
+ ];
47
+
48
+ export const SAFE_BASH_PREFIXES = [
49
+ 'git status',
50
+ 'git log',
51
+ 'git diff',
52
+ 'git show',
53
+ 'git blame',
54
+ 'git branch',
55
+ 'git fetch',
56
+ 'git remote',
57
+ 'git config',
58
+ 'git pull',
59
+ 'git stash',
60
+ 'git worktree',
61
+ 'git checkout -b',
62
+ 'git switch -c',
63
+ 'gh',
64
+ 'bd',
65
+ 'touch .beads/',
66
+ 'npx gitnexus',
67
+ ];
68
+
69
+ export const NATIVE_TEAM_TOOLS = [
70
+ 'Task',
71
+ 'TeamCreate',
72
+ 'TeamDelete',
73
+ 'SendMessage',
74
+ 'TaskCreate',
75
+ 'TaskUpdate',
76
+ 'TaskList',
77
+ 'TaskGet',
78
+ 'TaskOutput',
79
+ 'TaskStop',
80
+ ];
81
+
82
+ export const INTERACTIVE_TOOLS = [
83
+ 'AskUserQuestion',
84
+ 'EnterPlanMode',
85
+ 'EnterWorktree',
86
+ ];
package/hooks/hooks.json CHANGED
@@ -2,12 +2,17 @@
2
2
  "hooks": {
3
3
  "PreToolUse": [
4
4
  {
5
- "matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
5
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
9
  "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/main-guard.mjs",
10
10
  "timeout": 5000
11
+ },
12
+ {
13
+ "type": "command",
14
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-edit-gate.mjs",
15
+ "timeout": 5000
11
16
  }
12
17
  ]
13
18
  },
@@ -25,16 +30,6 @@
25
30
  "timeout": 5000
26
31
  }
27
32
  ]
28
- },
29
- {
30
- "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
31
- "hooks": [
32
- {
33
- "type": "command",
34
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-edit-gate.mjs",
35
- "timeout": 5000
36
- }
37
- ]
38
33
  }
39
34
  ],
40
35
  "PostToolUse": [
@@ -58,6 +53,21 @@
58
53
  }
59
54
  ]
60
55
  },
56
+ {
57
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
58
+ "hooks": [
59
+ {
60
+ "type": "command",
61
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/quality-check.cjs",
62
+ "timeout": 30000
63
+ },
64
+ {
65
+ "type": "command",
66
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/quality-check.py",
67
+ "timeout": 30000
68
+ }
69
+ ]
70
+ },
61
71
  {
62
72
  "matcher": "Bash|mcp__serena__find_symbol|mcp__serena__get_symbols_overview|mcp__serena__search_for_pattern|mcp__serena__find_referencing_symbols|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|mcp__serena__rename_symbol",
63
73
  "hooks": [
@@ -69,33 +79,33 @@
69
79
  ]
70
80
  }
71
81
  ],
72
- "SessionStart": [
82
+ "Stop": [
73
83
  {
74
84
  "hooks": [
75
85
  {
76
86
  "type": "command",
77
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-compact-restore.mjs",
87
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-stop-gate.mjs",
78
88
  "timeout": 5000
79
89
  },
80
90
  {
81
91
  "type": "command",
82
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/serena-workflow-reminder.py"
92
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-memory-gate.mjs",
93
+ "timeout": 8000
83
94
  }
84
95
  ]
85
96
  }
86
97
  ],
87
- "Stop": [
98
+ "SessionStart": [
88
99
  {
89
100
  "hooks": [
90
101
  {
91
102
  "type": "command",
92
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-stop-gate.mjs",
103
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-compact-restore.mjs",
93
104
  "timeout": 5000
94
105
  },
95
106
  {
96
107
  "type": "command",
97
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-memory-gate.mjs",
98
- "timeout": 8000
108
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/serena-workflow-reminder.py"
99
109
  }
100
110
  ]
101
111
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { execSync } from 'node:child_process';
8
8
  import { readFileSync } from 'node:fs';
9
+ import { WRITE_TOOLS, SAFE_BASH_PREFIXES } from './guard-rules.mjs';
9
10
 
10
11
  let branch = '';
11
12
  try {
@@ -44,18 +45,7 @@ function deny(reason) {
44
45
  process.exit(0);
45
46
  }
46
47
 
47
- const WRITE_TOOLS = new Set([
48
- 'Edit',
49
- 'Write',
50
- 'MultiEdit',
51
- 'NotebookEdit',
52
- 'mcp__serena__rename_symbol',
53
- 'mcp__serena__replace_symbol_body',
54
- 'mcp__serena__insert_after_symbol',
55
- 'mcp__serena__insert_before_symbol',
56
- ]);
57
-
58
- if (WRITE_TOOLS.has(tool)) {
48
+ if (WRITE_TOOLS.includes(tool)) {
59
49
  deny(`⛔ On '${branch}' — start on a feature branch and claim an issue.\n`
60
50
  + ' git checkout -b feature/<name>\n'
61
51
  + ' bd update <id> --claim\n');
@@ -91,17 +81,9 @@ if (tool === 'Bash') {
91
81
  // Important: do not allow generic checkout/switch forms, which include
92
82
  // mutating variants such as `git checkout -- <path>`.
93
83
  const SAFE_BASH_PATTERNS = [
94
- /^git\s+(status|log|diff|branch|show|describe|fetch|remote|config)\b/,
95
- /^git\s+pull\b/,
96
- /^git\s+stash\b/,
97
- /^git\s+worktree\b/,
98
- /^git\s+checkout\s+-b\s+\S+/,
99
- /^git\s+switch\s+-c\s+\S+/,
84
+ ...SAFE_BASH_PREFIXES.map(prefix => new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)),
100
85
  // Allow post-merge sync to protected branch only (not arbitrary origin refs)
101
86
  ...protectedBranches.map(b => new RegExp(`^git\\s+reset\\s+--hard\\s+origin/${b}\\b`)),
102
- /^gh\s+/,
103
- /^bd\s+/,
104
- /^touch\s+\.beads\//,
105
87
  ];
106
88
 
107
89
  if (SAFE_BASH_PATTERNS.some(p => p.test(cmd))) {