xtrm-tools 2.4.1 → 2.4.3

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 (126) hide show
  1. package/README.md +15 -6
  2. package/cli/dist/index.cjs +738 -239
  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 +140 -14
  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-memory-gate.mjs +24 -16
  20. package/hooks/beads-stop-gate.mjs +58 -8
  21. package/hooks/guard-rules.mjs +117 -0
  22. package/hooks/hooks.json +28 -18
  23. package/hooks/main-guard.mjs +22 -22
  24. package/hooks/quality-check.cjs +1286 -0
  25. package/hooks/quality-check.py +345 -0
  26. package/hooks/session-state.mjs +138 -0
  27. package/package.json +2 -1
  28. package/project-skills/quality-gates/.claude/settings.json +1 -24
  29. package/skills/creating-service-skills/SKILL.md +433 -0
  30. package/skills/creating-service-skills/references/script_quality_standards.md +425 -0
  31. package/skills/creating-service-skills/references/service_skill_system_guide.md +278 -0
  32. package/skills/creating-service-skills/scripts/bootstrap.py +326 -0
  33. package/skills/creating-service-skills/scripts/deep_dive.py +304 -0
  34. package/skills/creating-service-skills/scripts/scaffolder.py +482 -0
  35. package/skills/scoping-service-skills/SKILL.md +231 -0
  36. package/skills/scoping-service-skills/scripts/scope.py +74 -0
  37. package/skills/sync-docs/SKILL.md +235 -0
  38. package/skills/sync-docs/evals/evals.json +89 -0
  39. package/skills/sync-docs/references/doc-structure.md +104 -0
  40. package/skills/sync-docs/references/schema.md +103 -0
  41. package/skills/sync-docs/scripts/context_gatherer.py +246 -0
  42. package/skills/sync-docs/scripts/doc_structure_analyzer.py +495 -0
  43. package/skills/sync-docs/scripts/validate_doc.py +365 -0
  44. package/skills/sync-docs-workspace/iteration-1/benchmark.json +293 -0
  45. package/skills/sync-docs-workspace/iteration-1/benchmark.md +13 -0
  46. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/eval_metadata.json +27 -0
  47. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/outputs/result.md +210 -0
  48. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/grading.json +28 -0
  49. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/timing.json +1 -0
  50. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/outputs/result.md +101 -0
  51. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/grading.json +28 -0
  52. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/timing.json +5 -0
  53. package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/timing.json +5 -0
  54. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/eval_metadata.json +27 -0
  55. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/outputs/result.md +198 -0
  56. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/grading.json +28 -0
  57. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/timing.json +1 -0
  58. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/outputs/result.md +94 -0
  59. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/grading.json +28 -0
  60. package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/timing.json +1 -0
  61. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/eval_metadata.json +27 -0
  62. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/outputs/result.md +237 -0
  63. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/grading.json +28 -0
  64. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
  65. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/outputs/result.md +134 -0
  66. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/grading.json +28 -0
  67. package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/timing.json +1 -0
  68. package/skills/sync-docs-workspace/iteration-2/benchmark.json +297 -0
  69. package/skills/sync-docs-workspace/iteration-2/benchmark.md +13 -0
  70. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/eval_metadata.json +27 -0
  71. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/outputs/result.md +137 -0
  72. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/grading.json +92 -0
  73. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/timing.json +1 -0
  74. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/outputs/result.md +134 -0
  75. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/grading.json +86 -0
  76. package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/timing.json +1 -0
  77. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/eval_metadata.json +27 -0
  78. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/outputs/result.md +193 -0
  79. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/grading.json +72 -0
  80. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/timing.json +1 -0
  81. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/outputs/result.md +211 -0
  82. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/grading.json +91 -0
  83. package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/timing.json +5 -0
  84. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/eval_metadata.json +27 -0
  85. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/outputs/result.md +182 -0
  86. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
  87. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
  88. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/outputs/result.md +222 -0
  89. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/grading.json +88 -0
  90. package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
  91. package/skills/sync-docs-workspace/iteration-3/benchmark.json +298 -0
  92. package/skills/sync-docs-workspace/iteration-3/benchmark.md +13 -0
  93. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/eval_metadata.json +27 -0
  94. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/outputs/result.md +125 -0
  95. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/grading.json +97 -0
  96. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/timing.json +5 -0
  97. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/outputs/result.md +144 -0
  98. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/grading.json +78 -0
  99. package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/timing.json +5 -0
  100. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/eval_metadata.json +27 -0
  101. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/outputs/result.md +104 -0
  102. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/grading.json +91 -0
  103. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/timing.json +5 -0
  104. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/outputs/result.md +79 -0
  105. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/grading.json +82 -0
  106. package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/timing.json +5 -0
  107. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/eval_metadata.json +27 -0
  108. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase1_context.json +302 -0
  109. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase2_drift.txt +33 -0
  110. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase3_analysis.json +114 -0
  111. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase4_fix.txt +118 -0
  112. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase5_validate.txt +38 -0
  113. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/result.md +158 -0
  114. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
  115. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/timing.json +5 -0
  116. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/outputs/result.md +71 -0
  117. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/grading.json +90 -0
  118. package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
  119. package/skills/updating-service-skills/SKILL.md +136 -0
  120. package/skills/updating-service-skills/scripts/drift_detector.py +222 -0
  121. package/skills/using-quality-gates/SKILL.md +254 -0
  122. package/skills/using-service-skills/SKILL.md +108 -0
  123. package/skills/using-service-skills/scripts/cataloger.py +74 -0
  124. package/skills/using-service-skills/scripts/skill_activator.py +152 -0
  125. package/skills/using-service-skills/scripts/test_skill_activator.py +58 -0
  126. package/skills/using-xtrm/SKILL.md +34 -38
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // Claude Code PreCompact hook — save in_progress beads issues before context is compacted.
3
- // Writes issue IDs to .beads/.last_active so beads-compact-restore.mjs can reinstate them.
3
+ // Writes a compact bundle to .beads/.last_active for restore hook.
4
+ // Bundle includes issue IDs and xtrm session state when available.
4
5
  // Exit 0 in all paths (informational only).
