groove-dev 0.27.61 → 0.27.63

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/CLAUDE.md CHANGED
@@ -263,3 +263,10 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
263
263
  - Dashboard: routing donut, cache panel, context health gauges
264
264
  - Monitor/QC agent mode (stay active, loop)
265
265
  - Distribution: demo video, HN launch, Twitter content
266
+
267
+ <!-- GROOVE:START -->
268
+ ## GROOVE Orchestration (auto-injected)
269
+ Active agents: 0
270
+ See AGENTS_REGISTRY.md for full agent state.
271
+ **Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
272
+ <!-- GROOVE:END -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.61",
3
+ "version": "0.27.63",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.61",
3
+ "version": "0.27.63",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -2,10 +2,10 @@
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
4
  import express from 'express';
5
- import { resolve, dirname, join, sep } from 'path';
5
+ import { resolve, dirname, join, sep, relative } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, copyFileSync, realpathSync } from 'fs';
8
- import { spawn, execFile } from 'child_process';
8
+ import { spawn, execFile, execFileSync } from 'child_process';
9
9
  import { createHash, randomUUID } from 'crypto';
10
10
  import { hostname, networkInterfaces, homedir } from 'os';
11
11
  import { lookup as mimeLookup } from './mimetypes.js';
@@ -2914,6 +2914,18 @@ Keep responses concise. Help them think, don't lecture them about the system the
2914
2914
  console.log(`[Groove] Project directory: ${projectWorkingDir}`);
2915
2915
  }
2916
2916
 
2917
+ function normalizeScope(patterns, baseDir) {
2918
+ if (!patterns || !Array.isArray(patterns)) return patterns;
2919
+ return patterns.map((p) => {
2920
+ if (typeof p === 'string' && p.startsWith('/')) {
2921
+ const rel = relative(baseDir, p);
2922
+ if (!rel.startsWith('..')) return rel;
2923
+ return p.slice(1);
2924
+ }
2925
+ return p;
2926
+ });
2927
+ }
2928
+
2917
2929
  // Separate phase 1 (builders) and phase 2 (QC/finisher)
2918
2930
  const phase1 = agentConfigs.filter((a) => !a.phase || a.phase === 1);
2919
2931
  let phase2 = agentConfigs.filter((a) => a.phase === 2);
@@ -2973,7 +2985,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2973
2985
  // Spawn fresh with the same name/team but new prompt + full context
