@imdeadpool/guardex 7.0.41 → 7.1.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 (118) hide show
  1. package/README.md +94 -13
  2. package/package.json +3 -1
  3. package/skills/gitguardex/SKILL.md +13 -0
  4. package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
  5. package/skills/gx-act/SKILL.md +82 -0
  6. package/src/agents/cleanup-sessions.js +126 -0
  7. package/src/agents/finish.js +172 -0
  8. package/src/agents/inspect.js +202 -0
  9. package/src/agents/launch.js +249 -0
  10. package/src/agents/registry.js +133 -0
  11. package/src/agents/selection-panel.js +571 -0
  12. package/src/agents/sessions.js +151 -0
  13. package/src/agents/start.js +591 -0
  14. package/src/agents/status.js +146 -0
  15. package/src/agents/terminal.js +152 -0
  16. package/src/budget/index.js +344 -0
  17. package/src/ci-init/index.js +265 -0
  18. package/src/cli/args.js +357 -3
  19. package/src/cli/commands/agents.js +364 -0
  20. package/src/cli/commands/bootstrap.js +92 -0
  21. package/src/cli/commands/branch.js +127 -0
  22. package/src/cli/commands/claude.js +674 -0
  23. package/src/cli/commands/doctor.js +268 -0
  24. package/src/cli/commands/finish.js +26 -0
  25. package/src/cli/commands/mcp.js +122 -0
  26. package/src/cli/commands/misc.js +304 -0
  27. package/src/cli/commands/pr.js +439 -0
  28. package/src/cli/commands/prompt.js +92 -0
  29. package/src/cli/commands/release.js +305 -0
  30. package/src/cli/commands/report.js +244 -0
  31. package/src/cli/commands/review.js +32 -0
  32. package/src/cli/commands/setup.js +242 -0
  33. package/src/cli/commands/status.js +338 -0
  34. package/src/cli/commands/watch.js +234 -0
  35. package/src/cli/main.js +85 -3613
  36. package/src/cli/shared/repo-env.js +161 -0
  37. package/src/cli/shared/sandbox.js +417 -0
  38. package/src/cli/shared/scaffolding.js +535 -0
  39. package/src/cli/shared/toolchain-shims.js +420 -0
  40. package/src/cockpit/action-runner.js +3 -0
  41. package/src/cockpit/actions.js +80 -0
  42. package/src/cockpit/control.js +1121 -0
  43. package/src/cockpit/index.js +426 -0
  44. package/src/cockpit/kitty-layout.js +549 -0
  45. package/src/cockpit/kitty-tree.js +144 -0
  46. package/src/cockpit/logs-reader.js +182 -0
  47. package/src/cockpit/menu.js +204 -0
  48. package/src/cockpit/pane-actions.js +597 -0
  49. package/src/cockpit/pane-menu.js +387 -0
  50. package/src/cockpit/projects-finder.js +178 -0
  51. package/src/cockpit/render.js +215 -0
  52. package/src/cockpit/settings-render.js +128 -0
  53. package/src/cockpit/settings.js +124 -0
  54. package/src/cockpit/shortcuts.js +24 -0
  55. package/src/cockpit/sidebar.js +311 -0
  56. package/src/cockpit/state.js +72 -0
  57. package/src/cockpit/theme.js +128 -0
  58. package/src/cockpit/welcome.js +266 -0
  59. package/src/context.js +304 -43
  60. package/src/core/runtime.js +6 -1
  61. package/src/doctor/index.js +45 -15
  62. package/src/finish/index.js +186 -7
  63. package/src/finish/preflight.js +177 -0
  64. package/src/finish/review-gate.js +182 -0
  65. package/src/git/index.js +511 -4
  66. package/src/hooks/index.js +0 -64
  67. package/src/kitty/command.js +101 -0
  68. package/src/kitty/runtime.js +250 -0
  69. package/src/mcp/collect.js +370 -0
  70. package/src/mcp/server.js +157 -0
  71. package/src/output/index.js +68 -2
  72. package/src/pr-review.js +264 -0
  73. package/src/pr.js +381 -0
  74. package/src/sandbox/index.js +13 -2
  75. package/src/scaffold/agent-worktree-prep.js +213 -0
  76. package/src/scaffold/index.js +127 -10
  77. package/src/speckit/index.js +226 -0
  78. package/src/submodule/index.js +288 -0
  79. package/src/terminal/index.js +45 -0
  80. package/src/terminal/kitty.js +622 -0
  81. package/src/terminal/tmux.js +125 -0
  82. package/src/tmux/command.js +27 -0
  83. package/src/tmux/session.js +89 -0
  84. package/src/toolchain/index.js +20 -0
  85. package/templates/AGENTS.monorepo-apps.md +26 -0
  86. package/templates/AGENTS.multiagent-safety.md +63 -323
  87. package/templates/AGENTS.multiagent-safety.min.md +11 -0
  88. package/templates/codex/skills/gitguardex/SKILL.md +2 -0
  89. package/templates/codex/skills/gx-act/SKILL.md +82 -0
  90. package/templates/githooks/pre-commit +44 -20
  91. package/templates/github/workflows/README.md +87 -0
  92. package/templates/github/workflows/ci-full.yml +55 -0
  93. package/templates/github/workflows/ci.yml +56 -0
  94. package/templates/github/workflows/cr.yml +20 -1
  95. package/templates/scripts/agent-branch-finish.sh +519 -23
  96. package/templates/scripts/agent-branch-merge.sh +4 -1
  97. package/templates/scripts/agent-branch-start.sh +176 -24
  98. package/templates/scripts/agent-preflight.sh +115 -0
  99. package/templates/scripts/agent-worktree-prune.sh +96 -5
  100. package/templates/scripts/codex-agent.sh +41 -97
  101. package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
  102. package/templates/scripts/review-bot-watch.sh +31 -2
  103. package/templates/scripts/agent-session-state.js +0 -171
  104. package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
  105. package/templates/vscode/guardex-active-agents/README.md +0 -34
  106. package/templates/vscode/guardex-active-agents/extension.js +0 -3782
  107. package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
  108. package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
  109. package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
  110. package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
  111. package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
  112. package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
  113. package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
  114. package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
  115. package/templates/vscode/guardex-active-agents/icon.png +0 -0
  116. package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
  117. package/templates/vscode/guardex-active-agents/package.json +0 -169
  118. package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
