@imdeadpool/guardex 7.0.43 → 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 (63) hide show
  1. package/README.md +26 -0
  2. package/package.json +2 -1
  3. package/skills/gx-act/SKILL.md +82 -0
  4. package/src/agents/inspect.js +17 -4
  5. package/src/agents/launch.js +10 -1
  6. package/src/agents/status.js +9 -6
  7. package/src/budget/index.js +2 -1
  8. package/src/cli/args.js +52 -2
  9. package/src/cli/commands/agents.js +364 -0
  10. package/src/cli/commands/bootstrap.js +92 -0
  11. package/src/cli/commands/branch.js +127 -0
  12. package/src/cli/commands/claude.js +674 -0
  13. package/src/cli/commands/doctor.js +268 -0
  14. package/src/cli/commands/finish.js +26 -0
  15. package/src/cli/commands/mcp.js +122 -0
  16. package/src/cli/commands/misc.js +304 -0
  17. package/src/cli/commands/pr.js +439 -0
  18. package/src/cli/commands/prompt.js +92 -0
  19. package/src/cli/commands/release.js +305 -0
  20. package/src/cli/commands/report.js +244 -0
  21. package/src/cli/commands/review.js +32 -0
  22. package/src/cli/commands/setup.js +242 -0
  23. package/src/cli/commands/status.js +338 -0
  24. package/src/cli/commands/watch.js +234 -0
  25. package/src/cli/main.js +68 -3726
  26. package/src/cli/shared/repo-env.js +161 -0
  27. package/src/cli/shared/sandbox.js +417 -0
  28. package/src/cli/shared/scaffolding.js +535 -0
  29. package/src/cli/shared/toolchain-shims.js +420 -0
  30. package/src/context.js +229 -11
  31. package/src/core/runtime.js +6 -1
  32. package/src/doctor/index.js +42 -13
  33. package/src/finish/index.js +147 -5
  34. package/src/finish/preflight.js +177 -0
  35. package/src/finish/review-gate.js +182 -0
  36. package/src/git/index.js +446 -4
  37. package/src/hooks/index.js +0 -64
  38. package/src/mcp/collect.js +370 -0
  39. package/src/mcp/server.js +157 -0
  40. package/src/output/index.js +67 -1
  41. package/src/pr-review.js +23 -0
  42. package/src/pr.js +381 -0
  43. package/src/sandbox/index.js +13 -2
  44. package/src/scaffold/agent-worktree-prep.js +213 -0
  45. package/src/scaffold/index.js +108 -10
  46. package/src/speckit/index.js +226 -0
  47. package/src/terminal/index.js +1 -76
  48. package/src/terminal/tmux.js +0 -1
  49. package/src/toolchain/index.js +20 -0
  50. package/templates/AGENTS.monorepo-apps.md +26 -0
  51. package/templates/AGENTS.multiagent-safety.md +61 -347
  52. package/templates/AGENTS.multiagent-safety.min.md +11 -0
  53. package/templates/codex/skills/gx-act/SKILL.md +82 -0
  54. package/templates/githooks/pre-commit +22 -19
  55. package/templates/scripts/agent-branch-finish.sh +8 -30
  56. package/templates/scripts/agent-branch-merge.sh +4 -1
  57. package/templates/scripts/agent-branch-start.sh +88 -3
  58. package/templates/scripts/agent-preflight.sh +31 -5
  59. package/templates/scripts/agent-worktree-prune.sh +1 -1
  60. package/templates/scripts/codex-agent.sh +0 -91
  61. package/src/agents/detect.js +0 -160
  62. package/src/cockpit/keybindings.js +0 -224
  63. package/src/cockpit/layout.js +0 -224