5
- //
6
- // Installed by: xtrm install
7
6
 
8
7
  import { execSync } from 'node:child_process';
9
8
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
10
9
  import path from 'node:path';
10
+ import { readSessionState } from './session-state.mjs';
11
11
 
12
12
  let input;
13
13
  try {
@@ -40,7 +40,38 @@ for (const line of output.split('\n')) {
40
40
  if (match) ids.push(match[1]);
41
41
  }
42
42
 
43
- if (ids.length === 0) process.exit(0);
43
+ const sessionState = readSessionState(cwd);
44
+ const bundle = {
45
+ ids,
46
+ sessionState: sessionState ? {
47
+ issueId: sessionState.issueId,
48
+ branch: sessionState.branch,
49
+ worktreePath: sessionState.worktreePath,
50
+ prNumber: sessionState.prNumber,
51
+ prUrl: sessionState.prUrl,
52
+ phase: sessionState.phase,
53
+ conflictFiles: Array.isArray(sessionState.conflictFiles) ? sessionState.conflictFiles : [],
54
+ startedAt: sessionState.startedAt,
55
+ lastChecked: sessionState.lastChecked,
56
+ } : null,
57
+ savedAt: new Date().toISOString(),
58
+ };
59
+
60
+ if (bundle.ids.length === 0 && !bundle.sessionState) process.exit(0);
61
+
62
+ writeFileSync(path.join(beadsDir, '.last_active'), JSON.stringify(bundle, null, 2) + '\n', 'utf8');
63
+
64
+ if (bundle.sessionState?.phase === 'waiting-merge') {
65
+ const pr = bundle.sessionState.prNumber != null ? `#${bundle.sessionState.prNumber}` : '(pending PR)';
66
+ process.stdout.write(
67
+ JSON.stringify({
68
+ hookSpecificOutput: {
69
+ hookEventName: 'PreCompact',
70
+ additionalSystemPrompt:
71
+ `PENDING: xtrm finish waiting for PR ${pr} to merge. Re-run xtrm finish to resume.`,
72
+ },
73
+ }) + '\n',
74
+ );
75
+ }
44
76
 
45
- writeFileSync(path.join(beadsDir, '.last_active'), ids.join('\n') + '\n', 'utf8');
46
77
  process.exit(0);
@@ -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() {
@@ -11,7 +11,7 @@ import { execSync } from 'node:child_process';
11
11
  import { existsSync, unlinkSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
13
  import { readHookInput } from './beads-gate-core.mjs';
14
- import { resolveCwd, isBeadsProject, getSessionClaim } from './beads-gate-utils.mjs';
14
+ import { resolveCwd, isBeadsProject, getSessionClaim, clearSessionClaim } from './beads-gate-utils.mjs';
15
15
  import { memoryPromptMessage } from './beads-gate-messages.mjs';
16
16
 
17
17
  const input = readHookInput();
@@ -20,35 +20,43 @@ if (!input) process.exit(0);
20
20
  const cwd = resolveCwd(input);
21
21
  if (!cwd || !isBeadsProject(cwd)) process.exit(0);
22
22
 
23
+ const sessionId = input.session_id ?? null;
24
+ if (!sessionId) process.exit(0);
25
+
23
26
  // Agent signals evaluation complete by touching this marker, then stops again
24
27
  const marker = join(cwd, '.beads', '.memory-gate-done');
25
28
  if (existsSync(marker)) {
26
29
  try { unlinkSync(marker); } catch { /* ignore */ }
30
+ // Clear the claim and closed-this-session marker
31
+ clearSessionClaim(sessionId, cwd);
32
+ try {
33
+ execSync(`bd kv clear "closed-this-session:${sessionId}"`, {
34
+ cwd,
35
+ stdio: ['pipe', 'pipe', 'pipe'],
36
+ timeout: 5000,
37
+ });
38
+ } catch { /* ignore */ }
27
39
  process.exit(0);
28
40
  }
29
41
 
30
- // Only fire if this session had an active claim that is now closed
31
- const sessionId = input.session_id ?? null;
32
- if (!sessionId) process.exit(0);
33
-
34
- const claimId = getSessionClaim(sessionId, cwd);
35
- if (!claimId) process.exit(0); // no claim this session → no work to persist
36
-
37
- // Check if the claimed issue was closed this session
38
- let claimClosed = false;
42
+ // Check if an issue was closed this session (set by beads-claim-sync on bd close)
43
+ let closedIssueId = null;
39
44
  try {
40
- const out = execSync('bd list --status=closed', {
45
+ closedIssueId = execSync(`bd kv get "closed-this-session:${sessionId}"`, {
41
46
  encoding: 'utf8',
42
47
  cwd,
43
48
  stdio: ['pipe', 'pipe', 'pipe'],
44
- timeout: 8000,
45
- });
46
- claimClosed = out.includes(claimId);
47
- } catch {
49
+ timeout: 5000,
50
+ }).trim();
51
+ } catch (err) {
52
+ if (err.status === 1) {
53
+ // No closed-this-session marker → nothing to prompt about
54
+ process.exit(0);
55
+ }
48
56
  process.exit(0); // fail open
49
57
  }
50
58
 
51
- if (!claimClosed) process.exit(0);
59
+ if (!closedIssueId) process.exit(0);
52
60
 
53
61
  process.stderr.write(memoryPromptMessage());
54
62
  process.exit(2);
@@ -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,117 @@
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 read-only
50
+ 'git status',
51
+ 'git log',
52
+ 'git diff',
53
+ 'git show',
54
+ 'git blame',
55
+ 'git branch',
56
+ 'git fetch',
57
+ 'git remote',
58
+ 'git config',
59
+ 'git pull',
60
+ 'git stash',
61
+ 'git worktree',
62
+ 'git checkout -b',
63
+ 'git switch -c',
64
+ // Tools
65
+ 'gh',
66
+ 'bd',
67
+ 'npx gitnexus',
68
+ // Read-only filesystem
69
+ 'cat',
70
+ 'ls',
71
+ 'head',
72
+ 'tail',
73
+ 'pwd',
74
+ 'which',
75
+ 'type',
76
+ 'env',
77
+ 'printenv',
78
+ 'find',
79
+ 'grep',
80
+ 'rg',
81
+ 'fd',
82
+ 'wc',
83
+ 'sort',
84
+ 'uniq',
85
+ 'cut',
86
+ 'awk',
87
+ 'jq',
88
+ 'yq',
89
+ 'bat',
90
+ 'less',
91
+ 'more',
92
+ 'file',
93
+ 'stat',
94
+ 'du',
95
+ 'tree',
96
+ // Allowed writes (specific paths)
97
+ 'touch .beads/',
98
+ ];
99
+
100
+ export const NATIVE_TEAM_TOOLS = [
101
+ 'Task',
102
+ 'TeamCreate',
103
+ 'TeamDelete',
104
+ 'SendMessage',
105
+ 'TaskCreate',
106
+ 'TaskUpdate',
107
+ 'TaskList',
108
+ 'TaskGet',
109
+ 'TaskOutput',
110
+ 'TaskStop',
111
+ ];
112
+
113
+ export const INTERACTIVE_TOOLS = [
114
+ 'AskUserQuestion',
115
+ 'EnterPlanMode',
116
+ 'EnterWorktree',
117
+ ];
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
  }
@@ -5,7 +5,9 @@
5
5
  // Installed by: xtrm install
6
6
 
7
7
  import { execSync } from 'node:child_process';
8
- import { readFileSync } from 'node:fs';
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { WRITE_TOOLS, SAFE_BASH_PREFIXES } from './guard-rules.mjs';
9
11
 
10
12
  let branch = '';
11
13
  try {
@@ -34,6 +36,7 @@ try {
34
36
 
35
37
  const tool = input.tool_name ?? '';
36
38
  const hookEventName = input.hook_event_name ?? 'PreToolUse';
39
+ const cwd = input.cwd || process.cwd();
37
40
 
38
41
  function deny(reason) {
39
42
  process.stdout.write(JSON.stringify({
@@ -44,18 +47,23 @@ function deny(reason) {
44
47
  process.exit(0);
45
48
  }
46
49
 
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)) {
50
+ // Check for existing worktree/session state
51
+ function getSessionState(cwd) {
52
+ const statePath = join(cwd, '.xtrm-session-state.json');
53
+ if (!existsSync(statePath)) return null;
54
+ try {
55
+ return JSON.parse(readFileSync(statePath, 'utf8'));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ if (WRITE_TOOLS.includes(tool)) {
62
+ const state = getSessionState(cwd);
63
+ if (state?.worktreePath) {
64
+ deny(`⛔ On '${branch}' — worktree already created.\n`
65
+ + ` cd ${state.worktreePath}\n`);
66
+ }
59
67
  deny(`⛔ On '${branch}' — start on a feature branch and claim an issue.\n`
60
68
  + ' git checkout -b feature/<name>\n'
61
69
  + ' bd update <id> --claim\n');
@@ -91,17 +99,9 @@ if (tool === 'Bash') {
91
99
  // Important: do not allow generic checkout/switch forms, which include
92
100
  // mutating variants such as `git checkout -- <path>`.
93
101
  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+/,
102
+ ...SAFE_BASH_PREFIXES.map(prefix => new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)),
100
103
  // Allow post-merge sync to protected branch only (not arbitrary origin refs)
101
104
  ...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
105
  ];
106
106
 
107
107
  if (SAFE_BASH_PATTERNS.some(p => p.test(cmd))) {