xtrm-tools 0.5.45 → 0.5.47

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 (36) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +24 -5
  4. package/cli/dist/index.cjs +9812 -9983
  5. package/cli/dist/index.cjs.map +1 -1
  6. package/cli/package.json +1 -1
  7. package/config/instructions/agents-top.md +2 -4
  8. package/config/instructions/claude-top.md +2 -4
  9. package/config/pi/extensions/beads/index.ts +18 -78
  10. package/config/pi/extensions/custom-footer/index.ts +2 -3
  11. package/config/pi/extensions/xtrm-ui/format.ts +93 -0
  12. package/config/pi/extensions/xtrm-ui/index.ts +1044 -0
  13. package/config/pi/extensions/xtrm-ui/package.json +10 -0
  14. package/config/pi/extensions/xtrm-ui/themes/pidex-dark.json +85 -0
  15. package/config/pi/extensions/xtrm-ui/themes/pidex-light.json +85 -0
  16. package/config/pi/install-schema.json +0 -1
  17. package/hooks/beads-claim-sync.mjs +15 -96
  18. package/hooks/beads-gate-messages.mjs +2 -4
  19. package/hooks/beads-gate-utils.mjs +0 -18
  20. package/hooks/statusline.mjs +5 -3
  21. package/package.json +1 -1
  22. package/plugins/xtrm-tools/.claude-plugin/plugin.json +1 -1
  23. package/plugins/xtrm-tools/hooks/beads-claim-sync.mjs +15 -96
  24. package/plugins/xtrm-tools/hooks/beads-gate-messages.mjs +2 -4
  25. package/plugins/xtrm-tools/hooks/beads-gate-utils.mjs +0 -18
  26. package/plugins/xtrm-tools/hooks/statusline.mjs +5 -3
  27. package/plugins/xtrm-tools/skills/planning/SKILL.md +75 -20
  28. package/plugins/xtrm-tools/skills/using-xtrm/SKILL.md +1 -1
  29. package/plugins/xtrm-tools/skills/xt-debugging/SKILL.md +149 -0
  30. package/plugins/xtrm-tools/skills/xt-end/SKILL.md +28 -0
  31. package/skills/planning/SKILL.md +75 -20
  32. package/skills/using-xtrm/SKILL.md +1 -1
  33. package/skills/xt-debugging/SKILL.md +149 -0
  34. package/skills/xt-end/SKILL.md +28 -0
  35. package/plugins/xtrm-tools/skills/gitnexus-debugging/SKILL.md +0 -85
  36. package/skills/gitnexus-debugging/SKILL.md +0 -85
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "xtrm-ui",
3
+ "version": "1.0.0",
4
+ "description": "XTRM UI: pi-dex theme/header + custom XTRM footer",
5
+ "license": "MIT",
6
+ "pi": {
7
+ "extensions": ["./index.ts"],
8
+ "themes": ["./themes"]
9
+ }
10
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "pidex-dark",
4
+ "vars": {
5
+ "accentBlue": "#b8d3ff",
6
+ "accentCyan": "#b8d3ff",
7
+ "accentTeal": "#c7d2e0",
8
+ "successGreen": "#9fd59f",
9
+ "errorRed": "#ff9a9a",
10
+ "warningAmber": "#d2b48c",
11
+ "surface": "#000000",
12
+ "surfaceAlt": "#111111",
13
+ "surfaceMuted": "#1a1a1a",
14
+ "surfaceUser": "#1f1f1f",
15
+ "surfaceCustom": "#161616",
16
+ "gray": "#a7a7a7",
17
+ "dimGray": "#8a8a8a",
18
+ "borderGray": "#666666",
19
+ "borderBright": "#9a9a9a"
20
+ },
21
+ "colors": {
22
+ "accent": "accentBlue",
23
+ "border": "borderGray",
24
+ "borderAccent": "borderBright",
25
+ "borderMuted": "borderGray",
26
+ "success": "successGreen",
27
+ "error": "errorRed",
28
+ "warning": "warningAmber",
29
+ "muted": "gray",
30
+ "dim": "dimGray",
31
+ "text": "",
32
+ "thinkingText": "gray",
33
+
34
+ "selectedBg": "surfaceMuted",
35
+ "userMessageBg": "surfaceUser",
36
+ "userMessageText": "",
37
+ "customMessageBg": "surfaceCustom",
38
+ "customMessageText": "",
39
+ "customMessageLabel": "accentBlue",
40
+ "toolPendingBg": "surfaceAlt",
41
+ "toolSuccessBg": "surfaceAlt",
42
+ "toolErrorBg": "surfaceAlt",
43
+ "toolTitle": "",
44
+ "toolOutput": "gray",
45
+
46
+ "mdHeading": "warningAmber",
47
+ "mdLink": "accentBlue",
48
+ "mdLinkUrl": "dimGray",
49
+ "mdCode": "accentCyan",
50
+ "mdCodeBlock": "gray",
51
+ "mdCodeBlockBorder": "borderGray",
52
+ "mdQuote": "gray",
53
+ "mdQuoteBorder": "borderGray",
54
+ "mdHr": "borderGray",
55
+ "mdListBullet": "accentTeal",
56
+
57
+ "toolDiffAdded": "successGreen",
58
+ "toolDiffRemoved": "errorRed",
59
+ "toolDiffContext": "gray",
60
+
61
+ "syntaxComment": "#6b7280",
62
+ "syntaxKeyword": "#7aa2f7",
63
+ "syntaxFunction": "#c0caf5",
64
+ "syntaxVariable": "#a9b1d6",
65
+ "syntaxString": "#9ece6a",
66
+ "syntaxNumber": "#ff9e64",
67
+ "syntaxType": "#73daca",
68
+ "syntaxOperator": "#c0caf5",
69
+ "syntaxPunctuation": "#8f9bb3",
70
+
71
+ "thinkingOff": "borderGray",
72
+ "thinkingMinimal": "#707070",
73
+ "thinkingLow": "#7a7a7a",
74
+ "thinkingMedium": "#858585",
75
+ "thinkingHigh": "#8f8f8f",
76
+ "thinkingXhigh": "#999999",
77
+
78
+ "bashMode": "accentTeal"
79
+ },
80
+ "export": {
81
+ "pageBg": "#12161d",
82
+ "cardBg": "#171b22",
83
+ "infoBg": "#28230f"
84
+ }
85
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "pidex-light",
4
+ "vars": {
5
+ "accentBlue": "#44546a",
6
+ "accentCyan": "#5f6b7a",
7
+ "accentTeal": "#5f6b7a",
8
+ "successGreen": "#587558",
9
+ "errorRed": "#a85f5f",
10
+ "warningAmber": "#8e7348",
11
+ "surface": "#ffffff",
12
+ "surfaceAlt": "#f1f1f1",
13
+ "surfaceMuted": "#e6e6e6",
14
+ "surfaceUser": "#f0f0f0",
15
+ "surfaceCustom": "#f5f5f5",
16
+ "gray": "#5f5f5f",
17
+ "dimGray": "#7a7a7a",
18
+ "borderGray": "#b9b9b9",
19
+ "borderBright": "#9f9f9f"
20
+ },
21
+ "colors": {
22
+ "accent": "accentBlue",
23
+ "border": "borderGray",
24
+ "borderAccent": "borderBright",
25
+ "borderMuted": "borderGray",
26
+ "success": "successGreen",
27
+ "error": "errorRed",
28
+ "warning": "warningAmber",
29
+ "muted": "gray",
30
+ "dim": "dimGray",
31
+ "text": "",
32
+ "thinkingText": "gray",
33
+
34
+ "selectedBg": "surfaceMuted",
35
+ "userMessageBg": "surfaceUser",
36
+ "userMessageText": "",
37
+ "customMessageBg": "surfaceCustom",
38
+ "customMessageText": "",
39
+ "customMessageLabel": "accentBlue",
40
+ "toolPendingBg": "surfaceAlt",
41
+ "toolSuccessBg": "surfaceAlt",
42
+ "toolErrorBg": "surfaceAlt",
43
+ "toolTitle": "",
44
+ "toolOutput": "gray",
45
+
46
+ "mdHeading": "warningAmber",
47
+ "mdLink": "accentBlue",
48
+ "mdLinkUrl": "dimGray",
49
+ "mdCode": "accentCyan",
50
+ "mdCodeBlock": "gray",
51
+ "mdCodeBlockBorder": "borderGray",
52
+ "mdQuote": "gray",
53
+ "mdQuoteBorder": "borderGray",
54
+ "mdHr": "borderGray",
55
+ "mdListBullet": "accentTeal",
56
+
57
+ "toolDiffAdded": "successGreen",
58
+ "toolDiffRemoved": "errorRed",
59
+ "toolDiffContext": "gray",
60
+
61
+ "syntaxComment": "#6b7280",
62
+ "syntaxKeyword": "#3569c8",
63
+ "syntaxFunction": "#3b4351",
64
+ "syntaxVariable": "#364152",
65
+ "syntaxString": "#3f7d3d",
66
+ "syntaxNumber": "#b06500",
67
+ "syntaxType": "#2c7a7b",
68
+ "syntaxOperator": "#3b4351",
69
+ "syntaxPunctuation": "#55606f",
70
+
71
+ "thinkingOff": "borderGray",
72
+ "thinkingMinimal": "#9d9d9d",
73
+ "thinkingLow": "#979797",
74
+ "thinkingMedium": "#919191",
75
+ "thinkingHigh": "#8b8b8b",
76
+ "thinkingXhigh": "#858585",
77
+
78
+ "bashMode": "accentTeal"
79
+ },
80
+ "export": {
81
+ "pageBg": "#f5f7fa",
82
+ "cardBg": "#ffffff",
83
+ "infoBg": "#fff5e6"
84
+ }
85
+ }
@@ -34,7 +34,6 @@
34
34
  }