@@ -14,6 +14,8 @@ const {
14
14
  USER_LEVEL_SKILL_ASSETS,
15
15
  AGENTS_MARKER_START,
16
16
  AGENTS_MARKER_END,
17
+ MONOREPO_MARKER_START,
18
+ MONOREPO_MARKER_END,
17
19
  GITIGNORE_MARKER_START,
18
20
  GITIGNORE_MARKER_END,
19
21
  SHARED_VSCODE_SETTINGS_RELATIVE,
@@ -500,31 +502,54 @@ function removeLegacyManagedRepoFile(repoRoot, relativePath, options = {}) {
500
502
  return { status: dryRun ? 'would-remove' : 'removed', file: relativePath };
501
503
  }
502
504
 
503
- function ensureAgentsSnippet(repoRoot, dryRun) {
505
+ // A managed block longer than this many non-blank lines is treated as the full
506
+ // contract. The minimal block is ~8 lines and the full contract is ~171, so any
507
+ // threshold between the two is safe; 40 leaves a wide margin on both sides.
508
+ const FULL_BLOCK_LINE_THRESHOLD = 40;
509
+
510
+ function countNonBlankLines(text) {
511
+ return text.split('\n').filter((line) => line.trim().length > 0).length;
512
+ }
513
+
514
+ // Default install ships the minimal block; the full 171-line contract is opt-in
515
+ // via `options.contract` (--contract / --full). Once a repo has the full block,
516
+ // it is never silently downgraded: an existing managed block over the line
517
+ // threshold keeps refreshing from the full template even without the flag.
518
+ function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
504
519
  const agentsPath = path.join(repoRoot, 'AGENTS.md');
505
- const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
506
520
  const managedRegex = new RegExp(
507
521
  `${AGENTS_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
508
522
  'm',
509
523
  );
510
524
 
511
- if (!fs.existsSync(agentsPath)) {
525
+ const existing = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf8') : null;
526
+ const existingBlock = existing ? existing.match(managedRegex) : null;
527
+ const existingIsFull = Boolean(existingBlock)
528
+ && countNonBlankLines(existingBlock[0]) > FULL_BLOCK_LINE_THRESHOLD;
529
+ const wantFull = Boolean(options.contract) || existingIsFull;
530
+
531
+ const templateFile = wantFull
532
+ ? 'AGENTS.multiagent-safety.md'
533
+ : 'AGENTS.multiagent-safety.min.md';
534
+ const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, templateFile), 'utf8').trimEnd();
535
+ const variant = wantFull ? 'full contract block' : 'minimal block';
536
+
537
+ if (existing == null) {
512
538
  if (!dryRun) {
513
539
  fs.writeFileSync(agentsPath, `# AGENTS\n\n${snippet}\n`, 'utf8');
514
540
  }
515
- return { status: 'created', file: 'AGENTS.md' };
541
+ return { status: 'created', file: 'AGENTS.md', note: variant };
516
542
  }
517
543
 
518
- const existing = fs.readFileSync(agentsPath, 'utf8');
519
- if (managedRegex.test(existing)) {
520
- const next = existing.replace(managedRegex, snippet);
544
+ if (existingBlock) {
545
+ const next = existing.replace(managedRegex, () => snippet);
521
546
  if (next === existing) {
522
- return { status: 'unchanged', file: 'AGENTS.md' };
547
+ return { status: 'unchanged', file: 'AGENTS.md', note: variant };
523
548
  }
524
549
  if (!dryRun) {
525
550
  fs.writeFileSync(agentsPath, next, 'utf8');
526
551
  }
527
- return { status: 'updated', file: 'AGENTS.md', note: 'refreshed gitguardex-managed block' };
552
+ return { status: 'updated', file: 'AGENTS.md', note: `refreshed gitguardex-managed block (${variant})` };
528
553
  }
529
554
 
530
555
  if (existing.includes(AGENTS_MARKER_START)) {
@@ -536,7 +561,7 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
536
561
  fs.writeFileSync(agentsPath, `${existing}${separator}${snippet}\n`, 'utf8');
537
562
  }
538
563
 
539
- return { status: 'updated', file: 'AGENTS.md' };
564
+ return { status: 'updated', file: 'AGENTS.md', note: variant };
540
565
  }
541
566
 
542
567
  function ensureClaudeAgentsLink(repoRoot, dryRun) {
@@ -557,6 +582,77 @@ function ensureClaudeAgentsLink(repoRoot, dryRun) {
557
582
  return { status: dryRun ? 'would-create' : 'created', file: 'CLAUDE.md', note: 'symlink to AGENTS.md' };
558
583
  }
559
584
 
585
+ function detectMonorepoApps(repoRoot) {
586
+ const appsDir = path.join(repoRoot, 'apps');
587
+ let stat;
588
+ try {
589
+ stat = fs.statSync(appsDir);
590
+ } catch {
591
+ return false;
592
+ }
593
+ if (!stat.isDirectory()) return false;
594
+ let entries;
595
+ try {
596
+ entries = fs.readdirSync(appsDir, { withFileTypes: true });
597
+ } catch {
598
+ return false;
599
+ }
600
+ return entries.some(
601
+ (entry) =>
602
+ entry.isDirectory() &&
603
+ fs.existsSync(path.join(appsDir, entry.name, 'package.json')),
604
+ );
605
+ }
606
+
607
+ function ensureMonorepoAppsSnippet(repoRoot, dryRun) {
608
+ const agentsPath = path.join(repoRoot, 'AGENTS.md');
609
+ if (!detectMonorepoApps(repoRoot)) {
610
+ return {
611
+ status: 'skipped',
612
+ file: 'AGENTS.md',
613
+ note: 'no apps/<pkg>/package.json detected — monorepo block not needed',
614
+ };
615
+ }
616
+ const snippet = fs
617
+ .readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.monorepo-apps.md'), 'utf8')
618
+ .trimEnd();
619
+ const managedRegex = new RegExp(
620
+ `${MONOREPO_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${MONOREPO_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
621
+ 'm',
622
+ );
623
+
624
+ // Ensure AGENTS.md exists first (created by ensureAgentsSnippet upstream).
625
+ if (!fs.existsSync(agentsPath)) {
626
+ if (!dryRun) {
627
+ fs.writeFileSync(agentsPath, `# AGENTS\n\n${snippet}\n`, 'utf8');
628
+ }
629
+ return { status: 'created', file: 'AGENTS.md', note: 'monorepo-apps block' };
630
+ }
631
+
632
+ const existing = fs.readFileSync(agentsPath, 'utf8');
633
+ if (managedRegex.test(existing)) {
634
+ const next = existing.replace(managedRegex, snippet);
635
+ if (next === existing) {
636
+ return { status: 'unchanged', file: 'AGENTS.md', note: 'monorepo-apps block' };
637
+ }
638
+ if (!dryRun) {
639
+ fs.writeFileSync(agentsPath, next, 'utf8');
640
+ }
641
+ return {
642
+ status: 'updated',
643
+ file: 'AGENTS.md',
644
+ note: 'refreshed monorepo-apps block',
645
+ };
646
+ }
647
+
648
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
649
+ if (!dryRun) {
650
+ fs.writeFileSync(agentsPath, `${existing}${separator}${snippet}\n`, 'utf8');
651
+ }
652
+
653
+ return { status: 'updated', file: 'AGENTS.md', note: 'appended monorepo-apps block' };
654
+ }
655
+
560
656
  function ensureManagedGitignore(repoRoot, dryRun) {
561
657
  const gitignorePath = path.join(repoRoot, '.gitignore');
562
658
  const managedBlock = [
@@ -782,6 +878,8 @@ module.exports = {
782
878
  removeLegacyManagedRepoFile,
783
879
  ensureAgentsSnippet,
784
880
  ensureClaudeAgentsLink,
881
+ ensureMonorepoAppsSnippet,
882
+ detectMonorepoApps,
785
883
  ensureManagedGitignore,
786
884
  parseJsonObjectLikeFile,
787
885
  buildRepoVscodeSettings,
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const cp = require('node:child_process');
6
+
7
+ const { TOOL_NAME, SHORT_TOOL_NAME } = require('../context');
8
+
9
+ const SPECIFY_BIN = 'specify';
10
+ const SPECKIT_INSTALL_HINT =
11
+ 'uv tool install specify-cli --from git+https://github.com/github/spec-kit.git';
12
+
13
+ function whichSpecify() {
14
+ const result = cp.spawnSync(process.platform === 'win32' ? 'where' : 'which', [SPECIFY_BIN], {
15
+ encoding: 'utf8',
16
+ });
17
+ if (result.status === 0 && result.stdout && result.stdout.trim()) {
18
+ return result.stdout.trim().split(/\r?\n/)[0];
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function specifyVersion(specifyPath) {
24
+ const result = cp.spawnSync(specifyPath, ['--version'], { encoding: 'utf8' });
25
+ if (result.status === 0 && result.stdout) {
26
+ return result.stdout.trim();
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function isGitRepo(target) {
32
+ try {
33
+ const result = cp.spawnSync('git', ['-C', target, 'rev-parse', '--git-dir'], {
34
+ encoding: 'utf8',
35
+ });
36
+ return result.status === 0;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ function listSpecKitOpenSpecScaffolds(target) {
43
+ const planRoot = path.join(target, 'openspec', 'plan');
44
+ const changesRoot = path.join(target, 'openspec', 'changes');
45
+ const result = { planDirs: [], specsDirs: [] };
46
+ if (fs.existsSync(planRoot) && fs.statSync(planRoot).isDirectory()) {
47
+ for (const entry of fs.readdirSync(planRoot)) {
48
+ if (/^agent-.*-masterplan-setup-spec-kit-/i.test(entry)) {
49
+ result.planDirs.push(path.join(planRoot, entry));
50
+ }
51
+ }
52
+ }
53
+ if (fs.existsSync(changesRoot) && fs.statSync(changesRoot).isDirectory()) {
54
+ for (const entry of fs.readdirSync(changesRoot)) {
55
+ if (/^agent-.*-setup-spec-kit-/i.test(entry)) {
56
+ const specsDir = path.join(changesRoot, entry, 'specs');
57
+ if (fs.existsSync(specsDir)) result.specsDirs.push(specsDir);
58
+ }
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+
64
+ function pruneSpecKitScaffolds(target, { dryRun, logger }) {
65
+ const found = listSpecKitOpenSpecScaffolds(target);
66
+ const removed = [];
67
+ for (const dir of [...found.planDirs, ...found.specsDirs]) {
68
+ if (dryRun) {
69
+ logger(`[${TOOL_NAME}] dry-run: would prune ${path.relative(target, dir)}`);
70
+ } else {
71
+ fs.rmSync(dir, { recursive: true, force: true });
72
+ logger(`[${TOOL_NAME}] pruned ${path.relative(target, dir)}`);
73
+ }
74
+ removed.push(dir);
75
+ }
76
+ return removed;
77
+ }
78
+
79
+ function runSpecifyInit(target, { dryRun, logger }) {
80
+ const args = ['init', '--here', '--ai', 'claude', '--force', '--ignore-agent-tools'];
81
+ if (dryRun) {
82
+ logger(`[${TOOL_NAME}] dry-run: would run \`${SPECIFY_BIN} ${args.join(' ')}\` in ${target}`);
83
+ return { status: 'dry-run' };
84
+ }
85
+ const result = cp.spawnSync(SPECIFY_BIN, args, {
86
+ cwd: target,
87
+ stdio: 'inherit',
88
+ });
89
+ if (result.status !== 0) {
90
+ throw new Error(`${SPECIFY_BIN} init exited with status ${result.status}`);
91
+ }
92
+ return { status: 'ok' };
93
+ }
94
+
95
+ function isSpecKitAlreadyInstalled(target) {
96
+ return fs.existsSync(path.join(target, '.specify', 'integration.json'));
97
+ }
98
+
99
+ function installSpeckit({
100
+ target = process.cwd(),
101
+ dryRun = false,
102
+ prune = true,
103
+ force = false,
104
+ silent = false,
105
+ logger = console.log,
106
+ }) {
107
+ const resolved = path.resolve(target);
108
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
109
+ if (silent) {
110
+ logger(`[${TOOL_NAME}] ⚠️ speckit: target ${resolved} does not exist; skipping.`);
111
+ return { status: 'skipped', reason: 'no-target' };
112
+ }
113
+ throw new Error(`Target directory does not exist: ${resolved}`);
114
+ }
115
+ if (!isGitRepo(resolved)) {
116
+ logger(
117
+ `[${TOOL_NAME}] ⚠️ ${resolved} is not a git repo. Spec Kit will scaffold without git extension wiring.`,
118
+ );
119
+ }
120
+ if (!force && isSpecKitAlreadyInstalled(resolved)) {
121
+ logger(`[${TOOL_NAME}] ✅ Spec Kit already installed at ${resolved}/.specify (use --speckit-force to reinstall).`);
122
+ return { status: 'already-installed', target: resolved };
123
+ }
124
+ const specifyPath = whichSpecify();
125
+ if (!specifyPath) {
126
+ if (silent) {
127
+ logger(
128
+ `[${TOOL_NAME}] ⚠️ speckit: \`${SPECIFY_BIN}\` not on PATH; skipping speckit install. ` +
129
+ `Install with: ${SPECKIT_INSTALL_HINT}`,
130
+ );
131
+ return { status: 'skipped', reason: 'specify-missing' };
132
+ }
133
+ throw new Error(
134
+ `${SPECIFY_BIN} CLI not found on PATH. Install with:\n ${SPECKIT_INSTALL_HINT}`,
135
+ );
136
+ }
137
+ const version = specifyVersion(specifyPath);
138
+ logger(`[${TOOL_NAME}] specify-cli: ${specifyPath}${version ? ` (${version})` : ''}`);
139
+ logger(`[${TOOL_NAME}] Running \`${SPECIFY_BIN} init --here --ai claude --force\` in ${resolved}`);
140
+
141
+ const initResult = runSpecifyInit(resolved, { dryRun, logger });
142
+
143
+ let pruned = [];
144
+ if (prune && (initResult.status === 'ok' || dryRun)) {
145
+ pruned = pruneSpecKitScaffolds(resolved, { dryRun, logger });
146
+ }
147
+
148
+ logger('');
149
+ logger(`[${TOOL_NAME}] ✅ Spec Kit installed. Next:`);
150
+ logger(` - Start a fresh Claude session at ${resolved}`);
151
+ logger(` - Use slash skills: /speckit-constitution, /speckit-specify, /speckit-plan, /speckit-tasks, /speckit-implement`);
152
+ logger(` - Agent worktree flow is unchanged — run \`${SHORT_TOOL_NAME} pivot "<task>" "claude"\` to start work.`);
153
+
154
+ return { status: 'installed', specifyPath, version, dryRun, prunedScaffolds: pruned, target: resolved };
155
+ }
156
+
157
+ function printSpeckitHelp() {
158
+ const lines = [
159
+ `Usage: ${SHORT_TOOL_NAME} speckit [options]`,
160
+ '',
161
+ ' Install Spec Kit (specify-cli) SDD slash skills into the current repo.',
162
+ ' Runs `specify init --here --ai claude --force --ignore-agent-tools` and',
163
+ ' prunes the heavy auto-generated openspec/plan + specs/ scaffolds the',
164
+ ' specify-cli emits, so it composes cleanly with the existing gx workflow.',
165
+ '',
166
+ 'Options:',
167
+ ' --target <path> Run in <path> instead of cwd',
168
+ ' --no-prune Keep the auto-generated openspec/plan + specs scaffolds',
169
+ ' --dry-run Print actions without modifying files',
170
+ ' -h, --help Show this help',
171
+ '',
172
+ 'Prerequisite:',
173
+ ` ${SPECIFY_BIN} CLI on PATH. Install with:`,
174
+ ` ${SPECKIT_INSTALL_HINT}`,
175
+ ];
176
+ console.log(lines.join('\n'));
177
+ }
178
+
179
+ function runSpeckitCommand(rawArgs) {
180
+ const args = Array.isArray(rawArgs) ? [...rawArgs] : [];
181
+ let target = process.cwd();
182
+ let prune = true;
183
+ let dryRun = false;
184
+ let force = false;
185
+
186
+ while (args.length > 0) {
187
+ const arg = args.shift();
188
+ if (arg === '-h' || arg === '--help' || arg === 'help') {
189
+ printSpeckitHelp();
190
+ return;
191
+ }
192
+ if (arg === '--target') {
193
+ const next = args.shift();
194
+ if (!next) throw new Error('--target requires a path value');
195
+ target = next;
196
+ continue;
197
+ }
198
+ if (arg === '--no-prune') {
199
+ prune = false;
200
+ continue;
201
+ }
202
+ if (arg === '--prune') {
203
+ prune = true;
204
+ continue;
205
+ }
206
+ if (arg === '--dry-run') {
207
+ dryRun = true;
208
+ continue;
209
+ }
210
+ if (arg === '--force' || arg === '--reinstall') {
211
+ force = true;
212
+ continue;
213
+ }
214
+ throw new Error(`Unknown option: ${arg}`);
215
+ }
216
+
217
+ installSpeckit({ target, prune, dryRun, force });
218
+ }
219
+
220
+ module.exports = {
221
+ runSpeckitCommand,
222
+ installSpeckit,
223
+ pruneSpecKitScaffolds,
224
+ whichSpecify,
225
+ isSpecKitAlreadyInstalled,
226
+ };
@@ -14,6 +14,7 @@ function normalizeBackendName(value, fallback = DEFAULT_BACKEND) {
14
14
  return normalized;
15
15
  }
16
16
 
17
+ // Internal: build the kitty/tmux backend pair consumed by selectTerminalBackend.
17
18
  function createBackends(options = {}) {
18
19
  return {
19
20
  kitty: options.kittyBackend || kitty.createBackend(options.kitty || {}),
@@ -21,73 +22,6 @@ function createBackends(options = {}) {
21
22
  };
22
23
  }
23
24
 
24
- function firstText(...values) {
25
- for (const value of values) {
26
- if (typeof value === 'string' && value.trim().length > 0) return value.trim();
27
- }
28
- return '';
29
- }
30
-
31
- function metadataOf(target = {}) {
32
- return target.metadata && typeof target.metadata === 'object' ? target.metadata : {};
33
- }
34
-
35
- function terminalOf(target = {}) {
36
- return target.terminal && typeof target.terminal === 'object' ? target.terminal : {};
37
- }
38
-
39
- function tmuxOf(target = {}) {
40
- return target.tmux && typeof target.tmux === 'object' ? target.tmux : {};
41
- }
42
-
43
- function kittyOf(target = {}) {
44
- return target.kitty && typeof target.kitty === 'object' ? target.kitty : {};
45
- }
46
-
47
- function resolveTargetBackendName(target = {}, fallback = '') {
48
- const metadata = metadataOf(target);
49
- const terminal = terminalOf(target);
50
- const explicit = firstText(
51
- target.terminalBackend,
52
- target.backend,
53
- terminal.backend,
54
- metadata.terminalBackend,
55
- metadata['terminal.backend'],
56
- );
57
- if (explicit) return normalizeBackendName(explicit);
58
-
59
- const tmux = tmuxOf(target);
60
- if (firstText(target.paneId, target.tmuxPaneId, target.tmuxTarget, tmux.paneId, tmux.target, metadata.tmuxPaneId, metadata['tmux.paneId'])) {
61
- return 'tmux';
62
- }
63
-
64
- const kittyTarget = kittyOf(target);
65
- if (firstText(
66
- target.kittyMatch,
67
- target.match,
68
- target.kittyWindowId,
69
- target.windowId,
70
- target.kittyTitle,
71
- target.windowTitle,
72
- terminal.match,
73
- terminal.windowId,
74
- terminal.title,
75
- kittyTarget.match,
76
- kittyTarget.windowId,
77
- kittyTarget.title,
78
- metadata.kittyMatch,
79
- metadata['kitty.match'],
80
- metadata.kittyWindowId,
81
- metadata['kitty.windowId'],
82
- metadata.kittyTitle,
83
- metadata['kitty.title'],
84
- )) {
85
- return 'kitty';
86
- }
87
-
88
- return fallback ? normalizeBackendName(fallback) : '';
89
- }
90
-
91
25
  function selectTerminalBackend(value = DEFAULT_BACKEND, options = {}) {
92
26
  const name = normalizeBackendName(value);
93
27
  const backends = createBackends(options);
@@ -102,19 +36,10 @@ function selectTerminalBackend(value = DEFAULT_BACKEND, options = {}) {
102
36
  return backends[name];
103
37
  }
104
38
 
105
- function selectTerminalBackendForTarget(target = {}, options = {}) {
106
- const name = resolveTargetBackendName(target, options.defaultBackend);
107
- if (!name) return null;
108
- return selectTerminalBackend(name, options);
109
- }
110
-
111
39
  module.exports = {
112
40
  DEFAULT_BACKEND,
113
41
  normalizeBackendName,
114
- resolveTargetBackendName,
115
42
  selectTerminalBackend,
116
- selectTerminalBackendForTarget,
117
- createBackends,
118
43
  kitty,
119
44
  tmux,
120
45
  };
@@ -122,5 +122,4 @@ function createBackend(options = {}) {
122
122
 
123
123
  module.exports = {
124
124
  createBackend,
125
- targetId,
126
125
  };
@@ -293,7 +293,24 @@ function buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools)
293
293
  return `${dependencyPrefix}Install missing companion tools now? (${installCommands.join(' && ')})`;
294
294
  }
295
295
 
296
+ // Process-scoped memo for the slow `npm list -g` probe (~1.6-2.4s). Detection is
297
+ // invariant within one gx invocation, but the bare-`gx`/`gx status` path queries
298
+ // it twice (self-update check + status snapshot). Busted after a global install.
299
+ let globalToolchainDetectionCache = null;
300
+
301
+ function resetGlobalToolchainDetectionCache() {
302
+ globalToolchainDetectionCache = null;
303
+ }
304
+
296
305
  function detectGlobalToolchainPackages() {
306
+ if (globalToolchainDetectionCache) {
307
+ return globalToolchainDetectionCache;
308
+ }
309
+ globalToolchainDetectionCache = computeGlobalToolchainPackages();
310
+ return globalToolchainDetectionCache;
311
+ }
312
+
313
+ function computeGlobalToolchainPackages() {
297
314
  const result = run(NPM_BIN, ['list', '-g', '--depth=0', '--json']);
298
315
  if (result.status !== 0) {
299
316
  const stderr = (result.stderr || '').trim();
@@ -563,6 +580,8 @@ function performCompanionInstall(missingPackages, missingLocalTools) {
563
580
  };
564
581
  }
565
582
  installed.push(...missingPackages);
583
+ // Global package set changed; drop the memo so any later detection re-probes.
584
+ resetGlobalToolchainDetectionCache();
566
585
  }
567
586
 
568
587
  for (const tool of missingLocalTools) {
@@ -599,6 +618,7 @@ module.exports = {
599
618
  describeCompanionInstallCommands,
600
619
  buildMissingCompanionInstallPrompt,
601
620
  detectGlobalToolchainPackages,
621
+ resetGlobalToolchainDetectionCache,
602
622
  detectRequiredSystemTools,
603
623
  detectOptionalLocalCompanionTools,
604
624
  askGlobalInstallForMissing,
@@ -0,0 +1,26 @@
1
+ <!-- monorepo-apps:START -->
2
+ ## Monorepo workflow (`apps/*`)
3
+
4
+ This repo has `apps/*` (storefront, backend, etc.). The **root worktree is kept on the protected base branch** so the user can keep `pnpm <app>:dev` running there and see merged-to-main state in real time. Never edit or commit on the root worktree.
5
+
6
+ ### Per-task loop
7
+
8
+ 1. **Start in a sibling worktree.** Run `gx pivot` (auto) or `gx branch start --type <kind> --task <slug>` — both spawn a worktree under `.omx/agent-worktrees/` on a fresh `agent/*` branch.
9
+ 2. **Run scoped dev servers from your worktree**, e.g. `pnpm --filter storefront dev` from `.omx/agent-worktrees/<your>/`. Pick a non-conflicting port if the user is already running the root.
10
+ 3. **Commit + push** as you go — the agent branch tracks `origin/agent/*`. The user can watch your branch live in their git client.
11
+ 4. **Ship via PR.** When the user approves the work, run `gx ship` (alias for `gx finish --via-pr --wait-for-merge --cleanup`). This: opens a PR → auto-merges to the protected base → prunes the worktree + branch.
12
+ 5. The user's root worktree is now showing the merged result on next pull.
13
+
14
+ ### Cross-app guardrails
15
+
16
+ - Edits to **both** `apps/storefront` AND `apps/backend` in one branch → split into two PRs unless they must land atomically. Reviews stay clean, rollbacks stay surgical.
17
+ - Edits to root configs (`pnpm-workspace.yaml`, `turbo.json`, `package.json`) lock every other agent. Claim → change → release fast.
18
+ - Migrations under `apps/backend/src/migrations/*` require explicit user OK before commit — they're irreversible on prod.
19
+ - Don't `pnpm install` from the root unless the user asks; do it inside your worktree if you added a dep.
20
+
21
+ ### What the user sees
22
+
23
+ - `git log --all --graph --oneline` shows every active agent branch in real time.
24
+ - `gx status` lists active worktrees + their branches.
25
+ - Each `gx ship` produces a PR — link goes in the user's GitHub notifications.
26
+ <!-- monorepo-apps:END -->