xtrm-cli 0.5.0

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 (93) hide show
  1. package/.gemini/settings.json +39 -0
  2. package/dist/index.cjs +57378 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2 -0
  5. package/extensions/beads.ts +109 -0
  6. package/extensions/core/adapter.ts +45 -0
  7. package/extensions/core/lib.ts +3 -0
  8. package/extensions/core/logger.ts +45 -0
  9. package/extensions/core/runner.ts +71 -0
  10. package/extensions/custom-footer.ts +160 -0
  11. package/extensions/main-guard-post-push.ts +44 -0
  12. package/extensions/main-guard.ts +126 -0
  13. package/extensions/minimal-mode.ts +201 -0
  14. package/extensions/quality-gates.ts +67 -0
  15. package/extensions/service-skills.ts +150 -0
  16. package/extensions/xtrm-loader.ts +89 -0
  17. package/hooks/gitnexus-impact-reminder.py +13 -0
  18. package/lib/atomic-config.js +236 -0
  19. package/lib/config-adapter.js +231 -0
  20. package/lib/config-injector.js +80 -0
  21. package/lib/context.js +73 -0
  22. package/lib/diff.js +142 -0
  23. package/lib/env-manager.js +160 -0
  24. package/lib/sync-mcp-cli.js +345 -0
  25. package/lib/sync.js +227 -0
  26. package/package.json +47 -0
  27. package/src/adapters/base.ts +29 -0
  28. package/src/adapters/claude.ts +38 -0
  29. package/src/adapters/registry.ts +21 -0
  30. package/src/commands/claude.ts +122 -0
  31. package/src/commands/clean.ts +371 -0
  32. package/src/commands/end.ts +239 -0
  33. package/src/commands/finish.ts +25 -0
  34. package/src/commands/help.ts +180 -0
  35. package/src/commands/init.ts +959 -0
  36. package/src/commands/install-pi.ts +276 -0
  37. package/src/commands/install-service-skills.ts +281 -0
  38. package/src/commands/install.ts +427 -0
  39. package/src/commands/pi-install.ts +119 -0
  40. package/src/commands/pi.ts +128 -0
  41. package/src/commands/reset.ts +12 -0
  42. package/src/commands/status.ts +170 -0
  43. package/src/commands/worktree.ts +193 -0
  44. package/src/core/context.ts +141 -0
  45. package/src/core/diff.ts +174 -0
  46. package/src/core/interactive-plan.ts +165 -0
  47. package/src/core/manifest.ts +26 -0
  48. package/src/core/preflight.ts +142 -0
  49. package/src/core/rollback.ts +32 -0
  50. package/src/core/session-state.ts +139 -0
  51. package/src/core/sync-executor.ts +427 -0
  52. package/src/core/xtrm-finish.ts +267 -0
  53. package/src/index.ts +87 -0
  54. package/src/tests/policy-parity.test.ts +204 -0
  55. package/src/tests/session-flow-parity.test.ts +118 -0
  56. package/src/tests/session-state.test.ts +124 -0
  57. package/src/tests/xtrm-finish.test.ts +148 -0
  58. package/src/types/config.ts +51 -0
  59. package/src/types/models.ts +52 -0
  60. package/src/utils/atomic-config.ts +467 -0
  61. package/src/utils/banner.ts +194 -0
  62. package/src/utils/config-adapter.ts +90 -0
  63. package/src/utils/config-injector.ts +81 -0
  64. package/src/utils/env-manager.ts +193 -0
  65. package/src/utils/hash.ts +42 -0
  66. package/src/utils/repo-root.ts +39 -0
  67. package/src/utils/sync-mcp-cli.ts +395 -0
  68. package/src/utils/theme.ts +37 -0
  69. package/src/utils/worktree-session.ts +93 -0
  70. package/test/atomic-config-prune.test.ts +101 -0
  71. package/test/atomic-config.test.ts +138 -0
  72. package/test/clean.test.ts +172 -0
  73. package/test/config-schema.test.ts +52 -0
  74. package/test/context.test.ts +33 -0
  75. package/test/end-worktree.test.ts +168 -0
  76. package/test/extensions/beads.test.ts +166 -0
  77. package/test/extensions/extension-harness.ts +85 -0
  78. package/test/extensions/main-guard.test.ts +77 -0
  79. package/test/extensions/minimal-mode.test.ts +107 -0
  80. package/test/extensions/quality-gates.test.ts +79 -0
  81. package/test/extensions/service-skills.test.ts +84 -0
  82. package/test/extensions/xtrm-loader.test.ts +53 -0
  83. package/test/hooks/quality-check-hooks.test.ts +45 -0
  84. package/test/hooks.test.ts +1075 -0
  85. package/test/install-pi.test.ts +185 -0
  86. package/test/install-project.test.ts +378 -0
  87. package/test/install-service-skills.test.ts +131 -0
  88. package/test/install-surface.test.ts +72 -0
  89. package/test/runtime-subcommands.test.ts +121 -0
  90. package/test/session-launcher.test.ts +139 -0
  91. package/tsconfig.json +22 -0
  92. package/tsup.config.ts +17 -0
  93. package/vitest.config.ts +10 -0
