xtrm-tools 2.4.1 → 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.
- package/README.md +15 -6
- package/cli/dist/index.cjs +738 -239
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +10 -0
- package/config/pi/extensions/core/adapter.ts +2 -14
- package/config/pi/extensions/core/guard-rules.ts +70 -0
- package/config/pi/extensions/core/session-state.ts +59 -0
- package/config/pi/extensions/main-guard.ts +10 -14
- package/config/pi/extensions/plan-mode/README.md +65 -0
- package/config/pi/extensions/plan-mode/index.ts +340 -0
- package/config/pi/extensions/plan-mode/utils.ts +168 -0
- package/config/pi/extensions/service-skills.ts +51 -7
- package/config/pi/extensions/session-flow.ts +117 -0
- package/hooks/beads-claim-sync.mjs +123 -2
- package/hooks/beads-compact-restore.mjs +41 -9
- package/hooks/beads-compact-save.mjs +36 -5
- package/hooks/beads-gate-messages.mjs +27 -1
- package/hooks/beads-stop-gate.mjs +58 -8
- package/hooks/guard-rules.mjs +86 -0
- package/hooks/hooks.json +28 -18
- package/hooks/main-guard.mjs +3 -21
- package/hooks/quality-check.cjs +1286 -0
- package/hooks/quality-check.py +345 -0
- package/hooks/session-state.mjs +138 -0
- package/package.json +2 -1
- package/project-skills/quality-gates/.claude/settings.json +1 -24
- package/skills/creating-service-skills/SKILL.md +433 -0
- package/skills/creating-service-skills/references/script_quality_standards.md +425 -0
- package/skills/creating-service-skills/references/service_skill_system_guide.md +278 -0
- package/skills/creating-service-skills/scripts/bootstrap.py +326 -0
- package/skills/creating-service-skills/scripts/deep_dive.py +304 -0
- package/skills/creating-service-skills/scripts/scaffolder.py +482 -0
- package/skills/scoping-service-skills/SKILL.md +231 -0
- package/skills/scoping-service-skills/scripts/scope.py +74 -0
- package/skills/sync-docs/SKILL.md +235 -0
- package/skills/sync-docs/evals/evals.json +89 -0
- package/skills/sync-docs/references/doc-structure.md +104 -0
- package/skills/sync-docs/references/schema.md +103 -0
- package/skills/sync-docs/scripts/context_gatherer.py +246 -0
- package/skills/sync-docs/scripts/doc_structure_analyzer.py +495 -0
- package/skills/sync-docs/scripts/validate_doc.py +365 -0
- package/skills/sync-docs-workspace/iteration-1/benchmark.json +293 -0
- package/skills/sync-docs-workspace/iteration-1/benchmark.md +13 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/outputs/result.md +210 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/outputs/result.md +101 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/outputs/result.md +198 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/outputs/result.md +94 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/outputs/result.md +237 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/outputs/result.md +134 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/benchmark.json +297 -0
- package/skills/sync-docs-workspace/iteration-2/benchmark.md +13 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/outputs/result.md +137 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/grading.json +92 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/outputs/result.md +134 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/grading.json +86 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/outputs/result.md +193 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/grading.json +72 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/outputs/result.md +211 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/grading.json +91 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/outputs/result.md +182 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/outputs/result.md +222 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/grading.json +88 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/benchmark.json +298 -0
- package/skills/sync-docs-workspace/iteration-3/benchmark.md +13 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/outputs/result.md +125 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/grading.json +97 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/outputs/result.md +144 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/grading.json +78 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/outputs/result.md +104 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/grading.json +91 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/outputs/result.md +79 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/grading.json +82 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase1_context.json +302 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase2_drift.txt +33 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase3_analysis.json +114 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase4_fix.txt +118 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase5_validate.txt +38 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/result.md +158 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/outputs/result.md +71 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/grading.json +90 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
- package/skills/updating-service-skills/SKILL.md +136 -0
- package/skills/updating-service-skills/scripts/drift_detector.py +222 -0
- package/skills/using-quality-gates/SKILL.md +254 -0
- package/skills/using-service-skills/SKILL.md +108 -0
- package/skills/using-service-skills/scripts/cataloger.py +74 -0
- package/skills/using-service-skills/scripts/skill_activator.py +152 -0
- package/skills/using-service-skills/scripts/test_skill_activator.py +58 -0
- package/skills/using-xtrm/SKILL.md +34 -38
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for plan mode.
|
|
3
|
+
* Extracted for testability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Destructive commands blocked in plan mode
|
|
7
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
8
|
+
/\brm\b/i,
|
|
9
|
+
/\brmdir\b/i,
|
|
10
|
+
/\bmv\b/i,
|
|
11
|
+
/\bcp\b/i,
|
|
12
|
+
/\bmkdir\b/i,
|
|
13
|
+
/\btouch\b/i,
|
|
14
|
+
/\bchmod\b/i,
|
|
15
|
+
/\bchown\b/i,
|
|
16
|
+
/\bchgrp\b/i,
|
|
17
|
+
/\bln\b/i,
|
|
18
|
+
/\btee\b/i,
|
|
19
|
+
/\btruncate\b/i,
|
|
20
|
+
/\bdd\b/i,
|
|
21
|
+
/\bshred\b/i,
|
|
22
|
+
/(^|[^<])>(?!>)/,
|
|
23
|
+
/>>/,
|
|
24
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
25
|
+
/\byarn\s+(add|remove|install|publish)/i,
|
|
26
|
+
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
27
|
+
/\bpip\s+(install|uninstall)/i,
|
|
28
|
+
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
29
|
+
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
30
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
31
|
+
/\bsudo\b/i,
|
|
32
|
+
/\bsu\b/i,
|
|
33
|
+
/\bkill\b/i,
|
|
34
|
+
/\bpkill\b/i,
|
|
35
|
+
/\bkillall\b/i,
|
|
36
|
+
/\breboot\b/i,
|
|
37
|
+
/\bshutdown\b/i,
|
|
38
|
+
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
39
|
+
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
40
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Safe read-only commands allowed in plan mode
|
|
44
|
+
const SAFE_PATTERNS = [
|
|
45
|
+
/^\s*cat\b/,
|
|
46
|
+
/^\s*head\b/,
|
|
47
|
+
/^\s*tail\b/,
|
|
48
|
+
/^\s*less\b/,
|
|
49
|
+
/^\s*more\b/,
|
|
50
|
+
/^\s*grep\b/,
|
|
51
|
+
/^\s*find\b/,
|
|
52
|
+
/^\s*ls\b/,
|
|
53
|
+
/^\s*pwd\b/,
|
|
54
|
+
/^\s*echo\b/,
|
|
55
|
+
/^\s*printf\b/,
|
|
56
|
+
/^\s*wc\b/,
|
|
57
|
+
/^\s*sort\b/,
|
|
58
|
+
/^\s*uniq\b/,
|
|
59
|
+
/^\s*diff\b/,
|
|
60
|
+
/^\s*file\b/,
|
|
61
|
+
/^\s*stat\b/,
|
|
62
|
+
/^\s*du\b/,
|
|
63
|
+
/^\s*df\b/,
|
|
64
|
+
/^\s*tree\b/,
|
|
65
|
+
/^\s*which\b/,
|
|
66
|
+
/^\s*whereis\b/,
|
|
67
|
+
/^\s*type\b/,
|
|
68
|
+
/^\s*env\b/,
|
|
69
|
+
/^\s*printenv\b/,
|
|
70
|
+
/^\s*uname\b/,
|
|
71
|
+
/^\s*whoami\b/,
|
|
72
|
+
/^\s*id\b/,
|
|
73
|
+
/^\s*date\b/,
|
|
74
|
+
/^\s*cal\b/,
|
|
75
|
+
/^\s*uptime\b/,
|
|
76
|
+
/^\s*ps\b/,
|
|
77
|
+
/^\s*top\b/,
|
|
78
|
+
/^\s*htop\b/,
|
|
79
|
+
/^\s*free\b/,
|
|
80
|
+
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
81
|
+
/^\s*git\s+ls-/i,
|
|
82
|
+
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
83
|
+
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
84
|
+
/^\s*node\s+--version/i,
|
|
85
|
+
/^\s*python\s+--version/i,
|
|
86
|
+
/^\s*curl\s/i,
|
|
87
|
+
/^\s*wget\s+-O\s*-/i,
|
|
88
|
+
/^\s*jq\b/,
|
|
89
|
+
/^\s*sed\s+-n/i,
|
|
90
|
+
/^\s*awk\b/,
|
|
91
|
+
/^\s*rg\b/,
|
|
92
|
+
/^\s*fd\b/,
|
|
93
|
+
/^\s*bat\b/,
|
|
94
|
+
/^\s*exa\b/,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export function isSafeCommand(command: string): boolean {
|
|
98
|
+
const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command));
|
|
99
|
+
const isSafe = SAFE_PATTERNS.some((p) => p.test(command));
|
|
100
|
+
return !isDestructive && isSafe;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface TodoItem {
|
|
104
|
+
step: number;
|
|
105
|
+
text: string;
|
|
106
|
+
completed: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function cleanStepText(text: string): string {
|
|
110
|
+
let cleaned = text
|
|
111
|
+
.replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1") // Remove bold/italic
|
|
112
|
+
.replace(/`([^`]+)`/g, "$1") // Remove code
|
|
113
|
+
.replace(
|
|
114
|
+
/^(Use|Run|Execute|Create|Write|Read|Check|Verify|Update|Modify|Add|Remove|Delete|Install)\s+(the\s+)?/i,
|
|
115
|
+
"",
|
|
116
|
+
)
|
|
117
|
+
.replace(/\s+/g, " ")
|
|
118
|
+
.trim();
|
|
119
|
+
|
|
120
|
+
if (cleaned.length > 0) {
|
|
121
|
+
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
122
|
+
}
|
|
123
|
+
if (cleaned.length > 50) {
|
|
124
|
+
cleaned = `${cleaned.slice(0, 47)}...`;
|
|
125
|
+
}
|
|
126
|
+
return cleaned;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function extractTodoItems(message: string): TodoItem[] {
|
|
130
|
+
const items: TodoItem[] = [];
|
|
131
|
+
const headerMatch = message.match(/\*{0,2}Plan:\*{0,2}\s*\n/i);
|
|
132
|
+
if (!headerMatch) return items;
|
|
133
|
+
|
|
134
|
+
const planSection = message.slice(message.indexOf(headerMatch[0]) + headerMatch[0].length);
|
|
135
|
+
const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm;
|
|
136
|
+
|
|
137
|
+
for (const match of planSection.matchAll(numberedPattern)) {
|
|
138
|
+
const text = match[2]
|
|
139
|
+
.trim()
|
|
140
|
+
.replace(/\*{1,2}$/, "")
|
|
141
|
+
.trim();
|
|
142
|
+
if (text.length > 5 && !text.startsWith("`") && !text.startsWith("/") && !text.startsWith("-")) {
|
|
143
|
+
const cleaned = cleanStepText(text);
|
|
144
|
+
if (cleaned.length > 3) {
|
|
145
|
+
items.push({ step: items.length + 1, text: cleaned, completed: false });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return items;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function extractDoneSteps(message: string): number[] {
|
|
153
|
+
const steps: number[] = [];
|
|
154
|
+
for (const match of message.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
155
|
+
const step = Number(match[1]);
|
|
156
|
+
if (Number.isFinite(step)) steps.push(step);
|
|
157
|
+
}
|
|
158
|
+
return steps;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function markCompletedSteps(text: string, items: TodoItem[]): number {
|
|
162
|
+
const doneSteps = extractDoneSteps(text);
|
|
163
|
+
for (const step of doneSteps) {
|
|
164
|
+
const item = items.find((t) => t.step === step);
|
|
165
|
+
if (item) item.completed = true;
|
|
166
|
+
}
|
|
167
|
+
return doneSteps.length;
|
|
168
|
+
}
|
|
@@ -3,18 +3,56 @@ import { SubprocessRunner } from "./core/lib";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
|
|
6
|
+
const SERVICE_REGISTRY_FILES = [
|
|
7
|
+
"service-registry.json",
|
|
8
|
+
path.join(".claude", "skills", "service-registry.json"),
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const GLOBAL_SKILL_ROOTS = [
|
|
12
|
+
path.join(process.env.HOME || "", ".agents", "skills"),
|
|
13
|
+
path.join(process.env.HOME || "", ".claude", "skills"),
|
|
14
|
+
];
|
|
15
|
+
|
|
6
16
|
export default function (pi: ExtensionAPI) {
|
|
7
17
|
const getCwd = (ctx: any) => ctx.cwd || process.cwd();
|
|
8
18
|
|
|
19
|
+
const resolveRegistryPath = (cwd: string): string | null => {
|
|
20
|
+
for (const rel of SERVICE_REGISTRY_FILES) {
|
|
21
|
+
const candidate = path.join(cwd, rel);
|
|
22
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const resolveSkillScript = (cwd: string, skillName: string, scriptName: string): string | null => {
|
|
28
|
+
const localPath = path.join(cwd, ".claude", "skills", skillName, "scripts", scriptName);
|
|
29
|
+
if (fs.existsSync(localPath)) return localPath;
|
|
30
|
+
|
|
31
|
+
for (const root of GLOBAL_SKILL_ROOTS) {
|
|
32
|
+
if (!root) continue;
|
|
33
|
+
const candidate = path.join(root, skillName, "scripts", scriptName);
|
|
34
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
|
|
9
40
|
// 1. Catalog Injection
|
|
10
41
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
11
42
|
const cwd = getCwd(ctx);
|
|
12
|
-
const
|
|
13
|
-
if (!
|
|
43
|
+
const registryPath = resolveRegistryPath(cwd);
|
|
44
|
+
if (!registryPath) return undefined;
|
|
45
|
+
|
|
46
|
+
const catalogerPath = resolveSkillScript(cwd, "using-service-skills", "cataloger.py");
|
|
47
|
+
if (!catalogerPath) return undefined;
|
|
14
48
|
|
|
15
49
|
const result = await SubprocessRunner.run("python3", [catalogerPath], {
|
|
16
50
|
cwd,
|
|
17
|
-
env: {
|
|
51
|
+
env: {
|
|
52
|
+
...process.env,
|
|
53
|
+
CLAUDE_PROJECT_DIR: cwd,
|
|
54
|
+
SERVICE_REGISTRY_PATH: registryPath,
|
|
55
|
+
},
|
|
18
56
|
});
|
|
19
57
|
|
|
20
58
|
if (result.code === 0 && result.stdout.trim()) {
|
|
@@ -23,7 +61,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
23
61
|
return undefined;
|
|
24
62
|
});
|
|
25
63
|
|
|
26
|
-
|
|
27
64
|
const toClaudeToolName = (toolName: string): string => {
|
|
28
65
|
if (toolName === "bash") return "Bash";
|
|
29
66
|
if (toolName === "read_file") return "Read";
|
|
@@ -37,8 +74,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
37
74
|
// 2. Drift Detection (skill activation is before_agent_start only — not per-tool)
|
|
38
75
|
pi.on("tool_result", async (event, ctx) => {
|
|
39
76
|
const cwd = getCwd(ctx);
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
77
|
+
const registryPath = resolveRegistryPath(cwd);
|
|
78
|
+
if (!registryPath) return undefined;
|
|
79
|
+
|
|
80
|
+
const driftDetectorPath = resolveSkillScript(cwd, "updating-service-skills", "drift_detector.py");
|
|
81
|
+
if (!driftDetectorPath) return undefined;
|
|
42
82
|
|
|
43
83
|
const hookInput = JSON.stringify({
|
|
44
84
|
tool_name: toClaudeToolName(event.toolName),
|
|
@@ -49,7 +89,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
49
89
|
const result = await SubprocessRunner.run("python3", [driftDetectorPath], {
|
|
50
90
|
cwd,
|
|
51
91
|
input: hookInput,
|
|
52
|
-
env: {
|
|
92
|
+
env: {
|
|
93
|
+
...process.env,
|
|
94
|
+
CLAUDE_PROJECT_DIR: cwd,
|
|
95
|
+
SERVICE_REGISTRY_PATH: registryPath,
|
|
96
|
+
},
|
|
53
97
|
timeoutMs: 10000,
|
|
54
98
|
});
|
|
55
99
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { isBashToolResult } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { SubprocessRunner, EventAdapter } from "./core/lib";
|
|
6
|
+
import { readSessionState } from "./core/session-state";
|
|
7
|
+
|
|
8
|
+
function isClaimCommand(command: string): { isClaim: boolean; issueId: string | null } {
|
|
9
|
+
if (!/\bbd\s+update\b/.test(command) || !/--claim\b/.test(command)) {
|
|
10
|
+
return { isClaim: false, issueId: null };
|
|
11
|
+
}
|
|
12
|
+
const match = command.match(/\bbd\s+update\s+(\S+)/);
|
|
13
|
+
return { isClaim: true, issueId: match?.[1] ?? null };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function statePathFrom(startCwd: string): string {
|
|
17
|
+
let current = path.resolve(startCwd || process.cwd());
|
|
18
|
+
for (;;) {
|
|
19
|
+
const candidate = path.join(current, ".xtrm-session-state.json");
|
|
20
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
21
|
+
const parent = path.dirname(current);
|
|
22
|
+
if (parent === current) return path.join(startCwd, ".xtrm-session-state.json");
|
|
23
|
+
current = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function ensureWorktreeSessionState(cwd: string, issueId: string): Promise<{ ok: boolean; message?: string }> {
|
|
28
|
+
const repoRootResult = await SubprocessRunner.run("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
29
|
+
if (repoRootResult.code !== 0 || !repoRootResult.stdout) return { ok: false, message: "not a git repo" };
|
|
30
|
+
const repoRoot = repoRootResult.stdout.trim();
|
|
31
|
+
|
|
32
|
+
const gitDir = await SubprocessRunner.run("git", ["rev-parse", "--git-dir"], { cwd });
|
|
33
|
+
const commonDir = await SubprocessRunner.run("git", ["rev-parse", "--git-common-dir"], { cwd });
|
|
34
|
+
if (gitDir.code === 0 && commonDir.code === 0 && gitDir.stdout.trim() !== commonDir.stdout.trim()) {
|
|
35
|
+
return { ok: false, message: "already in linked worktree" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const overstoryDir = path.join(repoRoot, ".overstory");
|
|
39
|
+
const worktreesBase = fs.existsSync(overstoryDir)
|
|
40
|
+
? path.join(overstoryDir, "worktrees")
|
|
41
|
+
: path.join(repoRoot, ".worktrees");
|
|
42
|
+
fs.mkdirSync(worktreesBase, { recursive: true });
|
|
43
|
+
|
|
44
|
+
const branch = `feature/${issueId}`;
|
|
45
|
+
const worktreePath = path.join(worktreesBase, issueId);
|
|
46
|
+
if (!fs.existsSync(worktreePath)) {
|
|
47
|
+
const branchExists = (await SubprocessRunner.run("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: repoRoot })).code === 0;
|
|
48
|
+
const addArgs = branchExists
|
|
49
|
+
? ["worktree", "add", worktreePath, branch]
|
|
50
|
+
: ["worktree", "add", worktreePath, "-b", branch];
|
|
51
|
+
const add = await SubprocessRunner.run("git", addArgs, { cwd: repoRoot, timeoutMs: 20000 });
|
|
52
|
+
if (add.code !== 0) {
|
|
53
|
+
return { ok: false, message: add.stderr || add.stdout || "worktree creation failed" };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const statePath = statePathFrom(repoRoot);
|
|
58
|
+
const payload = {
|
|
59
|
+
issueId,
|
|
60
|
+
branch,
|
|
61
|
+
worktreePath,
|
|
62
|
+
prNumber: null,
|
|
63
|
+
prUrl: null,
|
|
64
|
+
phase: "claimed",
|
|
65
|
+
conflictFiles: [],
|
|
66
|
+
startedAt: new Date().toISOString(),
|
|
67
|
+
lastChecked: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
70
|
+
return { ok: true, message: `Worktree created: ${worktreePath} Branch: ${branch}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function (pi: ExtensionAPI) {
|
|
74
|
+
const getCwd = (ctx: any) => ctx.cwd || process.cwd();
|
|
75
|
+
|
|
76
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
77
|
+
if (!isBashToolResult(event)) return undefined;
|
|
78
|
+
const cwd = getCwd(ctx);
|
|
79
|
+
if (!EventAdapter.isBeadsProject(cwd)) return undefined;
|
|
80
|
+
|
|
81
|
+
const command = event.input.command || "";
|
|
82
|
+
const { isClaim, issueId } = isClaimCommand(command);
|
|
83
|
+
if (!isClaim || !issueId) return undefined;
|
|
84
|
+
|
|
85
|
+
const ensured = await ensureWorktreeSessionState(cwd, issueId);
|
|
86
|
+
if (ensured.ok) {
|
|
87
|
+
const text = `\n\n🧭 Session Flow: ${ensured.message}`;
|
|
88
|
+
return { content: [...event.content, { type: "text", text }] };
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
94
|
+
const cwd = getCwd(ctx);
|
|
95
|
+
if (!EventAdapter.isBeadsProject(cwd)) return undefined;
|
|
96
|
+
const state = readSessionState(cwd);
|
|
97
|
+
if (!state) return undefined;
|
|
98
|
+
|
|
99
|
+
if (state.phase === "waiting-merge" || state.phase === "pending-cleanup") {
|
|
100
|
+
const pr = state.prNumber != null ? `#${state.prNumber}` : "(pending PR)";
|
|
101
|
+
const url = state.prUrl ? ` ${state.prUrl}` : "";
|
|
102
|
+
pi.sendUserMessage(`🚫 PR ${pr}${url} not yet merged. Run: xtrm finish`);
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (state.phase === "conflicting") {
|
|
107
|
+
const files = state.conflictFiles?.length ? state.conflictFiles.join(", ") : "unknown files";
|
|
108
|
+
pi.sendUserMessage(`🚫 Merge conflicts in: ${files}. Resolve, push, then: xtrm finish`);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (state.phase === "claimed" || state.phase === "phase1-done") {
|
|
113
|
+
pi.sendUserMessage(`⚠ Session has an active worktree at ${state.worktreePath}. Consider running: xtrm finish`);
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// beads-claim-sync — PostToolUse hook
|
|
3
3
|
// Auto-sets kv claim on bd update --claim; auto-clears on bd close.
|
|
4
|
+
// Also bootstraps worktree-first session state for xtrm finish workflow.
|
|
4
5
|
|
|
5
6
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
7
8
|
import { join } from 'node:path';
|
|
9
|
+
import { writeSessionState } from './session-state.mjs';
|
|
8
10
|
|
|
9
11
|
function readInput() {
|
|
10
12
|
try {
|
|
@@ -35,6 +37,112 @@ function commandSucceeded(payload) {
|
|
|
35
37
|
return true;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function runGit(args, cwd, timeout = 8000) {
|
|
41
|
+
return spawnSync('git', args, {
|
|
42
|
+
cwd,
|
|
43
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
timeout,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getRepoRoot(cwd) {
|
|
50
|
+
const result = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
51
|
+
if (result.status !== 0) return null;
|
|
52
|
+
return result.stdout.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function inLinkedWorktree(cwd) {
|
|
56
|
+
const gitDir = runGit(['rev-parse', '--git-dir'], cwd);
|
|
57
|
+
const gitCommonDir = runGit(['rev-parse', '--git-common-dir'], cwd);
|
|
58
|
+
if (gitDir.status !== 0 || gitCommonDir.status !== 0) return false;
|
|
59
|
+
return gitDir.stdout.trim() !== gitCommonDir.stdout.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ensureWorktreeForClaim(cwd, issueId) {
|
|
63
|
+
const repoRoot = getRepoRoot(cwd);
|
|
64
|
+
if (!repoRoot) return { created: false, reason: 'not-git' };
|
|
65
|
+
|
|
66
|
+
if (inLinkedWorktree(cwd)) {
|
|
67
|
+
return { created: false, reason: 'already-worktree', repoRoot };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const overstoryDir = join(repoRoot, '.overstory');
|
|
71
|
+
const worktreesBase = existsSync(overstoryDir)
|
|
72
|
+
? join(overstoryDir, 'worktrees')
|
|
73
|
+
: join(repoRoot, '.worktrees');
|
|
74
|
+
|
|
75
|
+
mkdirSync(worktreesBase, { recursive: true });
|
|
76
|
+
|
|
77
|
+
const branch = `feature/${issueId}`;
|
|
78
|
+
const worktreePath = join(worktreesBase, issueId);
|
|
79
|
+
|
|
80
|
+
if (existsSync(worktreePath)) {
|
|
81
|
+
// Already created previously — rewrite state file for continuity.
|
|
82
|
+
try {
|
|
83
|
+
const stateFile = writeSessionState({
|
|
84
|
+
issueId,
|
|
85
|
+
branch,
|
|
86
|
+
worktreePath,
|
|
87
|
+
prNumber: null,
|
|
88
|
+
prUrl: null,
|
|
89
|
+
phase: 'claimed',
|
|
90
|
+
conflictFiles: [],
|
|
91
|
+
}, { cwd: repoRoot });
|
|
92
|
+
return { created: false, reason: 'exists', repoRoot, branch, worktreePath, stateFile };
|
|
93
|
+
} catch {
|
|
94
|
+
return { created: false, reason: 'exists', repoRoot, branch, worktreePath };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const branchExists = runGit(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], repoRoot).status === 0;
|
|
99
|
+
const addArgs = branchExists
|
|
100
|
+
? ['worktree', 'add', worktreePath, branch]
|
|
101
|
+
: ['worktree', 'add', worktreePath, '-b', branch];
|
|
102
|
+
|
|
103
|
+
const addResult = runGit(addArgs, repoRoot, 20000);
|
|
104
|
+
if (addResult.status !== 0) {
|
|
105
|
+
return {
|
|
106
|
+
created: false,
|
|
107
|
+
reason: 'create-failed',
|
|
108
|
+
repoRoot,
|
|
109
|
+
branch,
|
|
110
|
+
worktreePath,
|
|
111
|
+
error: (addResult.stderr || addResult.stdout || '').trim(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const stateFile = writeSessionState({
|
|
117
|
+
issueId,
|
|
118
|
+
branch,
|
|
119
|
+
worktreePath,
|
|
120
|
+
prNumber: null,
|
|
121
|
+
prUrl: null,
|
|
122
|
+
phase: 'claimed',
|
|
123
|
+
conflictFiles: [],
|
|
124
|
+
}, { cwd: repoRoot });
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
created: true,
|
|
128
|
+
reason: 'created',
|
|
129
|
+
repoRoot,
|
|
130
|
+
branch,
|
|
131
|
+
worktreePath,
|
|
132
|
+
stateFile,
|
|
133
|
+
};
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return {
|
|
136
|
+
created: true,
|
|
137
|
+
reason: 'created-state-write-failed',
|
|
138
|
+
repoRoot,
|
|
139
|
+
branch,
|
|
140
|
+
worktreePath,
|
|
141
|
+
error: String(err?.message || err),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
38
146
|
function main() {
|
|
39
147
|
const input = readInput();
|
|
40
148
|
if (!input || input.hook_event_name !== 'PostToolUse') process.exit(0);
|
|
@@ -68,8 +176,21 @@ function main() {
|
|
|
68
176
|
process.exit(0);
|
|
69
177
|
}
|
|
70
178
|
|
|
179
|
+
const wt = ensureWorktreeForClaim(cwd, issueId);
|
|
180
|
+
const details = [];
|
|
181
|
+
if (wt.created) {
|
|
182
|
+
details.push(`🧭 **Session Flow**: Worktree created: \`${wt.worktreePath}\` Branch: \`${wt.branch}\``);
|
|
183
|
+
} else if (wt.reason === 'exists') {
|
|
184
|
+
details.push(`🧭 **Session Flow**: Worktree already exists: \`${wt.worktreePath}\` Branch: \`${wt.branch}\``);
|
|
185
|
+
} else if (wt.reason === 'already-worktree') {
|
|
186
|
+
details.push('🧭 **Session Flow**: Already in a linked worktree — skipping nested worktree creation.');
|
|
187
|
+
} else if (wt.reason === 'create-failed') {
|
|
188
|
+
const err = wt.error ? `\nWarning: ${wt.error}` : '';
|
|
189
|
+
details.push(`⚠️ **Session Flow**: Worktree creation failed for \`${issueId}\`. Continuing without blocking claim.${err}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
71
192
|
process.stdout.write(JSON.stringify({
|
|
72
|
-
additionalContext: `\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}
|
|
193
|
+
additionalContext: `\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`.${details.length ? `\n${details.join('\n')}` : ''}`,
|
|
73
194
|
}));
|
|
74
195
|
process.stdout.write('\n');
|
|
75
196
|
process.exit(0);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Claude Code SessionStart hook — restore in_progress beads issues after context compaction.
|
|
3
3
|
// Reads .beads/.last_active (written by beads-compact-save.mjs), reinstates statuses,
|
|
4
|
-
// deletes the marker, and injects a brief agent context message.
|
|
4
|
+
// restores session state file, deletes the marker, and injects a brief agent context message.
|
|
5
5
|
// Exit 0 in all paths (informational only).
|
|
6
|
-
//
|
|
7
|
-
// Installed by: xtrm install
|
|
8
6
|
|
|
9
7
|
import { execSync } from 'node:child_process';
|
|
10
8
|
import { readFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
11
9
|
import path from 'node:path';
|
|
10
|
+
import { writeSessionState } from './session-state.mjs';
|
|
12
11
|
|
|
13
12
|
let input;
|
|
14
13
|
try {
|
|
@@ -22,13 +21,26 @@ const lastActivePath = path.join(cwd, '.beads', '.last_active');
|
|
|
22
21
|
|
|
23
22
|
if (!existsSync(lastActivePath)) process.exit(0);
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
let ids = [];
|
|
25
|
+
let sessionState = null;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const raw = readFileSync(lastActivePath, 'utf8').trim();
|
|
29
|
+
if (raw.startsWith('{')) {
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
ids = Array.isArray(parsed.ids) ? parsed.ids.filter(Boolean) : [];
|
|
32
|
+
sessionState = parsed.sessionState ?? null;
|
|
33
|
+
} else {
|
|
34
|
+
// Backward compatibility: legacy newline format
|
|
35
|
+
ids = raw.split('\n').filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// If file is malformed, just delete and continue fail-open.
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
// Clean up regardless of whether restore succeeds
|
|
28
42
|
unlinkSync(lastActivePath);
|
|
29
43
|
|
|
30
|
-
if (ids.length === 0) process.exit(0);
|
|
31
|
-
|
|
32
44
|
let restored = 0;
|
|
33
45
|
for (const id of ids) {
|
|
34
46
|
try {
|
|
@@ -44,13 +56,33 @@ for (const id of ids) {
|
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
|
|
59
|
+
let restoredSession = false;
|
|
60
|
+
if (sessionState && typeof sessionState === 'object') {
|
|
61
|
+
try {
|
|
62
|
+
writeSessionState(sessionState, { cwd });
|
|
63
|
+
restoredSession = true;
|
|
64
|
+
} catch {
|
|
65
|
+
// fail open
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (restored > 0 || restoredSession) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
if (restored > 0) {
|
|
72
|
+
lines.push(`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (restoredSession && (sessionState.phase === 'waiting-merge' || sessionState.phase === 'pending-cleanup')) {
|
|
76
|
+
const pr = sessionState.prNumber != null ? `#${sessionState.prNumber}` : '(pending PR)';
|
|
77
|
+
const prUrl = sessionState.prUrl ? ` ${sessionState.prUrl}` : '';
|
|
78
|
+
lines.push(`RESUME: Run xtrm finish — PR ${pr}${prUrl} waiting for merge. Worktree: ${sessionState.worktreePath}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
48
81
|
process.stdout.write(
|
|
49
82
|
JSON.stringify({
|
|
50
83
|
hookSpecificOutput: {
|
|
51
84
|
hookEventName: 'SessionStart',
|
|
52
|
-
additionalSystemPrompt:
|
|
53
|
-
`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction. Check \`bd list\` for details.`,
|
|
85
|
+
additionalSystemPrompt: `${lines.join(' ')} Check \`bd list\` for details.`,
|
|
54
86
|
},
|
|
55
87
|
}) + '\n',
|
|
56
88
|
);
|
|
@@ -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
|
|
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
|
-
|
|
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);
|