@@ -0,0 +1,674 @@
1
+ // `gx claude` — Claude Code integration commands.
2
+ //
3
+ // Subcommands:
4
+ // gx claude install install/update .claude/settings.json, hooks, slash
5
+ // commands, and the gitguardex agent skill in the
6
+ // target repo. Idempotent.
7
+ // gx claude check diagnose Claude Code wiring (no mutations).
8
+ // gx claude uninstall remove gitguardex-managed Claude Code wiring.
9
+ // gx claude doctor alias for `check --fix`.
10
+ //
11
+ // This command makes a repo "Claude Code-ready" so the agent can pivot, claim
12
+ // files, open PRs, and follow the gitguardex contract without manual setup.
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { TOOL_NAME, SHORT_TOOL_NAME } = require('../../context');
17
+ const { resolveRepoRoot } = require('../../git');
18
+
19
+ const SETTINGS_REL = '.claude/settings.json';
20
+ const HOOKS_REL = '.claude/hooks';
21
+ const COMMANDS_REL = '.claude/commands';
22
+ const SKILLS_REL = '.claude/skills';
23
+ // Repo-scoped MCP registration so any agent in the target repo can see the
24
+ // cross-repo agent radar (`gx mcp`). Read-only server; opt out with --no-mcp.
25
+ const MCP_REL = '.mcp.json';
26
+ const MCP_SERVER_KEY = SHORT_TOOL_NAME;
27
+
28
+ const MANAGED_HOOK_FILES = [
29
+ 'skill_guard.py',
30
+ 'skill_activation.py',
31
+ 'agent_branch_advisor.py',
32
+ 'post_edit_tracker.py',
33
+ 'skill_tracker.py',
34
+ ];
35
+
36
+ const MANAGED_SLASH_COMMANDS = [
37
+ 'gx-act.md',
38
+ 'gx-doctor.md',
39
+ 'gx-finish.md',
40
+ 'gx-pivot.md',
41
+ 'gx-pr.md',
42
+ 'gx-setup.md',
43
+ 'gx-status.md',
44
+ ];
45
+
46
+ const EXPECTED_HOOK_MATCHERS = {
47
+ SessionStart: ['agent-stalled-report.sh', 'agent_branch_advisor.py'],
48
+ UserPromptSubmit: ['skill_activation.py', 'agent_branch_advisor.py'],
49
+ PreToolUse: ['skill_guard.py'],
50
+ PostToolUse: ['post_edit_tracker.py', 'skill_tracker.py'],
51
+ };
52
+
53
+ const TEMPLATE_DEFAULT_SETTINGS = {
54
+ hooks: {
55
+ SessionStart: [
56
+ {
57
+ hooks: [
58
+ {
59
+ type: 'command',
60
+ command: 'bash "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/scripts/agent-stalled-report.sh"',
61
+ },
62
+ {
63
+ type: 'command',
64
+ command: 'python3 "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.claude/hooks/agent_branch_advisor.py"',
65
+ },
66
+ ],
67
+ },
68
+ ],
69
+ UserPromptSubmit: [
70
+ {
71
+ hooks: [
72
+ {
73
+ type: 'command',
74
+ command: 'python3 "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.claude/hooks/skill_activation.py"',
75
+ },
76
+ {
77
+ type: 'command',
78
+ command: 'python3 "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.claude/hooks/agent_branch_advisor.py"',
79
+ },
80
+ ],
81
+ },
82
+ ],
83
+ PreToolUse: [
84
+ {
85
+ matcher: 'Bash|Edit|MultiEdit|Write|ApplyPatch|apply_patch|Patch',
86
+ hooks: [
87
+ {
88
+ type: 'command',
89
+ command: 'python3 "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.claude/hooks/skill_guard.py"',
90
+ },
91
+ ],
92
+ },
93
+ ],
94
+ PostToolUse: [
95
+ {
96
+ matcher: 'Edit|Write|MultiEdit',
97
+ hooks: [
98
+ {
99
+ type: 'command',
100
+ command: 'python3 "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.claude/hooks/post_edit_tracker.py"',
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ matcher: 'Skill',
106
+ hooks: [
107
+ {
108
+ type: 'command',
109
+ command: 'python3 "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/.claude/hooks/skill_tracker.py"',
110
+ },
111
+ ],
112
+ },
113
+ ],
114
+ },
115
+ };
116
+
117
+ function logInfo(msg) {
118
+ console.log(`[${TOOL_NAME}] ${msg}`);
119
+ }
120
+ function logOk(msg) {
121
+ console.log(`[${TOOL_NAME}] ✅ ${msg}`);
122
+ }
123
+ function logWarn(msg) {
124
+ console.log(`[${TOOL_NAME}] ⚠️ ${msg}`);
125
+ }
126
+ function logError(msg) {
127
+ console.error(`[${TOOL_NAME}] ❌ ${msg}`);
128
+ }
129
+
130
+ function readJsonIfExists(filePath) {
131
+ if (!fs.existsSync(filePath)) return null;
132
+ const raw = fs.readFileSync(filePath, 'utf8');
133
+ if (!raw.trim()) return null;
134
+ try {
135
+ return JSON.parse(raw);
136
+ } catch (error) {
137
+ throw new Error(`Failed to parse JSON at ${filePath}: ${error.message}`);
138
+ }
139
+ }
140
+
141
+ function writeJson(filePath, value, { dryRun }) {
142
+ const parent = path.dirname(filePath);
143
+ if (!fs.existsSync(parent)) {
144
+ if (!dryRun) fs.mkdirSync(parent, { recursive: true });
145
+ }
146
+ if (dryRun) return;
147
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
148
+ }
149
+
150
+ function deepClone(value) {
151
+ return JSON.parse(JSON.stringify(value));
152
+ }
153
+
154
+ function mergeHookGroupArrays(existingGroups, templateGroups) {
155
+ // Each group looks like { matcher?, hooks: [{type, command}, ...] }.
156
+ // We merge by matcher and by exact command string within hooks.
157
+ const out = Array.isArray(existingGroups) ? deepClone(existingGroups) : [];
158
+ for (const tplGroup of templateGroups) {
159
+ const matcher = tplGroup.matcher || null;
160
+ const targetGroup = out.find((g) => (g.matcher || null) === matcher);
161
+ if (!targetGroup) {
162
+ out.push(deepClone(tplGroup));
163
+ continue;
164
+ }
165
+ targetGroup.hooks = Array.isArray(targetGroup.hooks) ? targetGroup.hooks : [];
166
+ for (const tplHook of tplGroup.hooks || []) {
167
+ const exists = targetGroup.hooks.find((h) => h.command === tplHook.command);
168
+ if (!exists) targetGroup.hooks.push(deepClone(tplHook));
169
+ }
170
+ }
171
+ return out;
172
+ }
173
+
174
+ function mergeSettings(existing, template) {
175
+ const base = existing ? deepClone(existing) : {};
176
+ base.hooks = base.hooks || {};
177
+ for (const eventName of Object.keys(template.hooks)) {
178
+ base.hooks[eventName] = mergeHookGroupArrays(
179
+ base.hooks[eventName],
180
+ template.hooks[eventName],
181
+ );
182
+ }
183
+ return base;
184
+ }
185
+
186
+ function findPackageRoot() {
187
+ return path.resolve(__dirname, '..', '..', '..');
188
+ }
189
+
190
+ function copyFileIfDifferent(srcPath, destPath, { dryRun }) {
191
+ if (!fs.existsSync(srcPath)) {
192
+ return { status: 'source-missing', dest: destPath };
193
+ }
194
+ const srcContent = fs.readFileSync(srcPath);
195
+ if (fs.existsSync(destPath)) {
196
+ const existing = fs.readFileSync(destPath);
197
+ if (existing.equals(srcContent)) {
198
+ return { status: 'unchanged', dest: destPath };
199
+ }
200
+ if (!dryRun) fs.writeFileSync(destPath, srcContent);
201
+ return { status: 'updated', dest: destPath };
202
+ }
203
+ if (!dryRun) {
204
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
205
+ fs.writeFileSync(destPath, srcContent);
206
+ }
207
+ return { status: 'created', dest: destPath };
208
+ }
209
+
210
+ function installHooks(repoRoot, { dryRun }) {
211
+ const packageRoot = findPackageRoot();
212
+ const results = [];
213
+ for (const hookFile of MANAGED_HOOK_FILES) {
214
+ const src = path.join(packageRoot, HOOKS_REL, hookFile);
215
+ const dest = path.join(repoRoot, HOOKS_REL, hookFile);
216
+ const result = copyFileIfDifferent(src, dest, { dryRun });
217
+ if (result.status === 'source-missing') continue;
218
+ results.push({ hook: hookFile, ...result });
219
+ if (!dryRun && fs.existsSync(dest)) {
220
+ try {
221
+ fs.chmodSync(dest, 0o755);
222
+ } catch (_error) {
223
+ // Ignore chmod failures (Windows, etc.)
224
+ }
225
+ }
226
+ }
227
+ return results;
228
+ }
229
+
230
+ function installSlashCommands(repoRoot, { dryRun }) {
231
+ const packageRoot = findPackageRoot();
232
+ const results = [];
233
+ for (const filename of MANAGED_SLASH_COMMANDS) {
234
+ const src = path.join(packageRoot, COMMANDS_REL, filename);
235
+ const dest = path.join(repoRoot, COMMANDS_REL, filename);
236
+ const result = copyFileIfDifferent(src, dest, { dryRun });
237
+ if (result.status === 'source-missing') continue;
238
+ results.push({ command: filename, ...result });
239
+ }
240
+ return results;
241
+ }
242
+
243
+ function installAgentSkill(repoRoot, { dryRun }) {
244
+ const packageRoot = findPackageRoot();
245
+ const srcDir = path.join(packageRoot, SKILLS_REL, 'gitguardex');
246
+ if (!fs.existsSync(srcDir)) return { status: 'source-missing' };
247
+ const destDir = path.join(repoRoot, SKILLS_REL, 'gitguardex');
248
+ if (!dryRun) fs.mkdirSync(destDir, { recursive: true });
249
+ const results = [];
250
+ for (const entry of fs.readdirSync(srcDir)) {
251
+ const src = path.join(srcDir, entry);
252
+ const dest = path.join(destDir, entry);
253
+ if (fs.statSync(src).isDirectory()) continue;
254
+ const r = copyFileIfDifferent(src, dest, { dryRun });
255
+ results.push({ skill: `gitguardex/${entry}`, ...r });
256
+ }
257
+ return { status: 'ok', files: results };
258
+ }
259
+
260
+ function installSettings(repoRoot, { dryRun, force }) {
261
+ const settingsPath = path.join(repoRoot, SETTINGS_REL);
262
+ const existing = readJsonIfExists(settingsPath);
263
+ const merged = force
264
+ ? mergeSettings({}, TEMPLATE_DEFAULT_SETTINGS)
265
+ : mergeSettings(existing, TEMPLATE_DEFAULT_SETTINGS);
266
+
267
+ const before = existing ? JSON.stringify(existing) : '';
268
+ const after = JSON.stringify(merged);
269
+
270
+ if (before === after) {
271
+ return { status: 'unchanged', path: settingsPath };
272
+ }
273
+ writeJson(settingsPath, merged, { dryRun });
274
+ return {
275
+ status: existing ? 'updated' : 'created',
276
+ path: settingsPath,
277
+ };
278
+ }
279
+
280
+ function ensureSpeckitMarkers(repoRoot, { dryRun }) {
281
+ // SPECKIT START/END markers in AGENTS.md / CLAUDE.md are managed by speckit;
282
+ // we just confirm CLAUDE.md exists and is a symlink to AGENTS.md (or copy).
283
+ const agentsMd = path.join(repoRoot, 'AGENTS.md');
284
+ const claudeMd = path.join(repoRoot, 'CLAUDE.md');
285
+ if (!fs.existsSync(agentsMd)) {
286
+ return { status: 'no-agents-md', note: 'AGENTS.md not found; skipping CLAUDE.md sync.' };
287
+ }
288
+ let claudeStat = null;
289
+ try {
290
+ claudeStat = fs.lstatSync(claudeMd);
291
+ } catch (_error) {
292
+ claudeStat = null;
293
+ }
294
+
295
+ if (claudeStat && claudeStat.isSymbolicLink()) {
296
+ const target = fs.readlinkSync(claudeMd);
297
+ if (path.resolve(path.dirname(claudeMd), target) === path.resolve(agentsMd)) {
298
+ return { status: 'symlink-ok' };
299
+ }
300
+ if (!dryRun) {
301
+ fs.unlinkSync(claudeMd);
302
+ fs.symlinkSync('AGENTS.md', claudeMd);
303
+ }
304
+ return { status: 'symlink-repaired' };
305
+ }
306
+
307
+ if (claudeStat && claudeStat.isFile()) {
308
+ // Don't clobber a user's CLAUDE.md silently. Just note it.
309
+ return { status: 'claude-md-not-symlink', note: 'CLAUDE.md exists as regular file; not modifying.' };
310
+ }
311
+
312
+ if (!dryRun) {
313
+ try {
314
+ fs.symlinkSync('AGENTS.md', claudeMd);
315
+ return { status: 'symlink-created' };
316
+ } catch (error) {
317
+ // Fall back to a copy if symlink is unsupported.
318
+ fs.copyFileSync(agentsMd, claudeMd);
319
+ return { status: 'copy-created', note: `symlink failed (${error.code}); copied instead.` };
320
+ }
321
+ }
322
+ return { status: 'would-create-symlink' };
323
+ }
324
+
325
+ function describeStatus(s) {
326
+ if (s === 'unchanged') return '·';
327
+ if (s === 'created') return '+';
328
+ if (s === 'updated' || s === 'overwritten') return '~';
329
+ if (s.startsWith('symlink')) return 's';
330
+ return '?';
331
+ }
332
+
333
+ function mcpServerSpec() {
334
+ return { command: SHORT_TOOL_NAME, args: ['mcp', 'serve'] };
335
+ }
336
+
337
+ // Register the read-only `gx mcp` server in the target repo's .mcp.json so any
338
+ // agent there can call list_agents / who_owns / my_context. Merges into an
339
+ // existing .mcp.json without disturbing other servers; idempotent.
340
+ function installMcpServer(repoRoot, { dryRun }) {
341
+ const filePath = path.join(repoRoot, MCP_REL);
342
+ const fileExisted = fs.existsSync(filePath);
343
+ const config = readJsonIfExists(filePath) || {};
344
+ config.mcpServers = config.mcpServers || {};
345
+ const desired = mcpServerSpec();
346
+ const current = config.mcpServers[MCP_SERVER_KEY];
347
+ if (current && JSON.stringify(current) === JSON.stringify(desired)) {
348
+ return { status: 'unchanged', dest: filePath };
349
+ }
350
+ const status = current ? 'updated' : fileExisted ? 'merged' : 'created';
351
+ config.mcpServers[MCP_SERVER_KEY] = desired;
352
+ writeJson(filePath, config, { dryRun });
353
+ return { status, dest: filePath };
354
+ }
355
+
356
+ // Inverse of installMcpServer: drop the gx server. Removes the whole .mcp.json
357
+ // only when it held nothing but our server (no other servers AND no other
358
+ // top-level keys); otherwise prunes just the gx entry and preserves the rest.
359
+ function uninstallMcpServer(repoRoot, { dryRun }) {
360
+ const filePath = path.join(repoRoot, MCP_REL);
361
+ const config = readJsonIfExists(filePath);
362
+ if (!config || !config.mcpServers || !config.mcpServers[MCP_SERVER_KEY]) {
363
+ return { status: 'absent', dest: filePath };
364
+ }
365
+ delete config.mcpServers[MCP_SERVER_KEY];
366
+ const onlyOurs = Object.keys(config.mcpServers).length === 0 && Object.keys(config).length === 1;
367
+ if (!dryRun) {
368
+ if (onlyOurs) fs.unlinkSync(filePath);
369
+ else writeJson(filePath, config, { dryRun: false });
370
+ }
371
+ return { status: onlyOurs ? 'removed' : 'pruned', dest: filePath };
372
+ }
373
+
374
+ function runInstall(rawArgs) {
375
+ const opts = parseInstallArgs(rawArgs);
376
+ const repoRoot = resolveRepoRoot(opts.target);
377
+ logInfo(`Installing Claude Code integration into ${repoRoot}${opts.dryRun ? ' (dry-run)' : ''}`);
378
+
379
+ const settingsResult = installSettings(repoRoot, opts);
380
+ const hookResults = installHooks(repoRoot, opts);
381
+ const slashResults = installSlashCommands(repoRoot, opts);
382
+ const skillResult = installAgentSkill(repoRoot, opts);
383
+ const mcpResult = opts.noMcp
384
+ ? { status: 'skipped', dest: path.join(repoRoot, MCP_REL) }
385
+ : installMcpServer(repoRoot, opts);
386
+ const symlinkResult = ensureSpeckitMarkers(repoRoot, opts);
387
+
388
+ // Summary
389
+ const summarize = (label, items, key) => {
390
+ if (!items.length) return;
391
+ logInfo(`${label}:`);
392
+ for (const item of items) {
393
+ console.log(` ${describeStatus(item.status)} ${item[key]} (${item.status})`);
394
+ }
395
+ };
396
+
397
+ logInfo(`settings: ${settingsResult.status}`);
398
+ summarize('hooks', hookResults, 'hook');
399
+ summarize('slash commands', slashResults, 'command');
400
+ if (skillResult.status === 'ok') {
401
+ summarize('skill: gitguardex', skillResult.files, 'skill');
402
+ } else if (skillResult.status === 'source-missing') {
403
+ logWarn('gitguardex skill source missing in package; skipped.');
404
+ }
405
+ logInfo(`mcp server (${MCP_REL}): ${mcpResult.status}`);
406
+ logInfo(`CLAUDE.md symlink: ${symlinkResult.status}${symlinkResult.note ? ` (${symlinkResult.note})` : ''}`);
407
+
408
+ if (opts.json) {
409
+ process.stdout.write(JSON.stringify({
410
+ repoRoot,
411
+ settings: settingsResult,
412
+ hooks: hookResults,
413
+ slashCommands: slashResults,
414
+ skill: skillResult,
415
+ mcp: mcpResult,
416
+ symlink: symlinkResult,
417
+ dryRun: opts.dryRun,
418
+ }, null, 2) + '\n');
419
+ return;
420
+ }
421
+
422
+ if (!opts.dryRun) {
423
+ logOk('Claude Code wiring is in place.');
424
+ logInfo(`Next: run '${SHORT_TOOL_NAME} status' to verify and '${SHORT_TOOL_NAME} pivot "<task>" "claude-<name>"' to start work.`);
425
+ }
426
+ }
427
+
428
+ function runCheck(rawArgs) {
429
+ const opts = parseInstallArgs(rawArgs);
430
+ const repoRoot = resolveRepoRoot(opts.target);
431
+ const issues = [];
432
+
433
+ const settingsPath = path.join(repoRoot, SETTINGS_REL);
434
+ let settings = null;
435
+ try {
436
+ settings = readJsonIfExists(settingsPath);
437
+ } catch (error) {
438
+ issues.push({ severity: 'error', kind: 'settings-parse', message: error.message });
439
+ }
440
+ if (!settings) {
441
+ issues.push({
442
+ severity: 'error',
443
+ kind: 'settings-missing',
444
+ message: `${SETTINGS_REL} not found. Run '${SHORT_TOOL_NAME} claude install'.`,
445
+ });
446
+ } else {
447
+ const hooks = settings.hooks || {};
448
+ for (const eventName of Object.keys(EXPECTED_HOOK_MATCHERS)) {
449
+ const groups = hooks[eventName] || [];
450
+ const commands = groups.flatMap((g) => (g.hooks || []).map((h) => h.command || ''));
451
+ const expected = EXPECTED_HOOK_MATCHERS[eventName];
452
+ const missing = expected.filter((needle) => !commands.some((cmd) => cmd.includes(needle)));
453
+ for (const m of missing) {
454
+ issues.push({
455
+ severity: 'warning',
456
+ kind: 'hook-missing',
457
+ event: eventName,
458
+ message: `${eventName} hook missing reference to ${m}`,
459
+ });
460
+ }
461
+ }
462
+ }
463
+
464
+ for (const hook of MANAGED_HOOK_FILES) {
465
+ const hookPath = path.join(repoRoot, HOOKS_REL, hook);
466
+ if (!fs.existsSync(hookPath)) {
467
+ issues.push({
468
+ severity: 'error',
469
+ kind: 'hook-file-missing',
470
+ message: `${HOOKS_REL}/${hook} missing`,
471
+ });
472
+ } else {
473
+ try {
474
+ const mode = fs.statSync(hookPath).mode & 0o777;
475
+ if ((mode & 0o111) === 0 && process.platform !== 'win32') {
476
+ issues.push({
477
+ severity: 'warning',
478
+ kind: 'hook-not-executable',
479
+ message: `${HOOKS_REL}/${hook} is not executable`,
480
+ });
481
+ }
482
+ } catch (_error) {
483
+ // ignore
484
+ }
485
+ }
486
+ }
487
+
488
+ // MCP registration check
489
+ const mcpConfig = readJsonIfExists(path.join(repoRoot, MCP_REL));
490
+ const hasGxMcp = Boolean(mcpConfig && mcpConfig.mcpServers && mcpConfig.mcpServers[MCP_SERVER_KEY]);
491
+ if (!hasGxMcp) {
492
+ issues.push({
493
+ severity: 'warning',
494
+ kind: 'mcp-missing',
495
+ message: `${MCP_REL} does not register the '${MCP_SERVER_KEY}' MCP server (run '${SHORT_TOOL_NAME} claude install', or install --no-mcp to skip).`,
496
+ });
497
+ }
498
+
499
+ // Symlink check
500
+ const symlinkResult = ensureSpeckitMarkers(repoRoot, { dryRun: true });
501
+ if (symlinkResult.status === 'would-create-symlink'
502
+ || symlinkResult.status === 'claude-md-not-symlink') {
503
+ issues.push({
504
+ severity: 'warning',
505
+ kind: 'claude-md-symlink',
506
+ message: `CLAUDE.md not a symlink to AGENTS.md (${symlinkResult.status})`,
507
+ });
508
+ }
509
+
510
+ if (opts.json) {
511
+ process.stdout.write(JSON.stringify({ repoRoot, issues }, null, 2) + '\n');
512
+ return;
513
+ }
514
+
515
+ if (issues.length === 0) {
516
+ logOk('Claude Code wiring looks complete.');
517
+ return;
518
+ }
519
+ logWarn(`${issues.length} issue(s) detected:`);
520
+ for (const issue of issues) {
521
+ const tag = issue.severity === 'error' ? '✗' : '~';
522
+ console.log(` ${tag} [${issue.kind}] ${issue.message}`);
523
+ }
524
+ if (opts.fix) {
525
+ logInfo('Running install to repair...');
526
+ runInstall(rawArgs.filter((arg) => arg !== '--fix'));
527
+ return;
528
+ }
529
+ logInfo(`Run '${SHORT_TOOL_NAME} claude install' to fix.`);
530
+ process.exitCode = 1;
531
+ }
532
+
533
+ function runUninstall(rawArgs) {
534
+ const opts = parseInstallArgs(rawArgs);
535
+ const repoRoot = resolveRepoRoot(opts.target);
536
+
537
+ if (!opts.yes) {
538
+ logWarn('Refusing to uninstall without --yes. This will remove .claude/hooks/, .claude/commands/gx-*.md, and managed settings entries.');
539
+ process.exitCode = 1;
540
+ return;
541
+ }
542
+
543
+ const removed = [];
544
+ // Remove hook files
545
+ for (const hook of MANAGED_HOOK_FILES) {
546
+ const hookPath = path.join(repoRoot, HOOKS_REL, hook);
547
+ if (fs.existsSync(hookPath)) {
548
+ if (!opts.dryRun) fs.unlinkSync(hookPath);
549
+ removed.push(`${HOOKS_REL}/${hook}`);
550
+ }
551
+ }
552
+ // Remove slash commands
553
+ for (const cmd of MANAGED_SLASH_COMMANDS) {
554
+ const cmdPath = path.join(repoRoot, COMMANDS_REL, cmd);
555
+ if (fs.existsSync(cmdPath)) {
556
+ if (!opts.dryRun) fs.unlinkSync(cmdPath);
557
+ removed.push(`${COMMANDS_REL}/${cmd}`);
558
+ }
559
+ }
560
+ // Clean managed hooks from settings.json
561
+ const settingsPath = path.join(repoRoot, SETTINGS_REL);
562
+ const settings = readJsonIfExists(settingsPath);
563
+ if (settings && settings.hooks) {
564
+ for (const eventName of Object.keys(EXPECTED_HOOK_MATCHERS)) {
565
+ const groups = settings.hooks[eventName] || [];
566
+ const filteredGroups = groups.map((group) => {
567
+ const filteredHooks = (group.hooks || []).filter((h) => {
568
+ const cmd = h.command || '';
569
+ return !EXPECTED_HOOK_MATCHERS[eventName].some((needle) => cmd.includes(needle));
570
+ });
571
+ return { ...group, hooks: filteredHooks };
572
+ }).filter((group) => (group.hooks || []).length > 0);
573
+ if (filteredGroups.length === 0) {
574
+ delete settings.hooks[eventName];
575
+ } else {
576
+ settings.hooks[eventName] = filteredGroups;
577
+ }
578
+ }
579
+ if (!opts.dryRun) writeJson(settingsPath, settings, { dryRun: false });
580
+ removed.push(`${SETTINGS_REL} (managed entries pruned)`);
581
+ }
582
+ // Remove the gx MCP server from .mcp.json (drop the file if it only held ours)
583
+ const mcpRemoval = uninstallMcpServer(repoRoot, opts);
584
+ if (mcpRemoval.status !== 'absent') {
585
+ removed.push(`${MCP_REL} (${mcpRemoval.status === 'removed' ? 'removed' : `'${MCP_SERVER_KEY}' server pruned`})`);
586
+ }
587
+
588
+ logOk(`Removed ${removed.length} item(s)${opts.dryRun ? ' (dry-run)' : ''}.`);
589
+ for (const r of removed) console.log(` - ${r}`);
590
+ }
591
+
592
+ function parseInstallArgs(rawArgs) {
593
+ const opts = {
594
+ target: process.cwd(),
595
+ force: false,
596
+ dryRun: false,
597
+ json: false,
598
+ yes: false,
599
+ fix: false,
600
+ noMcp: false,
601
+ };
602
+ for (let index = 0; index < rawArgs.length; index += 1) {
603
+ const arg = rawArgs[index];
604
+ if (arg === '--target') { opts.target = rawArgs[++index]; continue; }
605
+ if (arg === '--force') { opts.force = true; continue; }
606
+ if (arg === '--dry-run') { opts.dryRun = true; continue; }
607
+ if (arg === '--json') { opts.json = true; continue; }
608
+ if (arg === '--yes' || arg === '-y') { opts.yes = true; continue; }
609
+ if (arg === '--fix') { opts.fix = true; continue; }
610
+ if (arg === '--no-mcp') { opts.noMcp = true; continue; }
611
+ }
612
+ return opts;
613
+ }
614
+
615
+ function printUsage() {
616
+ console.log(`Usage: ${SHORT_TOOL_NAME} claude <subcommand> [flags]
617
+
618
+ Subcommands:
619
+ install install/update .claude/settings.json + hooks + slash commands + .mcp.json.
620
+ check diagnose Claude Code wiring (read-only by default).
621
+ doctor alias: 'check --fix'.
622
+ uninstall remove gitguardex-managed Claude Code wiring (--yes required).
623
+
624
+ Flags:
625
+ --target <path> Operate in a different repo directory.
626
+ --force Overwrite existing managed entries instead of merging.
627
+ --no-mcp Skip registering the gx MCP server in .mcp.json.
628
+ --dry-run Report what would change without writing.
629
+ --json Emit JSON output.
630
+ --yes / -y Required for uninstall.
631
+ --fix For 'check': run install after diagnosing.
632
+ `);
633
+ }
634
+
635
+ function claude(rawArgs) {
636
+ const [subRaw, ...rest] = Array.isArray(rawArgs) ? rawArgs : [];
637
+ if (subRaw === '-h' || subRaw === '--help' || subRaw === 'help') {
638
+ printUsage();
639
+ return;
640
+ }
641
+ const sub = subRaw || 'check';
642
+ try {
643
+ if (sub === 'install') return runInstall(rest);
644
+ if (sub === 'check') return runCheck(rest);
645
+ if (sub === 'doctor') return runCheck([...rest, '--fix']);
646
+ if (sub === 'uninstall') return runUninstall(rest);
647
+ } catch (error) {
648
+ logError(error && error.message ? error.message : String(error));
649
+ process.exitCode = 1;
650
+ return;
651
+ }
652
+ logError(`Unknown 'claude' subcommand: ${sub}`);
653
+ printUsage();
654
+ process.exitCode = 64;
655
+ }
656
+
657
+ module.exports = {
658
+ claude,
659
+ installSettings,
660
+ mergeSettings,
661
+ mergeHookGroupArrays,
662
+ ensureSpeckitMarkers,
663
+ installHooks,
664
+ installSlashCommands,
665
+ installMcpServer,
666
+ uninstallMcpServer,
667
+ mcpServerSpec,
668
+ MANAGED_HOOK_FILES,
669
+ MANAGED_SLASH_COMMANDS,
670
+ MCP_REL,
671
+ MCP_SERVER_KEY,
672
+ TEMPLATE_DEFAULT_SETTINGS,
673
+ EXPECTED_HOOK_MATCHERS,
674
+ };