35
35
  ],
36
36
  "packages": [
37
- "npm:pi-dex",
38
37
  "npm:pi-gitnexus",
39
38
  "npm:pi-serena-tools",
40
39
  "npm:lsp-pi",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // beads-claim-sync — PostToolUse hook
3
3
  // bd update --claim → set kv claim
4
- // bd close → auto-commit staged changes, set closed-this-session kv for memory gate
4
+ // bd close → set closed-this-session kv for memory gate
5
5
 
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { readFileSync, existsSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
@@ -33,6 +33,14 @@ function resolveMainRoot(cwd) {
33
33
  return cwd;
34
34
  }
35
35
 
36
+ // Returns a per-session claim filename: 'statusline-claim' for the main session,
37
+ // 'statusline-claim-<worktreeName>' for worktree sessions.
38
+ // Prevents cross-session contamination when multiple worktrees run simultaneously.
39
+ function resolveClaimFileName(cwd) {
40
+ const m = cwd.match(/\/\.xtrm\/worktrees\/([^/]+)/);
41
+ return m ? `statusline-claim-${m[1]}` : 'statusline-claim';
42
+ }
43
+
36
44
  function isShellTool(toolName) {
37
45
  return toolName === 'Bash' || toolName === 'bash' || toolName === 'execute_shell_command';
38
46
  }
@@ -50,73 +58,7 @@ function commandSucceeded(payload) {
50
58
  return true;
51
59
  }
52
60
 
53
- function runGit(args, cwd, timeout = 8000) {
54
- return spawnSync('git', args, {
55
- cwd,
56
- stdio: ['pipe', 'pipe', 'pipe'],
57
- encoding: 'utf8',
58
- timeout,
59
- });
60
- }
61
-
62
- function runBd(args, cwd, timeout = 5000) {
63
- return spawnSync('bd', args, {
64
- cwd,
65
- stdio: ['pipe', 'pipe', 'pipe'],
66
- encoding: 'utf8',
67
- timeout,
68
- });
69
- }
70
-
71
- function hasGitChanges(cwd) {
72
- const result = runGit(['status', '--porcelain'], cwd);
73
- if (result.status !== 0) return false;
74
- return result.stdout.trim().length > 0;
75
- }
76
-
77
- function getCloseReason(cwd, issueId, command) {
78
- // 1. Parse --reason "..." from the command itself (fastest, no extra call)
79
- const reasonMatch = command.match(/--reason[=\s]+["']([^"']+)["']/);
80
- if (reasonMatch) return reasonMatch[1].trim();
81
-
82
- // 2. Fall back to bd show <id> --json
83
- const show = runBd(['show', issueId, '--json'], cwd);
84
- if (show.status === 0 && show.stdout) {
85
- try {
86
- const parsed = JSON.parse(show.stdout);
87
- const reason = parsed?.[0]?.close_reason;
88
- if (typeof reason === 'string' && reason.trim().length > 0) return reason.trim();
89
- } catch { /* fall through */ }
90
- }
91
-
92
- return `Close ${issueId}`;
93
- }
94
-
95
- function stageUntracked(cwd) {
96
- const result = runGit(['ls-files', '--others', '--exclude-standard'], cwd);
97
- if (result.status !== 0) return;
98
- const untracked = result.stdout.trim().split('\n').filter(Boolean);
99
- if (untracked.length === 0) return;
100
- runGit(['add', '--', ...untracked], cwd);
101
- }
102
-
103
- function autoCommit(cwd, issueId, command) {
104
- if (!hasGitChanges(cwd)) {
105
- return { ok: true, message: 'No changes detected — auto-commit skipped.' };
106
- }
107
-
108
- stageUntracked(cwd);
109
-
110
- const reason = getCloseReason(cwd, issueId, command);
111
- const commitMessage = `${reason} (${issueId})`;
112
- const result = runGit(['commit', '--no-verify', '-am', commitMessage], cwd, 15000);
113
- if (result.status !== 0) {
114
- const err = (result.stderr || result.stdout || '').trim();
115
- return { ok: false, message: `Auto-commit failed: ${err || 'unknown error'}` };
116
- }
117
61
 
118
- return { ok: true, message: `Auto-committed: \`${commitMessage}\`` };
119
- }
120
62
 
121
63
 
122
64
  function main() {
@@ -147,11 +89,11 @@ function main() {
147
89
  process.exit(0);
148
90
  }
149
91
 
150
- // Write claim state for statusline — always at main repo root so all sessions share it.
92
+ // Write claim state for statusline — per-worktree file under main root.
151
93
  try {
152
94
  const xtrmDir = join(resolveMainRoot(cwd), '.xtrm');
153
95
  mkdirSync(xtrmDir, { recursive: true });
154
- writeFileSync(join(xtrmDir, 'statusline-claim'), issueId);
96
+ writeFileSync(join(xtrmDir, resolveClaimFileName(cwd)), issueId);
155
97
  } catch { /* non-fatal */ }
156
98
 
157
99
  logEvent({
@@ -172,16 +114,13 @@ function main() {
172
114
  }
173
115
  }
174
116
 
175
- // On bd close: auto-commit staged changes, then mark closed-this-session for memory gate
117
+ // On bd close: mark closed-this-session for memory gate
176
118
  if (/\bbd\s+close\b/.test(command) && commandSucceeded(input)) {
177
119
  const match = command.match(/\bbd\s+close\s+(\S+)/);
178
120
  const closedIssueId = match?.[1];
179
121
 
180
- // Auto-commit before marking the gate (no-op if clean)
181
- const commit = closedIssueId ? autoCommit(cwd, closedIssueId, command) : null;
182
-
183
- // Clear claim state for statusline — use main root so worktree and main sessions agree.
184
- try { unlinkSync(join(resolveMainRoot(cwd), '.xtrm', 'statusline-claim')); } catch { /* ok if missing */ }
122
+ // Clear claim state for statusline — per-worktree file under main root.
123
+ try { unlinkSync(join(resolveMainRoot(cwd), '.xtrm', resolveClaimFileName(cwd))); } catch { /* ok if missing */ }
185
124
 
186
125
  // Mark this issue as closed this session (memory gate reads this)
187
126
  if (closedIssueId) {
@@ -190,10 +129,7 @@ function main() {
190
129
  stdio: ['pipe', 'pipe', 'pipe'],
191
130
  timeout: 5000,
192
131
  });
193
- }
194
132
 
195
- // Log bd lifecycle events
196
- if (closedIssueId) {
197
133
  logEvent({
198
134
  cwd,
199
135
  runtime: 'claude',
@@ -204,26 +140,9 @@ function main() {
204
140
  issueId: closedIssueId,
205
141
  });
206
142
  }
207
- if (commit) {
208
- logEvent({
209
- cwd,
210
- runtime: 'claude',
211
- sessionId,
212
- layer: 'bd',
213
- kind: 'bd.committed',
214
- outcome: commit.ok ? 'allow' : 'block',
215
- issueId: closedIssueId ?? null,
216
- data: { msg: commit.message },
217
- extra: { ok: commit.ok },
218
- });
219
- }
220
-
221
- const commitLine = commit
222
- ? `\n${commit.ok ? '✅' : '⚠️'} **Session Flow**: ${commit.message}`
223
- : '';
224
143
 
225
144
  process.stdout.write(JSON.stringify({
226
- additionalContext: `\n🔓 **Beads**: Issue closed.${commitLine}\nEvaluate insights, then acknowledge:\n \`bd remember "<insight>"\` (or note "nothing")\n \`touch .beads/.memory-gate-done\``,
145
+ additionalContext: `\n🔓 **Beads**: Issue closed. Evaluate insights, then acknowledge:\n \`bd remember "<insight>"\` (or note "nothing to persist")`,
227
146
  }));
228
147
  process.stdout.write('\n');
229
148
  process.exit(0);
@@ -12,7 +12,7 @@ export const SESSION_CLOSE_PROTOCOL =
12
12
  ' xt end\n';
13
13
 
14
14
  export const COMMIT_NEXT_STEPS =
15
- ' bd close <id> --reason="..." ← closes issue + auto-commits\n' +
15
+ ' bd close <id> --reason="..." ← closes issue\n' +
16
16
  ' xt end ← push, PR, merge, worktree cleanup\n';
17
17
 
18
18
  // ── Edit gate messages ───────────────────────────────────────────
@@ -58,9 +58,7 @@ export function stopBlockMessage(summary, claimed) {
58
58
 
59
59
  export function memoryPromptMessage(claimId, sessionId) {
60
60
  const claimLine = claimId ? `claim \`${claimId}\` was closed.\n` : '';
61
- const ackCmd = sessionId
62
- ? `bd kv set "memory-gate-done:${sessionId}"`
63
- : 'touch .beads/.memory-gate-done';
61
+ const ackCmd = `bd kv set "memory-gate-done:${sessionId}"`;
64
62
  return (
65
63
  `\u25cf Memory gate: ${claimLine}` +
66
64
  'For each candidate insight, check ALL 4:\n' +
@@ -133,24 +133,6 @@ export function getClosedThisSession(sessionId, cwd) {
133
133
  }
134
134
  }
135
135
 
136
- /**
137
- * Return true if the memory gate is pending acknowledgment for this session.
138
- * Pending = closed-this-session kv is set AND .beads/.memory-gate-done marker is absent.
139
- */
140
- export function isMemoryGatePending(sessionId, cwd) {
141
- if (existsSync(join(cwd, '.beads', '.memory-gate-done'))) return false;
142
- const closed = getClosedThisSession(sessionId, cwd);
143
- return !!closed; // null (unavailable) is falsy → fail open
144
- }
145
-
146
- /**
147
- * Return true if a Bash command is a memory-gate acknowledgment command.
148
- * These commands are allowed even while the memory gate is pending.
149
- */
150
- export function isMemoryAckCommand(command) {
151
- return /\bbd\s+remember\b/.test(command) || /\btouch\s+\.beads\/\.memory-gate-done\b/.test(command);
152
- }
153
-
154
136
  /**
155
137
  * If cwd is inside a .xtrm/worktrees/<name> directory, return the worktree root path.
156
138
  * Returns null if not in a worktree.
@@ -4,7 +4,7 @@
4
4
  // Line 2: ◐ italic(claim title) OR ○ bold(N) open — no background
5
5
  //
6
6
  // Colors: bold/dim/italic only — no explicit fg/bg, adapts to dark & light themes.
7
- // State: .xtrm/statusline-claim (written by beads-claim-sync.mjs)
7
+ // State: .xtrm/statusline-claim[-<worktreeName>] (written by beads-claim-sync.mjs)
8
8
  // Cache: /tmp per cwd, 5s TTL
9
9
 
10
10
  import { execSync } from 'node:child_process';
@@ -125,8 +125,10 @@ if (!data) {
125
125
  let claimTitle = null;
126
126
  let openCount = 0;
127
127
  if (existsSync(join(cwd, '.beads')) || existsSync(join(mainRoot, '.beads'))) {
128
- // Use mainRoot so all sessions (main + worktrees) share one canonical claim file.
129
- const claimFile = join(mainRoot, '.xtrm', 'statusline-claim');
128
+ // Per-worktree claim file prevents cross-session contamination.
129
+ const worktreeMatch = cwd.match(/\/\.xtrm\/worktrees\/([^/]+)/);
130
+ const claimFileName = worktreeMatch ? `statusline-claim-${worktreeMatch[1]}` : 'statusline-claim';
131
+ const claimFile = join(mainRoot, '.xtrm', claimFileName);
130
132
  claimId = existsSync(claimFile) ? (readFileSync(claimFile, 'utf8').trim() || null) : null;
131
133
  if (claimId) {
132
134
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "0.5.45",
3
+ "version": "0.5.47",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "0.5.45",
3
+ "version": "0.5.47",
4
4
  "description": "xtrm-tools: dual-runtime workflow enforcement (Claude Code + Pi) — hooks, extensions, skills, and MCP servers",
5
5
  "author": {
6
6
  "name": "jaggers"
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // beads-claim-sync — PostToolUse hook
3
3
  // bd update --claim → set kv claim
4
- // bd close → auto-commit staged changes, set closed-this-session kv for memory gate
4
+ // bd close → set closed-this-session kv for memory gate
5
5
 
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { readFileSync, existsSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
@@ -33,6 +33,14 @@ function resolveMainRoot(cwd) {
33
33
  return cwd;
34
34
  }
35
35
 
36
+ // Returns a per-session claim filename: 'statusline-claim' for the main session,
37
+ // 'statusline-claim-<worktreeName>' for worktree sessions.
38
+ // Prevents cross-session contamination when multiple worktrees run simultaneously.
39
+ function resolveClaimFileName(cwd) {
40
+ const m = cwd.match(/\/\.xtrm\/worktrees\/([^/]+)/);
41
+ return m ? `statusline-claim-${m[1]}` : 'statusline-claim';
42
+ }
43
+
36
44
  function isShellTool(toolName) {
37
45
  return toolName === 'Bash' || toolName === 'bash' || toolName === 'execute_shell_command';
38
46
  }
@@ -50,73 +58,7 @@ function commandSucceeded(payload) {
50
58
  return true;
51
59
  }
52
60
 
53
- function runGit(args, cwd, timeout = 8000) {
54
- return spawnSync('git', args, {
55
- cwd,
56
- stdio: ['pipe', 'pipe', 'pipe'],
57
- encoding: 'utf8',
58
- timeout,
59
- });
60
- }
61
-
62
- function runBd(args, cwd, timeout = 5000) {
63
- return spawnSync('bd', args, {
64
- cwd,
65
- stdio: ['pipe', 'pipe', 'pipe'],
66
- encoding: 'utf8',
67
- timeout,
68
- });
69
- }
70
-
71
- function hasGitChanges(cwd) {
72
- const result = runGit(['status', '--porcelain'], cwd);
73
- if (result.status !== 0) return false;
74
- return result.stdout.trim().length > 0;
75
- }
76
-
77
- function getCloseReason(cwd, issueId, command) {
78
- // 1. Parse --reason "..." from the command itself (fastest, no extra call)
79
- const reasonMatch = command.match(/--reason[=\s]+["']([^"']+)["']/);
80
- if (reasonMatch) return reasonMatch[1].trim();
81
-
82
- // 2. Fall back to bd show <id> --json
83
- const show = runBd(['show', issueId, '--json'], cwd);
84
- if (show.status === 0 && show.stdout) {
85
- try {
86
- const parsed = JSON.parse(show.stdout);
87
- const reason = parsed?.[0]?.close_reason;
88
- if (typeof reason === 'string' && reason.trim().length > 0) return reason.trim();
89
- } catch { /* fall through */ }
90
- }
91
-
92
- return `Close ${issueId}`;
93
- }
94
-
95
- function stageUntracked(cwd) {
96
- const result = runGit(['ls-files', '--others', '--exclude-standard'], cwd);
97
- if (result.status !== 0) return;
98
- const untracked = result.stdout.trim().split('\n').filter(Boolean);
99
- if (untracked.length === 0) return;
100
- runGit(['add', '--', ...untracked], cwd);
101
- }
102
-
103
- function autoCommit(cwd, issueId, command) {
104
- if (!hasGitChanges(cwd)) {
105
- return { ok: true, message: 'No changes detected — auto-commit skipped.' };
106
- }
107
-
108
- stageUntracked(cwd);
109
-
110
- const reason = getCloseReason(cwd, issueId, command);
111
- const commitMessage = `${reason} (${issueId})`;
112
- const result = runGit(['commit', '--no-verify', '-am', commitMessage], cwd, 15000);
113
- if (result.status !== 0) {
114
- const err = (result.stderr || result.stdout || '').trim();
115
- return { ok: false, message: `Auto-commit failed: ${err || 'unknown error'}` };
116
- }
117
61
 
118
- return { ok: true, message: `Auto-committed: \`${commitMessage}\`` };
119
- }
120
62
 
121
63
 
122
64
  function main() {
@@ -147,11 +89,11 @@ function main() {
147
89
  process.exit(0);
148
90
  }
149
91
 
150
- // Write claim state for statusline — always at main repo root so all sessions share it.
92
+ // Write claim state for statusline — per-worktree file under main root.
151
93
  try {
152
94
  const xtrmDir = join(resolveMainRoot(cwd), '.xtrm');
153
95
  mkdirSync(xtrmDir, { recursive: true });
154
- writeFileSync(join(xtrmDir, 'statusline-claim'), issueId);
96
+ writeFileSync(join(xtrmDir, resolveClaimFileName(cwd)), issueId);
155
97
  } catch { /* non-fatal */ }
156
98
 
157
99
  logEvent({
@@ -172,16 +114,13 @@ function main() {
172
114
  }
173
115
  }
174
116
 
175
- // On bd close: auto-commit staged changes, then mark closed-this-session for memory gate
117
+ // On bd close: mark closed-this-session for memory gate
176
118
  if (/\bbd\s+close\b/.test(command) && commandSucceeded(input)) {
177
119
  const match = command.match(/\bbd\s+close\s+(\S+)/);
178
120
  const closedIssueId = match?.[1];
179
121
 
180
- // Auto-commit before marking the gate (no-op if clean)
181
- const commit = closedIssueId ? autoCommit(cwd, closedIssueId, command) : null;
182
-
183
- // Clear claim state for statusline — use main root so worktree and main sessions agree.
184
- try { unlinkSync(join(resolveMainRoot(cwd), '.xtrm', 'statusline-claim')); } catch { /* ok if missing */ }
122
+ // Clear claim state for statusline — per-worktree file under main root.
123
+ try { unlinkSync(join(resolveMainRoot(cwd), '.xtrm', resolveClaimFileName(cwd))); } catch { /* ok if missing */ }
185
124
 
186
125
  // Mark this issue as closed this session (memory gate reads this)
187
126
  if (closedIssueId) {
@@ -190,10 +129,7 @@ function main() {
190
129
  stdio: ['pipe', 'pipe', 'pipe'],
191
130
  timeout: 5000,
192
131
  });
193
- }
194
132
 
195
- // Log bd lifecycle events
196
- if (closedIssueId) {
197
133
  logEvent({
198
134
  cwd,
199
135
  runtime: 'claude',
@@ -204,26 +140,9 @@ function main() {
204
140
  issueId: closedIssueId,
205
141
  });
206
142
  }
207
- if (commit) {
208
- logEvent({
209
- cwd,
210
- runtime: 'claude',
211
- sessionId,
212
- layer: 'bd',
213
- kind: 'bd.committed',
214
- outcome: commit.ok ? 'allow' : 'block',
215
- issueId: closedIssueId ?? null,
216
- data: { msg: commit.message },
217
- extra: { ok: commit.ok },
218
- });
219
- }
220
-
221
- const commitLine = commit
222
- ? `\n${commit.ok ? '✅' : '⚠️'} **Session Flow**: ${commit.message}`
223
- : '';
224
143
 
225
144
  process.stdout.write(JSON.stringify({
226
- additionalContext: `\n🔓 **Beads**: Issue closed.${commitLine}\nEvaluate insights, then acknowledge:\n \`bd remember "<insight>"\` (or note "nothing")\n \`touch .beads/.memory-gate-done\``,
145
+ additionalContext: `\n🔓 **Beads**: Issue closed. Evaluate insights, then acknowledge:\n \`bd remember "<insight>"\` (or note "nothing to persist")`,
227
146
  }));
228
147
  process.stdout.write('\n');
229
148
  process.exit(0);
@@ -12,7 +12,7 @@ export const SESSION_CLOSE_PROTOCOL =
12
12
  ' xt end\n';
13
13
 
14
14
  export const COMMIT_NEXT_STEPS =
15
- ' bd close <id> --reason="..." ← closes issue + auto-commits\n' +
15
+ ' bd close <id> --reason="..." ← closes issue\n' +
16
16
  ' xt end ← push, PR, merge, worktree cleanup\n';
17
17
 
18
18
  // ── Edit gate messages ───────────────────────────────────────────
@@ -58,9 +58,7 @@ export function stopBlockMessage(summary, claimed) {
58
58
 
59
59
  export function memoryPromptMessage(claimId, sessionId) {
60
60
  const claimLine = claimId ? `claim \`${claimId}\` was closed.\n` : '';
61
- const ackCmd = sessionId
62
- ? `bd kv set "memory-gate-done:${sessionId}"`
63
- : 'touch .beads/.memory-gate-done';
61
+ const ackCmd = `bd kv set "memory-gate-done:${sessionId}"`;
64
62
  return (
65
63
  `\u25cf Memory gate: ${claimLine}` +
66
64
  'For each candidate insight, check ALL 4:\n' +
@@ -133,24 +133,6 @@ export function getClosedThisSession(sessionId, cwd) {
133
133
  }
134
134
  }
135
135
 
136
- /**
137
- * Return true if the memory gate is pending acknowledgment for this session.
138
- * Pending = closed-this-session kv is set AND .beads/.memory-gate-done marker is absent.
139
- */
140
- export function isMemoryGatePending(sessionId, cwd) {
141
- if (existsSync(join(cwd, '.beads', '.memory-gate-done'))) return false;
142
- const closed = getClosedThisSession(sessionId, cwd);
143
- return !!closed; // null (unavailable) is falsy → fail open
144
- }
145
-
146
- /**
147
- * Return true if a Bash command is a memory-gate acknowledgment command.
148
- * These commands are allowed even while the memory gate is pending.
149
- */
150
- export function isMemoryAckCommand(command) {
151
- return /\bbd\s+remember\b/.test(command) || /\btouch\s+\.beads\/\.memory-gate-done\b/.test(command);
152
- }
153
-
154
136
  /**
155
137
  * If cwd is inside a .xtrm/worktrees/<name> directory, return the worktree root path.
156
138
  * Returns null if not in a worktree.