2974
2986
  const validated = validateAgentConfig({
2975
2987
  role: existing.role,
2976
- scope: config.scope || existing.scope || [],
2988
+ scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
2977
2989
  prompt,
2978
2990
  provider: config.provider || existing.provider || undefined,
2979
2991
  model: config.model || existing.model || daemon.config?.defaultModel || 'auto',
@@ -2995,7 +3007,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2995
3007
  try {
2996
3008
  const validated = validateAgentConfig({
2997
3009
  role: config.role,
2998
- scope: config.scope || [],
3010
+ scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
2999
3011
  prompt,
3000
3012
  provider: config.provider || undefined,
3001
3013
  model: config.model || daemon.config?.defaultModel || 'auto',
@@ -3015,6 +3027,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
3015
3027
  }
3016
3028
  }
3017
3029
 
3030
+ if (failed.length > 0) {
3031
+ console.warn(`[Groove] Team launch had ${failed.length} failure(s):`, failed.map((f) => `${f.role}: ${f.error}`).join(', '));
3032
+ }
3033
+
3018
3034
  // Phase 2 agents also scoped to projectWorkingDir
3019
3035
  if (phase2.length > 0 && phase1Ids.length > 0) {
3020
3036
  // Dedup: if a running idle fullstack already exists in this team,
@@ -4619,9 +4635,37 @@ Keep responses concise. Help them think, don't lecture them about the system the
4619
4635
  : join(base, 'venv', 'bin', 'python3');
4620
4636
  }
4621
4637
 
4638
+ let _cachedGitBash = undefined;
4639
+ function findGitBash() {
4640
+ if (_cachedGitBash !== undefined) return _cachedGitBash;
4641
+ try {
4642
+ const gitPath = execFileSync('where', ['git'], { timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] })
4643
+ .toString().trim().split('\n')[0].trim();
4644
+ // git.exe is typically at <Git>\cmd\git.exe — navigate up to Git root
4645
+ const gitDir = dirname(dirname(gitPath));
4646
+ const candidate = join(gitDir, 'bin', 'bash.exe');
4647
+ if (existsSync(candidate)) { _cachedGitBash = candidate; return _cachedGitBash; }
4648
+ } catch { /* where failed — try common paths */ }
4649
+ const fallbacks = [
4650
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
4651
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
4652
+ ];
4653
+ for (const p of fallbacks) {
4654
+ if (existsSync(p)) { _cachedGitBash = p; return _cachedGitBash; }
4655
+ }
4656
+ _cachedGitBash = null;
4657
+ return null;
4658
+ }
4659
+
4622
4660
  function spawnSetupSh(cwd) {
4623
4661
  if (IS_WIN) {
4624
- return spawn('cmd.exe', ['/c', 'bash setup.sh --json'], {
4662
+ const bashPath = findGitBash();
4663
+ if (!bashPath) {
4664
+ const err = new Error('Could not find bash. Ensure Git for Windows is installed from https://git-scm.com');
4665
+ err.code = 'BASH_NOT_FOUND';
4666
+ throw err;
4667
+ }
4668
+ return spawn(bashPath, ['setup.sh', '--json'], {
4625
4669
  cwd,
4626
4670
  stdio: ['ignore', 'pipe', 'pipe'],
4627
4671
  env: { ...process.env, PYTHONUNBUFFERED: '1' },
@@ -4681,10 +4725,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
4681
4725
 
4682
4726
  app.get('/api/network/install/status', networkGate, (req, res) => {
4683
4727
  const installPath = networkRoot();
4684
- const installed = existsSync(resolve(installPath, 'setup.sh'));
4728
+ const dirExists = existsSync(installPath);
4729
+ const installed = dirExists && existsSync(resolve(installPath, 'setup.sh'));
4730
+ const stale = dirExists && !installed;
4685
4731
  res.json({
4686
4732
  installed,
4687
- path: installed ? installPath : null,
4733
+ stale,
4734
+ path: dirExists ? installPath : null,
4688
4735
  version: installed ? getInstalledNetworkVersion() : null,
4689
4736
  });
4690
4737
  });
@@ -4702,9 +4749,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
4702
4749
  return res.status(500).json({ error: 'Invalid install path' });
4703
4750
  }
4704
4751
 
4705
- // Refuse to clone over an existing directory avoids surprising merges.
4752
+ // If directory exists from a previous failed install, clean it up automatically.
4706
4753
  if (existsSync(installPath)) {
4707
- return res.status(400).json({ error: 'Install path already exists; uninstall first' });
4754
+ if (daemon.config?.networkBeta?.installed) {
4755
+ return res.status(400).json({ error: 'Install path already exists; uninstall first' });
4756
+ }
4757
+ try {
4758
+ rmSync(installPath, { recursive: true, force: true });
4759
+ daemon.audit?.log?.('network.install.stale-cleanup', { path: installPath });
4760
+ } catch (cleanupErr) {
4761
+ return res.status(500).json({ error: `Failed to clean stale install directory: ${cleanupErr.message}` });
4762
+ }
4708
4763
  }
4709
4764
 
4710
4765
  daemon.networkInstall = { running: true, startedAt: Date.now() };
@@ -4728,7 +4783,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4728
4783
  };
4729
4784
 
4730
4785
  try {
4731
- const pat = daemon.credentials?.getKey?.('github-pat') || null;
4786
+ const pat = daemon.credentials?.getKey?.('github') || daemon.credentials?.getKey?.('github-pat') || null;
4732
4787
 
4733
4788
  let installVersion;
4734
4789
  try {
@@ -4739,6 +4794,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
4739
4794
 
4740
4795
  broadcastInstallProgress('cloning', `Cloning network package ${installVersion}...`, 0);
4741
4796
 
4797
+ // Pre-flight: verify git is installed before attempting clone.
4798
+ const gitInstalled = await new Promise((resolveGit) => {
4799
+ execFile('git', ['--version'], { timeout: 5000 }, (err) => resolveGit(!err));
4800
+ });
4801
+ if (!gitInstalled) {
4802
+ return fail('Git is not installed. Install Git from https://git-scm.com and restart Groove.');
4803
+ }
4804
+
4742
4805
  const cloneArgs = ['clone', '--branch', installVersion, '--depth', '1', NETWORK_REPO_URL, installPath];
4743
4806
  const cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
4744
4807
  if (pat) {
@@ -4768,14 +4831,30 @@ Keep responses concise. Help them think, don't lecture them about the system the
4768
4831
  });
4769
4832
 
4770
4833
  if (cloneCode.code !== 0) {
4771
- const hint = stripCredentials(cloneErr.trim().split('\n').slice(-1)[0] || 'git clone failed');
4834
+ let hint;
4835
+ const errMsg = cloneCode.err || '';
4836
+ const lastLine = cloneErr.trim().split('\n').slice(-1)[0] || '';
4837
+ if (errMsg.includes('ENOENT')) {
4838
+ hint = 'Git is not installed. Install Git from https://git-scm.com and restart Groove.';
4839
+ } else if (/Authentication failed|could not read Username/i.test(cloneErr)) {
4840
+ hint = 'Authentication failed — run "groove set-key github-pat <token>" to set a GitHub PAT.';
4841
+ } else if (/not found/i.test(cloneErr)) {
4842
+ hint = `Repository or tag not found (${installVersion}). Check NETWORK_REPO_URL and tag.`;
4843
+ } else {
4844
+ hint = stripCredentials(lastLine || errMsg || 'git clone failed');
4845
+ }
4772
4846
  return fail(`Clone failed: ${hint}`);
4773
4847
  }
4774
4848
 
4775
4849
  broadcastInstallProgress('cloned', 'Repository cloned', 10);
4776
4850
 
4777
4851
  // Run setup.sh --json from the install directory
4778
- const setup = spawnSetupSh(installPath);
4852
+ let setup;
4853
+ try {
4854
+ setup = spawnSetupSh(installPath);
4855
+ } catch (spawnErr) {
4856
+ return fail(`Setup failed: ${spawnErr.message}`);
4857
+ }
4779
4858
 
4780
4859
  daemon.networkInstall.proc = setup;
4781
4860
 
@@ -4809,7 +4888,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
4809
4888
  });
4810
4889
 
4811
4890
  if (setupResult.code !== 0) {
4812
- const hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
4891
+ let hint;
4892
+ if (setupResult.code === -1 || setupResult.err?.includes('ENOENT')) {
4893
+ hint = 'bash not found — ensure Git for Windows is installed from https://git-scm.com';
4894
+ } else {
4895
+ hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
4896
+ }
4813
4897
  return fail(`Setup failed: ${hint}`);
4814
4898
  }
4815
4899
 
@@ -4883,9 +4967,16 @@ Keep responses concise. Help them think, don't lecture them about the system the
4883
4967
  // surface that. Uses spawn with array args — no shell interpolation.
4884
4968
  function fetchLatestNetworkTag() {
4885
4969
  return new Promise((resolvePromise) => {
4970
+ const tagEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
4971
+ const tagPat = daemon.credentials?.getKey?.('github') || daemon.credentials?.getKey?.('github-pat') || null;
4972
+ if (tagPat) {
4973
+ tagEnv.GIT_CONFIG_COUNT = '1';
4974
+ tagEnv.GIT_CONFIG_KEY_0 = 'http.extraHeader';
4975
+ tagEnv.GIT_CONFIG_VALUE_0 = `Authorization: token ${tagPat}`;
4976
+ }
4886
4977
  const proc = spawn('git', ['ls-remote', '--tags', NETWORK_REPO_URL], {
4887
4978
  stdio: ['ignore', 'pipe', 'pipe'],
4888
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
4979
+ env: tagEnv,
4889
4980
  });
4890
4981
  let stdout = '';
4891
4982
  let stderr = '';
@@ -5023,7 +5114,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
5023
5114
 
5024
5115
  broadcastUpdateProgress('deps', 'Updating dependencies...', 30);
5025
5116
 
5026
- const setup = spawnSetupSh(installPath);
5117
+ let setup;
5118
+ try {
5119
+ setup = spawnSetupSh(installPath);
5120
+ } catch (spawnErr) {
5121
+ return fail(`Setup failed: ${spawnErr.message}`);
5122
+ }
5027
5123
 
5028
5124
  daemon.networkInstall.proc = setup;
5029
5125
 
@@ -5054,7 +5150,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
5054
5150
  });
5055
5151
 
5056
5152
  if (setupResult.code !== 0) {
5057
- const hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
5153
+ let hint;
5154
+ if (setupResult.code === -1 || setupResult.err?.includes('ENOENT')) {
5155
+ hint = 'bash not found — ensure Git for Windows is installed from https://git-scm.com';
5156
+ } else {
5157
+ hint = stderrBuf.trim().split('\n').slice(-1)[0] || `setup.sh exited ${setupResult.code}`;
5158
+ }
5058
5159
  return fail(`Setup failed: ${hint}`);
5059
5160
  }
5060
5161