@@ -0,0 +1,201 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ createBashTool,
4
+ createEditTool,
5
+ createFindTool,
6
+ createGrepTool,
7
+ createLsTool,
8
+ createReadTool,
9
+ createWriteTool,
10
+ } from "@mariozechner/pi-coding-agent";
11
+ import { Text } from "@mariozechner/pi-tui";
12
+
13
+ function getTextContent(result: any): string {
14
+ if (!result?.content || !Array.isArray(result.content)) return "";
15
+ return result.content
16
+ .filter((c: any) => c?.type === "text" && typeof c.text === "string")
17
+ .map((c: any) => c.text)
18
+ .join("\n")
19
+ .trim();
20
+ }
21
+
22
+ function oneLine(s: string): string {
23
+ return (s || "").replace(/\s+/g, " ").trim();
24
+ }
25
+
26
+ function summarize(result: any): { text: string; isError: boolean } {
27
+ const raw = getTextContent(result);
28
+ if (!raw) return { text: "done", isError: false };
29
+ const line = oneLine(raw.split("\n").find((l) => l.trim()) || "");
30
+ const lower = line.toLowerCase();
31
+ const isError = lower.includes("error") || lower.includes("failed") || lower.includes("exception");
32
+ return { text: line.slice(0, 140) || "done", isError };
33
+ }
34
+
35
+ const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
36
+ function createBuiltInTools(cwd: string) {
37
+ return {
38
+ read: createReadTool(cwd),
39
+ bash: createBashTool(cwd),
40
+ edit: createEditTool(cwd),
41
+ write: createWriteTool(cwd),
42
+ find: createFindTool(cwd),
43
+ grep: createGrepTool(cwd),
44
+ ls: createLsTool(cwd),
45
+ };
46
+ }
47
+ function getBuiltInTools(cwd: string) {
48
+ let tools = toolCache.get(cwd);
49
+ if (!tools) {
50
+ tools = createBuiltInTools(cwd);
51
+ toolCache.set(cwd, tools);
52
+ }
53
+ return tools;
54
+ }
55
+
56
+ export default function (pi: ExtensionAPI) {
57
+ let minimalEnabled = true;
58
+ let thinkingStatusEnabled = true;
59
+ let spinnerTimer: NodeJS.Timeout | null = null;
60
+ let spinnerIndex = 0;
61
+ const frames = ["thinking ", "thinking. ", "thinking.. ", "thinking..."];
62
+
63
+ const clearSpinner = (ctx: any) => {
64
+ if (spinnerTimer) {
65
+ clearInterval(spinnerTimer);
66
+ spinnerTimer = null;
67
+ }
68
+ if (ctx?.hasUI) {
69
+ ctx.ui.setStatus("thinking", undefined);
70
+ ctx.ui.setHeader(undefined);
71
+ }
72
+ };
73
+
74
+ const mountThinkingHeader = (ctx: any) => {
75
+ if (!ctx?.hasUI) return;
76
+ ctx.ui.setHeader((_tui: any, theme: any) => ({
77
+ invalidate() {},
78
+ render(width: number): string[] {
79
+ const text = frames[spinnerIndex];
80
+ return [oneLine(theme.fg("accent", text)).slice(0, width)];
81
+ },
82
+ }));
83
+ };
84
+
85
+ const startSpinner = (ctx: any) => {
86
+ if (!thinkingStatusEnabled || !ctx?.hasUI) return;
87
+ clearSpinner(ctx);
88
+ spinnerIndex = 0;
89
+ ctx.ui.setStatus("thinking", frames[spinnerIndex]);
90
+ mountThinkingHeader(ctx);
91
+ spinnerTimer = setInterval(() => {
92
+ spinnerIndex = (spinnerIndex + 1) % frames.length;
93
+ ctx.ui.setStatus("thinking", frames[spinnerIndex]);
94
+ mountThinkingHeader(ctx);
95
+ }, 220);
96
+ };
97
+
98
+ const renderCollapsedResult = (result: any, theme: any) => {
99
+ const s = summarize(result);
100
+ const icon = s.isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
101
+ const text = s.isError ? theme.fg("error", s.text) : theme.fg("muted", s.text);
102
+ if (minimalEnabled) return new Text(` ${icon} ${text}`, 0, 0);
103
+ return new Text(theme.fg("muted", ` → ${s.text}`), 0, 0);
104
+ };
105
+
106
+ const renderExpandedResult = (result: any, theme: any) => {
107
+ const text = getTextContent(result);
108
+ if (!text) return new Text("", 0, 0);
109
+ const output = text.split("\n").map((line) => theme.fg("toolOutput", line)).join("\n");
110
+ return new Text(`\n${output}`, 0, 0);
111
+ };
112
+
113
+ pi.registerTool({
114
+ name: "bash",
115
+ label: "bash",
116
+ description: getBuiltInTools(process.cwd()).bash.description,
117
+ parameters: getBuiltInTools(process.cwd()).bash.parameters,
118
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
119
+ return getBuiltInTools(ctx.cwd).bash.execute(toolCallId, params, signal, onUpdate);
120
+ },
121
+ renderCall(args, theme) {
122
+ const cmd = oneLine(args.command || "");
123
+ return new Text(`${theme.fg("toolTitle", theme.bold("bash"))}(${theme.fg("accent", cmd || "...")})`, 0, 0);
124
+ },
125
+ renderResult(result, { expanded }, theme) {
126
+ return expanded ? renderExpandedResult(result, theme) : renderCollapsedResult(result, theme);
127
+ },
128
+ });
129
+
130
+ for (const name of ["read", "write", "edit", "find", "grep", "ls"] as const) {
131
+ pi.registerTool({
132
+ name,
133
+ label: name,
134
+ description: (getBuiltInTools(process.cwd()) as any)[name].description,
135
+ parameters: (getBuiltInTools(process.cwd()) as any)[name].parameters,
136
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
137
+ return (getBuiltInTools(ctx.cwd) as any)[name].execute(toolCallId, params, signal, onUpdate);
138
+ },
139
+ renderCall(args, theme) {
140
+ const suffix = oneLine(args.path || args.pattern || "");
141
+ return new Text(`${theme.fg("toolTitle", theme.bold(name))}${suffix ? `(${theme.fg("accent", suffix)})` : ""}`, 0, 0);
142
+ },
143
+ renderResult(result, { expanded }, theme) {
144
+ return expanded ? renderExpandedResult(result, theme) : renderCollapsedResult(result, theme);
145
+ },
146
+ });
147
+ }
148
+
149
+ pi.registerCommand("minimal-on", {
150
+ description: "Enable minimal collapsed tool output",
151
+ handler: async (_args, ctx) => {
152
+ minimalEnabled = true;
153
+ ctx.ui.notify("Minimal mode enabled", "info");
154
+ },
155
+ });
156
+
157
+ pi.registerCommand("minimal-off", {
158
+ description: "Disable minimal collapsed tool output",
159
+ handler: async (_args, ctx) => {
160
+ minimalEnabled = false;
161
+ ctx.ui.notify("Minimal mode disabled", "info");
162
+ },
163
+ });
164
+
165
+ pi.registerCommand("minimal-toggle", {
166
+ description: "Toggle minimal collapsed tool output",
167
+ handler: async (_args, ctx) => {
168
+ minimalEnabled = !minimalEnabled;
169
+ ctx.ui.notify(`Minimal mode ${minimalEnabled ? "enabled" : "disabled"}`, "info");
170
+ },
171
+ });
172
+
173
+ pi.registerCommand("thinking-status-toggle", {
174
+ description: "Toggle flashing thinking status indicator",
175
+ handler: async (_args, ctx) => {
176
+ thinkingStatusEnabled = !thinkingStatusEnabled;
177
+ if (!thinkingStatusEnabled) clearSpinner(ctx);
178
+ ctx.ui.notify(`Thinking status ${thinkingStatusEnabled ? "enabled" : "disabled"}`, "info");
179
+ },
180
+ });
181
+
182
+ pi.on("turn_start", async (_event, ctx) => {
183
+ startSpinner(ctx);
184
+ return undefined;
185
+ });
186
+
187
+ pi.on("turn_end", async (_event, ctx) => {
188
+ clearSpinner(ctx);
189
+ return undefined;
190
+ });
191
+
192
+ pi.on("agent_end", async (_event, ctx) => {
193
+ clearSpinner(ctx);
194
+ return undefined;
195
+ });
196
+
197
+ pi.on("session_shutdown", async (_event, ctx) => {
198
+ clearSpinner(ctx);
199
+ return undefined;
200
+ });
201
+ }
@@ -0,0 +1,67 @@
1
+ import type { ExtensionAPI, ToolResultEvent } from "@mariozechner/pi-coding-agent";
2
+ import { SubprocessRunner, EventAdapter, Logger } from "./core/lib";
3
+ import * as path from "node:path";
4
+ import * as fs from "node:fs";
5
+
6
+ const logger = new Logger({ namespace: "quality-gates" });
7
+
8
+ export default function (pi: ExtensionAPI) {
9
+ pi.on("tool_result", async (event, ctx) => {
10
+ if (!EventAdapter.isMutatingFileTool(event)) return undefined;
11
+
12
+ const cwd = ctx.cwd || process.cwd();
13
+ const filePath = EventAdapter.extractPathFromToolInput(event, cwd);
14
+ if (!filePath) return undefined;
15
+
16
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
17
+ const ext = path.extname(fullPath);
18
+
19
+ let scriptPath: string | null = null;
20
+ let runner: string = "node";
21
+
22
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
23
+ scriptPath = path.join(cwd, ".claude", "hooks", "quality-check.cjs");
24
+ runner = "node";
25
+ } else if (ext === ".py") {
26
+ scriptPath = path.join(cwd, ".claude", "hooks", "quality-check.py");
27
+ runner = "python3";
28
+ }
29
+
30
+ if (!scriptPath || !fs.existsSync(scriptPath)) return undefined;
31
+
32
+ const hookInput = JSON.stringify({
33
+ tool_name: event.toolName,
34
+ tool_input: event.input,
35
+ cwd: cwd,
36
+ });
37
+
38
+ const result = await SubprocessRunner.run(runner, [scriptPath], {
39
+ cwd,
40
+ input: hookInput,
41
+ env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
42
+ timeoutMs: 30000,
43
+ });
44
+
45
+ if (result.code === 0) {
46
+ if (result.stderr && result.stderr.trim()) {
47
+ const newContent = [...event.content];
48
+ newContent.push({ type: "text", text: `\n\n**Quality Gate**: ${result.stderr.trim()}` });
49
+ return { content: newContent };
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ if (result.code === 2) {
55
+ const newContent = [...event.content];
56
+ newContent.push({ type: "text", text: `\n\n**Quality Gate FAILED**:\n${result.stderr || result.stdout || "Unknown error"}` });
57
+
58
+ if (ctx.hasUI) {
59
+ ctx.ui.notify(`Quality Gate failed for ${path.basename(fullPath)}`, "error");
60
+ }
61
+
62
+ return { isError: true, content: newContent };
63
+ }
64
+
65
+ return undefined;
66
+ });
67
+ }
@@ -0,0 +1,150 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { SubprocessRunner, Logger } from "./core/lib";
3
+ import * as path from "node:path";
4
+ import * as fs from "node:fs";
5
+
6
+ const logger = new Logger({ namespace: "service-skills" });
7
+
8
+ const SERVICE_REGISTRY_FILES = [
9
+ "service-registry.json",
10
+ path.join(".claude", "skills", "service-registry.json"),
11
+ ];
12
+
13
+ const GLOBAL_SKILL_ROOTS = [
14
+ path.join(process.env.HOME || "", ".agents", "skills"),
15
+ path.join(process.env.HOME || "", ".claude", "skills"),
16
+ ];
17
+
18
+ export default function (pi: ExtensionAPI) {
19
+ const getCwd = (ctx: any) => ctx.cwd || process.cwd();
20
+
21
+ const resolveRegistryPath = (cwd: string): string | null => {
22
+ for (const rel of SERVICE_REGISTRY_FILES) {
23
+ const candidate = path.join(cwd, rel);
24
+ if (fs.existsSync(candidate)) return candidate;
25
+ }
26
+ return null;
27
+ };
28
+
29
+ const resolveSkillScript = (cwd: string, skillName: string, scriptName: string): string | null => {
30
+ const localPath = path.join(cwd, ".claude", "skills", skillName, "scripts", scriptName);
31
+ if (fs.existsSync(localPath)) return localPath;
32
+
33
+ for (const root of GLOBAL_SKILL_ROOTS) {
34
+ if (!root) continue;
35
+ const candidate = path.join(root, skillName, "scripts", scriptName);
36
+ if (fs.existsSync(candidate)) return candidate;
37
+ }
38
+
39
+ return null;
40
+ };
41
+
42
+ const toClaudeToolName = (toolName: string): string => {
43
+ if (toolName === "bash") return "Bash";
44
+ if (toolName === "read_file") return "Read";
45
+ if (toolName === "write" || toolName === "create_text_file") return "Write";
46
+ if (toolName === "edit" || toolName === "replace_content" || toolName === "replace_lines" || toolName === "insert_at_line" || toolName === "delete_lines") return "Edit";
47
+ if (toolName === "search_for_pattern") return "Grep";
48
+ if (toolName === "find_file" || toolName === "list_dir") return "Glob";
49
+ return toolName;
50
+ };
51
+
52
+ // 1. Catalog Injection
53
+ pi.on("before_agent_start", async (event, ctx) => {
54
+ const cwd = getCwd(ctx);
55
+ const registryPath = resolveRegistryPath(cwd);
56
+ if (!registryPath) return undefined;
57
+
58
+ const catalogerPath = resolveSkillScript(cwd, "using-service-skills", "cataloger.py");
59
+ if (!catalogerPath) return undefined;
60
+
61
+ const result = await SubprocessRunner.run("python3", [catalogerPath], {
62
+ cwd,
63
+ env: {
64
+ ...process.env,
65
+ CLAUDE_PROJECT_DIR: cwd,
66
+ SERVICE_REGISTRY_PATH: registryPath,
67
+ },
68
+ });
69
+
70
+ if (result.code === 0 && result.stdout.trim()) {
71
+ return { systemPrompt: event.systemPrompt + "\n\n" + result.stdout.trim() };
72
+ }
73
+ return undefined;
74
+ });
75
+
76
+ // 2. Territory Activation (optional; no-op when script not available)
77
+ pi.on("tool_call", async (event, ctx) => {
78
+ const cwd = getCwd(ctx);
79
+ const registryPath = resolveRegistryPath(cwd);
80
+ if (!registryPath) return undefined;
81
+
82
+ const activatorPath = resolveSkillScript(cwd, "using-service-skills", "skill_activator.py");
83
+ if (!activatorPath) return undefined;
84
+
85
+ const hookInput = JSON.stringify({
86
+ tool_name: toClaudeToolName(event.toolName),
87
+ tool_input: event.input,
88
+ cwd,
89
+ });
90
+
91
+ const result = await SubprocessRunner.run("python3", [activatorPath], {
92
+ cwd,
93
+ input: hookInput,
94
+ env: {
95
+ ...process.env,
96
+ CLAUDE_PROJECT_DIR: cwd,
97
+ SERVICE_REGISTRY_PATH: registryPath,
98
+ },
99
+ timeoutMs: 5000,
100
+ });
101
+
102
+ if (result.code === 0 && result.stdout.trim()) {
103
+ try {
104
+ const parsed = JSON.parse(result.stdout.trim());
105
+ const context = parsed.hookSpecificOutput?.additionalContext;
106
+ if (context && ctx.hasUI) {
107
+ ctx.ui.notify(context, "info");
108
+ }
109
+ } catch (e) {
110
+ logger.error("Failed to parse skill_activator output", e);
111
+ }
112
+ }
113
+
114
+ return undefined;
115
+ });
116
+
117
+ // 3. Drift Detection
118
+ pi.on("tool_result", async (event, ctx) => {
119
+ const cwd = getCwd(ctx);
120
+ const registryPath = resolveRegistryPath(cwd);
121
+ if (!registryPath) return undefined;
122
+
123
+ const driftDetectorPath = resolveSkillScript(cwd, "updating-service-skills", "drift_detector.py");
124
+ if (!driftDetectorPath) return undefined;
125
+
126
+ const hookInput = JSON.stringify({
127
+ tool_name: toClaudeToolName(event.toolName),
128
+ tool_input: event.input,
129
+ cwd,
130
+ });
131
+
132
+ const result = await SubprocessRunner.run("python3", [driftDetectorPath], {
133
+ cwd,
134
+ input: hookInput,
135
+ env: {
136
+ ...process.env,
137
+ CLAUDE_PROJECT_DIR: cwd,
138
+ SERVICE_REGISTRY_PATH: registryPath,
139
+ },
140
+ timeoutMs: 10000,
141
+ });
142
+
143
+ if (result.code === 0 && result.stdout.trim()) {
144
+ const newContent = [...event.content];
145
+ newContent.push({ type: "text", text: "\n\n" + result.stdout.trim() });
146
+ return { content: newContent };
147
+ }
148
+ return undefined;
149
+ });
150
+ }
@@ -0,0 +1,89 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { Logger } from "./core/lib";
5
+
6
+ const logger = new Logger({ namespace: "xtrm-loader" });
7
+
8
+ /**
9
+ * Recursively find markdown files in a directory.
10
+ */
11
+ function findMarkdownFiles(dir: string, basePath: string = ""): string[] {
12
+ const results: string[] = [];
13
+ if (!fs.existsSync(dir)) return results;
14
+
15
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
18
+ if (entry.isDirectory()) {
19
+ results.push(...findMarkdownFiles(path.join(dir, entry.name), relativePath));
20
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
21
+ results.push(relativePath);
22
+ }
23
+ }
24
+ return results;
25
+ }
26
+
27
+ export default function (pi: ExtensionAPI) {
28
+ let projectContext: string = "";
29
+
30
+ pi.on("session_start", async (_event, ctx) => {
31
+ const cwd = ctx.cwd;
32
+ const contextParts: string[] = [];
33
+
34
+ // 1. Architecture & Roadmap
35
+ const roadmapPaths = [
36
+ path.join(cwd, "architecture", "project_roadmap.md"),
37
+ path.join(cwd, "ROADMAP.md"),
38
+ path.join(cwd, "architecture", "index.md")
39
+ ];
40
+
41
+ for (const p of roadmapPaths) {
42
+ if (fs.existsSync(p)) {
43
+ const content = fs.readFileSync(p, "utf8");
44
+ contextParts.push(`## Project Roadmap & Architecture (${path.relative(cwd, p)})\n\n${content}`);
45
+ break; // Only load the first one found
46
+ }
47
+ }
48
+
49
+ // 2. Project Rules (.claude/rules)
50
+ const rulesDir = path.join(cwd, ".claude", "rules");
51
+ if (fs.existsSync(rulesDir)) {
52
+ const ruleFiles = findMarkdownFiles(rulesDir);
53
+ if (ruleFiles.length > 0) {
54
+ const rulesContent = ruleFiles.map(f => {
55
+ const content = fs.readFileSync(path.join(rulesDir, f), "utf8");
56
+ return `### Rule: ${f}\n${content}`;
57
+ }).join("\n\n");
58
+ contextParts.push(`## Project Rules\n\n${rulesContent}`);
59
+ }
60
+ }
61
+
62
+ // 3. Project Skills (.claude/skills)
63
+ const skillsDir = path.join(cwd, ".claude", "skills");
64
+ if (fs.existsSync(skillsDir)) {
65
+ const skillFiles = findMarkdownFiles(skillsDir);
66
+ if (skillFiles.length > 0) {
67
+ const skillsContent = skillFiles.map(f => {
68
+ // We only want to list the paths/names so the agent knows what it can read
69
+ return `- ${f} (Path: .claude/skills/${f})`;
70
+ }).join("\n");
71
+ contextParts.push(`## Available Project Skills\n\nExisting service skills and workflows found in .claude/skills/:\n\n${skillsContent}\n\nUse the read tool to load any of these skills if relevant to the current task.`);
72
+ }
73
+ }
74
+
75
+ projectContext = contextParts.join("\n\n---\n\n");
76
+
77
+ if (projectContext && ctx.hasUI) {
78
+ ctx.ui.notify("XTRM-Loader: Project context and skills indexed", "info");
79
+ }
80
+ });
81
+
82
+ pi.on("before_agent_start", async (event) => {
83
+ if (!projectContext) return undefined;
84
+
85
+ return {
86
+ systemPrompt: event.systemPrompt + "\n\n# Project Intelligence Context\n\n" + projectContext
87
+ };
88
+ });
89
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ import os
4
+
5
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
6
+ from agent_context import AgentContext
7
+
8
+ try:
9
+ ctx = AgentContext()
10
+ ctx.fail_open()
11
+ except Exception as e:
12
+ print(f"Hook error: {e}", file=sys.stderr)
13
+ sys.exit(0)