@soleri/core 9.15.0 → 9.16.7
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/data/flows/deliver.flow.yaml +11 -0
- package/data/flows/design.flow.yaml +4 -14
- package/data/flows/enhance.flow.yaml +10 -0
- package/data/flows/explore.flow.yaml +16 -0
- package/data/flows/fix.flow.yaml +1 -1
- package/data/flows/review.flow.yaml +13 -4
- package/dist/capabilities/chain-mapping.d.ts.map +1 -1
- package/dist/capabilities/chain-mapping.js +5 -4
- package/dist/capabilities/chain-mapping.js.map +1 -1
- package/dist/capabilities/registry.d.ts +6 -0
- package/dist/capabilities/registry.d.ts.map +1 -1
- package/dist/capabilities/registry.js +3 -2
- package/dist/capabilities/registry.js.map +1 -1
- package/dist/context/context-engine.js +1 -1
- package/dist/context/context-engine.js.map +1 -1
- package/dist/engine/core-ops.d.ts.map +1 -1
- package/dist/engine/core-ops.js +38 -1
- package/dist/engine/core-ops.js.map +1 -1
- package/dist/flows/epilogue.d.ts +5 -1
- package/dist/flows/epilogue.d.ts.map +1 -1
- package/dist/flows/epilogue.js +11 -3
- package/dist/flows/epilogue.js.map +1 -1
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +13 -5
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +1 -2
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +1 -0
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/plan-builder.d.ts +17 -1
- package/dist/flows/plan-builder.d.ts.map +1 -1
- package/dist/flows/plan-builder.js +67 -6
- package/dist/flows/plan-builder.js.map +1 -1
- package/dist/flows/probes.d.ts +1 -1
- package/dist/flows/probes.d.ts.map +1 -1
- package/dist/flows/probes.js +15 -3
- package/dist/flows/probes.js.map +1 -1
- package/dist/flows/types.d.ts +31 -4
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/flows/types.js +6 -1
- package/dist/flows/types.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/packs/pack-installer.d.ts.map +1 -1
- package/dist/packs/pack-installer.js +28 -2
- package/dist/packs/pack-installer.js.map +1 -1
- package/dist/planning/planner-types.d.ts +2 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +1 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +7 -0
- package/dist/planning/planner.js.map +1 -1
- package/dist/playbooks/playbook-executor.d.ts +10 -1
- package/dist/playbooks/playbook-executor.d.ts.map +1 -1
- package/dist/playbooks/playbook-executor.js +8 -2
- package/dist/playbooks/playbook-executor.js.map +1 -1
- package/dist/playbooks/playbook-types.d.ts +8 -0
- package/dist/playbooks/playbook-types.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +30 -0
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +60 -21
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts +11 -0
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +87 -17
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +38 -12
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +16 -4
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/context-facade.d.ts.map +1 -1
- package/dist/runtime/facades/context-facade.js +9 -3
- package/dist/runtime/facades/context-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +20 -7
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +12 -0
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +113 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +24 -3
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts +21 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +132 -38
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/schema-helpers.d.ts.map +1 -1
- package/dist/runtime/schema-helpers.js +4 -0
- package/dist/runtime/schema-helpers.js.map +1 -1
- package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.js +16 -3
- package/dist/runtime/vault-linking-ops.js.map +1 -1
- package/dist/scheduler/cron-validator.d.ts +15 -0
- package/dist/scheduler/cron-validator.d.ts.map +1 -0
- package/dist/scheduler/cron-validator.js +93 -0
- package/dist/scheduler/cron-validator.js.map +1 -0
- package/dist/scheduler/platform-linux.d.ts +14 -0
- package/dist/scheduler/platform-linux.d.ts.map +1 -0
- package/dist/scheduler/platform-linux.js +107 -0
- package/dist/scheduler/platform-linux.js.map +1 -0
- package/dist/scheduler/platform-macos.d.ts +15 -0
- package/dist/scheduler/platform-macos.d.ts.map +1 -0
- package/dist/scheduler/platform-macos.js +131 -0
- package/dist/scheduler/platform-macos.js.map +1 -0
- package/dist/scheduler/scheduler-ops.d.ts +14 -0
- package/dist/scheduler/scheduler-ops.d.ts.map +1 -0
- package/dist/scheduler/scheduler-ops.js +77 -0
- package/dist/scheduler/scheduler-ops.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -0
- package/dist/scheduler/scheduler.js +144 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/types.d.ts +48 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/scheduler/types.js +6 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +11 -0
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +132 -38
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/utils/worktree-reaper.d.ts +38 -0
- package/dist/utils/worktree-reaper.d.ts.map +1 -0
- package/dist/utils/worktree-reaper.js +85 -0
- package/dist/utils/worktree-reaper.js.map +1 -0
- package/dist/vault/scope-detector.d.ts.map +1 -1
- package/dist/vault/scope-detector.js +37 -4
- package/dist/vault/scope-detector.js.map +1 -1
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +3 -1
- package/dist/vault/vault-entries.js.map +1 -1
- package/package.json +1 -1
- package/src/agency/agency-manager.test.ts +4 -4
- package/src/agency/default-rules.test.ts +0 -13
- package/src/brain/brain-intelligence.test.ts +0 -5
- package/src/brain/second-brain-features.test.ts +2 -14
- package/src/capabilities/chain-mapping.test.ts +1 -6
- package/src/capabilities/chain-mapping.ts +6 -4
- package/src/capabilities/registry.test.ts +1 -1
- package/src/capabilities/registry.ts +9 -2
- package/src/chat/agent-loop.test.ts +1 -1
- package/src/chat/chat-enhanced.test.ts +0 -8
- package/src/claudemd/compose.test.ts +0 -5
- package/src/context/context-engine.test.ts +0 -1
- package/src/context/context-engine.ts +1 -1
- package/src/control/intent-router.test.ts +2 -2
- package/src/curator/tag-manager.test.ts +0 -4
- package/src/domain-packs/types.test.ts +0 -5
- package/src/dream/dream.test.ts +0 -7
- package/src/enforcement/registry.test.ts +2 -2
- package/src/engine/core-ops.test.ts +4 -22
- package/src/engine/core-ops.ts +36 -1
- package/src/engine/module-manifest.test.ts +1 -31
- package/src/engine/register-engine.test.ts +3 -33
- package/src/errors/retry.test.ts +3 -1
- package/src/flows/chain-runner.test.ts +0 -6
- package/src/flows/context-router.test.ts +3 -3
- package/src/flows/epilogue.test.ts +40 -2
- package/src/flows/epilogue.ts +11 -2
- package/src/flows/executor.test.ts +48 -2
- package/src/flows/executor.ts +15 -5
- package/src/flows/index.ts +1 -3
- package/src/flows/plan-builder.test.ts +201 -0
- package/src/flows/plan-builder.ts +81 -5
- package/src/flows/probes.ts +17 -3
- package/src/flows/types.ts +31 -2
- package/src/health/health-registry.test.ts +3 -1
- package/src/index.ts +17 -0
- package/src/intake/dedup-gate.test.ts +2 -6
- package/src/intake/text-ingester.test.ts +3 -4
- package/src/llm/llm-client.test.ts +1 -1
- package/src/llm/utils.test.ts +1 -1
- package/src/migrations/migration-runner.test.ts +0 -1
- package/src/operator/operator-context-store.test.ts +0 -13
- package/src/operator/operator-profile.test.ts +2 -20
- package/src/packs/pack-installer.ts +28 -2
- package/src/packs/pack-system.test.ts +2 -2
- package/src/persona/defaults.test.ts +19 -19
- package/src/planning/gap-passes.test.ts +0 -46
- package/src/planning/gap-patterns.test.ts +0 -42
- package/src/planning/goal-ancestry.test.ts +3 -1
- package/src/planning/plan-lifecycle.test.ts +15 -7
- package/src/planning/planner-types.ts +2 -0
- package/src/planning/planner.ts +8 -0
- package/src/planning/reconciliation-engine.test.ts +3 -10
- package/src/planning/task-complexity-assessor.test.ts +0 -5
- package/src/planning/task-verifier.test.ts +3 -1
- package/src/playbooks/generic/generic-playbooks.test.ts +0 -28
- package/src/playbooks/index.test.ts +0 -55
- package/src/playbooks/playbook-executor.test.ts +76 -0
- package/src/playbooks/playbook-executor.ts +24 -3
- package/src/playbooks/playbook-types.ts +8 -0
- package/src/plugins/plugin-registry.test.ts +6 -2
- package/src/project/project-registry.test.ts +2 -0
- package/src/queue/async-infrastructure.test.ts +6 -4
- package/src/queue/job-queue.test.ts +13 -7
- package/src/runtime/admin-extra-ops.test.ts +35 -30
- package/src/runtime/admin-extra-ops.ts +30 -0
- package/src/runtime/admin-ops.test.ts +0 -4
- package/src/runtime/admin-ops.ts +63 -21
- package/src/runtime/admin-setup-ops.test.ts +185 -13
- package/src/runtime/admin-setup-ops.ts +86 -16
- package/src/runtime/archive-ops.test.ts +0 -28
- package/src/runtime/branching-ops.test.ts +0 -17
- package/src/runtime/capture-ops.test.ts +41 -16
- package/src/runtime/capture-ops.ts +78 -46
- package/src/runtime/chain-ops.test.ts +0 -21
- package/src/runtime/facades/admin-facade.test.ts +0 -34
- package/src/runtime/facades/agency-facade.test.ts +0 -39
- package/src/runtime/facades/archive-facade.test.ts +0 -43
- package/src/runtime/facades/brain-facade.test.ts +8 -99
- package/src/runtime/facades/brain-facade.ts +29 -12
- package/src/runtime/facades/branching-facade.test.ts +30 -17
- package/src/runtime/facades/chat-facade.test.ts +0 -91
- package/src/runtime/facades/chat-service-ops.test.ts +0 -24
- package/src/runtime/facades/chat-session-ops.test.ts +0 -12
- package/src/runtime/facades/chat-transport-ops.test.ts +0 -23
- package/src/runtime/facades/context-facade.test.ts +0 -17
- package/src/runtime/facades/context-facade.ts +11 -4
- package/src/runtime/facades/control-facade.test.ts +0 -30
- package/src/runtime/facades/curator-facade.test.ts +0 -33
- package/src/runtime/facades/intake-facade.test.ts +0 -33
- package/src/runtime/facades/links-facade.test.ts +0 -37
- package/src/runtime/facades/loop-facade.test.ts +0 -26
- package/src/runtime/facades/memory-facade.test.ts +0 -18
- package/src/runtime/facades/memory-facade.ts +27 -11
- package/src/runtime/facades/operator-facade.test.ts +0 -31
- package/src/runtime/facades/orchestrate-facade.test.ts +0 -21
- package/src/runtime/facades/orchestrate-facade.ts +12 -0
- package/src/runtime/facades/plan-facade.test.ts +7 -32
- package/src/runtime/facades/plan-facade.ts +137 -4
- package/src/runtime/facades/review-facade.test.ts +1 -49
- package/src/runtime/facades/sync-facade.test.ts +24 -41
- package/src/runtime/facades/tier-facade.test.ts +30 -22
- package/src/runtime/facades/vault-facade.test.ts +0 -41
- package/src/runtime/facades/vault-facade.ts +26 -3
- package/src/runtime/grading-ops.test.ts +0 -27
- package/src/runtime/intake-ops.test.ts +0 -19
- package/src/runtime/loop-ops.test.ts +0 -48
- package/src/runtime/memory-cross-project-ops.test.ts +0 -14
- package/src/runtime/memory-extra-ops.test.ts +4 -8
- package/src/runtime/orchestrate-ops.test.ts +238 -19
- package/src/runtime/orchestrate-ops.ts +166 -41
- package/src/runtime/pack-ops.test.ts +0 -26
- package/src/runtime/planning-extra-ops.test.ts +2 -14
- package/src/runtime/playbook-ops-execution.test.ts +9 -20
- package/src/runtime/playbook-ops.test.ts +4 -67
- package/src/runtime/review-ops.test.ts +0 -15
- package/src/runtime/schema-helpers.ts +4 -0
- package/src/runtime/sync-ops.test.ts +0 -18
- package/src/runtime/tier-ops.test.ts +0 -21
- package/src/runtime/vault-extra-ops.test.ts +0 -12
- package/src/runtime/vault-linking-ops.test.ts +0 -4
- package/src/runtime/vault-linking-ops.ts +26 -8
- package/src/runtime/vault-sharing-ops.test.ts +0 -9
- package/src/scheduler/cron-validator.ts +101 -0
- package/src/scheduler/platform-linux.ts +122 -0
- package/src/scheduler/platform-macos.ts +150 -0
- package/src/scheduler/scheduler-ops.ts +77 -0
- package/src/scheduler/scheduler.test.ts +247 -0
- package/src/scheduler/scheduler.ts +174 -0
- package/src/scheduler/types.ts +52 -0
- package/src/skills/__tests__/sync-skills.test.ts +6 -17
- package/src/skills/global-claude-md.test.ts +113 -0
- package/src/skills/sync-skills.ts +143 -35
- package/src/skills/validate-skills.test.ts +12 -11
- package/src/telemetry/telemetry.test.ts +1 -0
- package/src/transport/http-server.test.ts +3 -0
- package/src/transport/session-manager.test.ts +3 -1
- package/src/transport/token-auth.test.ts +6 -9
- package/src/transport/ws-server.test.ts +10 -2
- package/src/utils/worktree-reaper.ts +113 -0
- package/src/vault/__tests__/vault-characterization.test.ts +0 -108
- package/src/vault/linking.test.ts +0 -2
- package/src/vault/playbook.test.ts +4 -1
- package/src/vault/scope-detector.test.ts +3 -1
- package/src/vault/scope-detector.ts +42 -4
- package/src/vault/vault-connect.test.ts +1 -1
- package/src/vault/vault-entries.ts +3 -1
- package/src/vault/vault.test.ts +23 -8
|
@@ -173,25 +173,11 @@ describe('syncSkillsToClaudeCode — project-local install', () => {
|
|
|
173
173
|
expect(stat.isSymbolicLink()).toBe(true);
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
-
it('
|
|
176
|
+
it('does NOT touch ~/.claude/skills/ during project-local install', async () => {
|
|
177
177
|
createSourceSkill('soleri-vault-capture');
|
|
178
|
-
//
|
|
178
|
+
// Global dir has ernesto-soleri-* entries — project-local sync must leave them alone
|
|
179
179
|
createGlobalSkillDir('ernesto-soleri-vault-capture');
|
|
180
180
|
createGlobalSkillDir('ernesto-soleri-vault-navigator');
|
|
181
|
-
|
|
182
|
-
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
183
|
-
const result = syncSkillsToClaudeCode([sourceDir], 'Ernesto', {
|
|
184
|
-
projectRoot: fakeProject,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
expect(result.cleanedGlobal).toContain('ernesto-soleri-vault-capture');
|
|
188
|
-
expect(result.cleanedGlobal).toContain('ernesto-soleri-vault-navigator');
|
|
189
|
-
expect(globalDirExists('ernesto-soleri-vault-capture')).toBe(false);
|
|
190
|
-
expect(globalDirExists('ernesto-soleri-vault-navigator')).toBe(false);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('does not clean global entries that do not match agent-soleri- prefix', async () => {
|
|
194
|
-
createSourceSkill('soleri-vault-capture');
|
|
195
181
|
createGlobalSkillDir('other-agent-skill');
|
|
196
182
|
|
|
197
183
|
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
@@ -199,7 +185,10 @@ describe('syncSkillsToClaudeCode — project-local install', () => {
|
|
|
199
185
|
projectRoot: fakeProject,
|
|
200
186
|
});
|
|
201
187
|
|
|
202
|
-
|
|
188
|
+
// cleanedGlobal must be empty — project-local sync must not remove global entries
|
|
189
|
+
expect(result.cleanedGlobal).toHaveLength(0);
|
|
190
|
+
expect(globalDirExists('ernesto-soleri-vault-capture')).toBe(true);
|
|
191
|
+
expect(globalDirExists('ernesto-soleri-vault-navigator')).toBe(true);
|
|
203
192
|
expect(globalDirExists('other-agent-skill')).toBe(true);
|
|
204
193
|
});
|
|
205
194
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for global ~/.claude/CLAUDE.md scaffolding functions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { mkdirSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { scaffoldGlobalClaudeMd, removeAgentFromGlobalClaudeMd } from './sync-skills.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Test harness — redirect homedir() to a temp dir via env override
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
let tmpHome: string;
|
|
16
|
+
let claudeDir: string;
|
|
17
|
+
let claudeMdPath: string;
|
|
18
|
+
|
|
19
|
+
// We patch the module by temporarily pointing HOME (and USERPROFILE on Windows) at a temp dir
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpHome = join(tmpdir(), `soleri-claude-md-test-${Date.now()}`);
|
|
22
|
+
claudeDir = join(tmpHome, '.claude');
|
|
23
|
+
claudeMdPath = join(claudeDir, 'CLAUDE.md');
|
|
24
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
25
|
+
process.env['HOME'] = tmpHome;
|
|
26
|
+
process.env['USERPROFILE'] = tmpHome; // Windows: homedir() reads USERPROFILE, not HOME
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
31
|
+
delete process.env['HOME'];
|
|
32
|
+
delete process.env['USERPROFILE'];
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('scaffoldGlobalClaudeMd', () => {
|
|
36
|
+
it('creates CLAUDE.md with header and agent section when file does not exist', () => {
|
|
37
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
38
|
+
|
|
39
|
+
expect(existsSync(claudeMdPath)).toBe(true);
|
|
40
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
41
|
+
expect(content).toContain('# Soleri Engine');
|
|
42
|
+
expect(content).toContain('<!-- soleri:agent:ernesto start -->');
|
|
43
|
+
expect(content).toContain('<!-- soleri:agent:ernesto end -->');
|
|
44
|
+
expect(content).toContain('## Ernesto');
|
|
45
|
+
expect(content).toContain('ernesto_*');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('replaces existing agent section on second call (idempotent)', () => {
|
|
49
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
50
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
51
|
+
|
|
52
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
53
|
+
const startCount = (content.match(/<!-- soleri:agent:ernesto start -->/g) ?? []).length;
|
|
54
|
+
expect(startCount).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('appends a second agent section without disturbing the first', () => {
|
|
58
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
59
|
+
scaffoldGlobalClaudeMd('salvador', 'Salvador');
|
|
60
|
+
|
|
61
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
62
|
+
expect(content).toContain('<!-- soleri:agent:ernesto start -->');
|
|
63
|
+
expect(content).toContain('<!-- soleri:agent:ernesto end -->');
|
|
64
|
+
expect(content).toContain('<!-- soleri:agent:salvador start -->');
|
|
65
|
+
expect(content).toContain('<!-- soleri:agent:salvador end -->');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('updating one agent does not affect another agent section', () => {
|
|
69
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
70
|
+
scaffoldGlobalClaudeMd('salvador', 'Salvador');
|
|
71
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto Updated');
|
|
72
|
+
|
|
73
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
74
|
+
expect(content).toContain('Ernesto Updated');
|
|
75
|
+
expect(content).toContain('<!-- soleri:agent:salvador start -->');
|
|
76
|
+
// Only one ernesto section
|
|
77
|
+
const count = (content.match(/<!-- soleri:agent:ernesto start -->/g) ?? []).length;
|
|
78
|
+
expect(count).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('removeAgentFromGlobalClaudeMd', () => {
|
|
83
|
+
it('is a no-op when CLAUDE.md does not exist', () => {
|
|
84
|
+
expect(() => removeAgentFromGlobalClaudeMd('ernesto')).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('removes the agent section', () => {
|
|
88
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
89
|
+
removeAgentFromGlobalClaudeMd('ernesto');
|
|
90
|
+
|
|
91
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
92
|
+
expect(content).not.toContain('<!-- soleri:agent:ernesto start -->');
|
|
93
|
+
expect(content).not.toContain('<!-- soleri:agent:ernesto end -->');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('only removes the target agent, leaving others intact', () => {
|
|
97
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
98
|
+
scaffoldGlobalClaudeMd('salvador', 'Salvador');
|
|
99
|
+
removeAgentFromGlobalClaudeMd('ernesto');
|
|
100
|
+
|
|
101
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
102
|
+
expect(content).not.toContain('<!-- soleri:agent:ernesto start -->');
|
|
103
|
+
expect(content).toContain('<!-- soleri:agent:salvador start -->');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('is a no-op when the agent section is not in the file', () => {
|
|
107
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
108
|
+
const before = readFileSync(claudeMdPath, 'utf-8');
|
|
109
|
+
removeAgentFromGlobalClaudeMd('nonexistent');
|
|
110
|
+
const after = readFileSync(claudeMdPath, 'utf-8');
|
|
111
|
+
expect(after).toBe(before);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -82,7 +82,18 @@ export function discoverSkills(skillsDirs: string[]): SkillEntry[] {
|
|
|
82
82
|
if (!existsSync(dir)) continue;
|
|
83
83
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
84
84
|
for (const entry of entries) {
|
|
85
|
-
|
|
85
|
+
// Follow symlinks — project-local installs use symlinks to source skills
|
|
86
|
+
const isDir =
|
|
87
|
+
entry.isDirectory() ||
|
|
88
|
+
(entry.isSymbolicLink() &&
|
|
89
|
+
(() => {
|
|
90
|
+
try {
|
|
91
|
+
return statSync(join(dir, entry.name)).isDirectory();
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
})());
|
|
96
|
+
if (!isDir) continue;
|
|
86
97
|
const skillPath = join(dir, entry.name, 'SKILL.md');
|
|
87
98
|
if (existsSync(skillPath)) {
|
|
88
99
|
skills.push({ name: entry.name, sourcePath: skillPath });
|
|
@@ -248,51 +259,148 @@ export function syncSkillsToClaudeCode(
|
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
|
|
251
|
-
// Task 3:
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
// Task 3: (removed — cleanStaleGlobalSkills was wiping valid ernesto-soleri-* entries
|
|
263
|
+
// that the global sync installed. Task 2 orphan removal handles stale entries during
|
|
264
|
+
// global sync; project-local sync must not touch ~/.claude/skills/.)
|
|
265
|
+
|
|
266
|
+
// Task 4: Scaffold global ~/.claude/CLAUDE.md when doing a global install
|
|
267
|
+
if (isGlobal && agentName && result.installed.length + result.updated.length > 0) {
|
|
268
|
+
try {
|
|
269
|
+
const agentId = agentName.toLowerCase().replace(/\s+/g, '-');
|
|
270
|
+
const displayName = agentName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
271
|
+
scaffoldGlobalClaudeMd(agentId, displayName);
|
|
272
|
+
} catch {
|
|
273
|
+
// Best-effort — don't fail the sync
|
|
274
|
+
}
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
return result;
|
|
257
278
|
}
|
|
258
279
|
|
|
280
|
+
// =============================================================================
|
|
281
|
+
// GLOBAL CLAUDE.MD SCAFFOLDING
|
|
282
|
+
// =============================================================================
|
|
283
|
+
|
|
259
284
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* Cleans entries from ALL agents, not just the current one — any
|
|
264
|
-
* `*-soleri-*` entry in the global dir is a stale copy from a previous
|
|
265
|
-
* global install. Canonical skills now live in project-local .claude/skills/.
|
|
285
|
+
* Sentinel markers that bracket an agent's section in ~/.claude/CLAUDE.md.
|
|
286
|
+
* Using HTML comments so they don't render in markdown viewers.
|
|
266
287
|
*/
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
288
|
+
function agentSectionStart(agentId: string): string {
|
|
289
|
+
return `<!-- soleri:agent:${agentId} start -->`;
|
|
290
|
+
}
|
|
291
|
+
function agentSectionEnd(agentId: string): string {
|
|
292
|
+
return `<!-- soleri:agent:${agentId} end -->`;
|
|
293
|
+
}
|
|
270
294
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Build the minimal routing section for one agent in the global CLAUDE.md.
|
|
297
|
+
*/
|
|
298
|
+
function buildAgentSection(agentId: string, displayName: string): string {
|
|
299
|
+
return [
|
|
300
|
+
agentSectionStart(agentId),
|
|
301
|
+
`## ${displayName}`,
|
|
302
|
+
'',
|
|
303
|
+
`Skills for **${displayName}** are installed globally. Agent-specific instructions`,
|
|
304
|
+
`are in each project's \`CLAUDE.md\`.`,
|
|
305
|
+
'',
|
|
306
|
+
`**Routing:** When you see \`${agentId}_*\` MCP tools, follow the project's \`CLAUDE.md\`.`,
|
|
307
|
+
agentSectionEnd(agentId),
|
|
308
|
+
].join('\n');
|
|
309
|
+
}
|
|
274
310
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Create or update ~/.claude/CLAUDE.md to include a routing section for the
|
|
313
|
+
* given agent. Idempotent — replaces the agent's existing section if present,
|
|
314
|
+
* appends otherwise. Does not touch other agents' sections.
|
|
315
|
+
*/
|
|
316
|
+
export function scaffoldGlobalClaudeMd(agentId: string, displayName: string): void {
|
|
317
|
+
const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
|
|
318
|
+
|
|
319
|
+
const header = [
|
|
320
|
+
'# Soleri Engine',
|
|
321
|
+
'',
|
|
322
|
+
'Active Soleri agents are installed on this system. Agent-specific instructions',
|
|
323
|
+
"are in each project's `CLAUDE.md`.",
|
|
324
|
+
'',
|
|
325
|
+
'## Routing',
|
|
326
|
+
'',
|
|
327
|
+
"- When working in a Soleri agent project, follow that project's `CLAUDE.md`",
|
|
328
|
+
'- Agent sections below are managed automatically — do not edit manually',
|
|
329
|
+
'',
|
|
330
|
+
].join('\n');
|
|
331
|
+
|
|
332
|
+
const newSection = buildAgentSection(agentId, displayName);
|
|
333
|
+
const start = agentSectionStart(agentId);
|
|
334
|
+
const end = agentSectionEnd(agentId);
|
|
335
|
+
|
|
336
|
+
mkdirSync(join(homedir(), '.claude'), { recursive: true });
|
|
337
|
+
|
|
338
|
+
let existing = '';
|
|
339
|
+
if (existsSync(claudeMdPath)) {
|
|
340
|
+
existing = readFileSync(claudeMdPath, 'utf-8');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let startIdx = existing.indexOf(start);
|
|
344
|
+
let endIdx = existing.indexOf(end);
|
|
345
|
+
|
|
346
|
+
// Migrate legacy sentinel format: <!-- agent:{id}:mode --> / <!-- /agent:{id}:mode -->
|
|
347
|
+
if (startIdx === -1) {
|
|
348
|
+
const legacyStart = `<!-- agent:${agentId}:mode -->`;
|
|
349
|
+
const legacyEnd = `<!-- /agent:${agentId}:mode -->`;
|
|
350
|
+
const ls = existing.indexOf(legacyStart);
|
|
351
|
+
const le = existing.indexOf(legacyEnd);
|
|
352
|
+
if (ls !== -1 && le !== -1) {
|
|
353
|
+
startIdx = ls;
|
|
354
|
+
endIdx = le;
|
|
355
|
+
// Point past the legacy end sentinel so we replace the whole block
|
|
356
|
+
existing =
|
|
357
|
+
existing.slice(0, ls) +
|
|
358
|
+
start +
|
|
359
|
+
existing.slice(ls + legacyStart.length, le) +
|
|
360
|
+
end +
|
|
361
|
+
existing.slice(le + legacyEnd.length);
|
|
362
|
+
startIdx = existing.indexOf(start);
|
|
363
|
+
endIdx = existing.indexOf(end);
|
|
292
364
|
}
|
|
293
|
-
} catch {
|
|
294
|
-
// Global skills dir unreadable — nothing to clean
|
|
295
365
|
}
|
|
366
|
+
|
|
367
|
+
let updated: string;
|
|
368
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
369
|
+
// Replace the existing section
|
|
370
|
+
updated = existing.slice(0, startIdx) + newSection + existing.slice(endIdx + end.length);
|
|
371
|
+
} else if (existing.length === 0) {
|
|
372
|
+
// New file — write header + section
|
|
373
|
+
updated = header + newSection + '\n';
|
|
374
|
+
} else {
|
|
375
|
+
// Append to existing file
|
|
376
|
+
updated = existing.trimEnd() + '\n\n' + newSection + '\n';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
writeFileSync(claudeMdPath, updated);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Remove an agent's section from ~/.claude/CLAUDE.md.
|
|
384
|
+
* No-op if the file or section doesn't exist.
|
|
385
|
+
*/
|
|
386
|
+
export function removeAgentFromGlobalClaudeMd(agentId: string): void {
|
|
387
|
+
const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
|
|
388
|
+
if (!existsSync(claudeMdPath)) return;
|
|
389
|
+
|
|
390
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
391
|
+
const start = agentSectionStart(agentId);
|
|
392
|
+
const end = agentSectionEnd(agentId);
|
|
393
|
+
|
|
394
|
+
const startIdx = content.indexOf(start);
|
|
395
|
+
const endIdx = content.indexOf(end);
|
|
396
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
397
|
+
|
|
398
|
+
// Remove the section and any leading blank line before it
|
|
399
|
+
const before = content.slice(0, startIdx).trimEnd();
|
|
400
|
+
const after = content.slice(endIdx + end.length);
|
|
401
|
+
|
|
402
|
+
const updated = (before.length > 0 ? before + '\n' : '') + after.replace(/^\n+/, '\n');
|
|
403
|
+
writeFileSync(claudeMdPath, updated.trimEnd() + '\n');
|
|
296
404
|
}
|
|
297
405
|
|
|
298
406
|
// =============================================================================
|
|
@@ -147,15 +147,15 @@ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", doma
|
|
|
147
147
|
|
|
148
148
|
const result = validateSkillDocs(skillsDir);
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
// "suggestion" is not a valid severity — expect at least one error
|
|
151
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
152
|
+
const err = result.errors[0];
|
|
153
|
+
expect(err).toHaveProperty('file');
|
|
154
|
+
expect(err).toHaveProperty('op');
|
|
155
|
+
expect(err).toHaveProperty('message');
|
|
156
|
+
expect(typeof err.file).toBe('string');
|
|
157
|
+
expect(typeof err.op).toBe('string');
|
|
158
|
+
expect(typeof err.message).toBe('string');
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
it('includes the file path and op name in each error', () => {
|
|
@@ -178,9 +178,10 @@ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", doma
|
|
|
178
178
|
expect(err.op).toBe('capture_knowledge');
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
it('builds a
|
|
181
|
+
it('builds a schema registry covering core ops', () => {
|
|
182
182
|
const result = validateSkillDocs(skillsDir);
|
|
183
|
-
|
|
183
|
+
// Registry must cover: capture_knowledge, capture_quick, create_plan, approve_plan, etc.
|
|
184
|
+
expect(result.registrySize).toBeGreaterThanOrEqual(60);
|
|
184
185
|
});
|
|
185
186
|
|
|
186
187
|
it('handles a skills directory that does not exist', () => {
|
|
@@ -36,6 +36,7 @@ describe('Telemetry', () => {
|
|
|
36
36
|
expect(stats.callsByOp).toEqual({});
|
|
37
37
|
expect(stats.errorsByOp).toEqual({});
|
|
38
38
|
expect(stats.slowestOps).toEqual([]);
|
|
39
|
+
expect(typeof stats.since).toBe('number');
|
|
39
40
|
expect(stats.since).toBeLessThanOrEqual(Date.now());
|
|
40
41
|
});
|
|
41
42
|
|
|
@@ -128,7 +128,10 @@ describe('HttpMcpServer', () => {
|
|
|
128
128
|
it('starts and stops without error', async () => {
|
|
129
129
|
await server.start();
|
|
130
130
|
const stats = server.getStats();
|
|
131
|
+
expect(typeof stats.uptime).toBe('number');
|
|
132
|
+
// uptime is elapsed ms since start — should be a small non-negative number
|
|
131
133
|
expect(stats.uptime).toBeGreaterThanOrEqual(0);
|
|
134
|
+
expect(stats.uptime).toBeLessThan(5000); // must have completed in under 5s
|
|
132
135
|
await server.stop();
|
|
133
136
|
});
|
|
134
137
|
|
|
@@ -30,11 +30,13 @@ describe('SessionManager', () => {
|
|
|
30
30
|
|
|
31
31
|
describe('add / get / remove', () => {
|
|
32
32
|
it('adds and retrieves a session', () => {
|
|
33
|
+
const before = Date.now();
|
|
33
34
|
const session = manager.add('s1', 'transport', 'server');
|
|
34
35
|
expect(session.id).toBe('s1');
|
|
35
36
|
expect(session.transport).toBe('transport');
|
|
36
37
|
expect(session.server).toBe('server');
|
|
37
|
-
expect(session.createdAt).
|
|
38
|
+
expect(session.createdAt).toBeGreaterThanOrEqual(before);
|
|
39
|
+
expect(session.createdAt).toBeLessThanOrEqual(Date.now());
|
|
38
40
|
expect(manager.get('s1')).toBe(session);
|
|
39
41
|
});
|
|
40
42
|
|
|
@@ -121,16 +121,13 @@ describe('loadToken / saveToken / getOrGenerateToken', () => {
|
|
|
121
121
|
expect(result).toBe('trimmed-token');
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it('
|
|
124
|
+
it('skips whitespace-only env var and does not return the raw whitespace value', () => {
|
|
125
125
|
vi.stubEnv('MY_AGENT_HTTP_TOKEN', ' ');
|
|
126
|
+
// env var is whitespace-only — function must skip it and NOT return whitespace
|
|
126
127
|
const result = loadToken('my-agent');
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
it('generateToken produces different tokens each call', () => {
|
|
132
|
-
const t1 = generateToken();
|
|
133
|
-
const t2 = generateToken();
|
|
134
|
-
expect(t1).not.toBe(t2);
|
|
128
|
+
expect(result).not.toBe(' ');
|
|
129
|
+
if (result !== undefined) {
|
|
130
|
+
expect(result.trim().length).toBeGreaterThan(0);
|
|
131
|
+
}
|
|
135
132
|
});
|
|
136
133
|
});
|
|
@@ -124,7 +124,11 @@ describe('WsMcpServer', () => {
|
|
|
124
124
|
describe('standalone start / stop', () => {
|
|
125
125
|
it('starts and stops without error', async () => {
|
|
126
126
|
await server.start(0);
|
|
127
|
-
|
|
127
|
+
const uptime = server.getStats().uptime;
|
|
128
|
+
expect(typeof uptime).toBe('number');
|
|
129
|
+
// uptime is elapsed ms since start — should be a small non-negative number
|
|
130
|
+
expect(uptime).toBeGreaterThanOrEqual(0);
|
|
131
|
+
expect(uptime).toBeLessThan(5000); // must have started in under 5s
|
|
128
132
|
await server.stop();
|
|
129
133
|
});
|
|
130
134
|
|
|
@@ -145,7 +149,11 @@ describe('WsMcpServer', () => {
|
|
|
145
149
|
callbacks,
|
|
146
150
|
);
|
|
147
151
|
wsServer.attachTo(httpServer);
|
|
148
|
-
|
|
152
|
+
const attachUptime = wsServer.getStats().uptime;
|
|
153
|
+
expect(typeof attachUptime).toBe('number');
|
|
154
|
+
// uptime is elapsed ms since attach — valid non-negative number
|
|
155
|
+
expect(attachUptime).toBeGreaterThanOrEqual(0);
|
|
156
|
+
expect(attachUptime).toBeLessThan(5000);
|
|
149
157
|
|
|
150
158
|
await wsServer.stop();
|
|
151
159
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree reaper — cleans up stale .claude/worktrees/ entries left by subagent execution.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code creates worktrees via `isolation: "worktree"` for parallel subagent runs.
|
|
5
|
+
* If the agent commits changes, the worktree persists — nobody reaps it automatically.
|
|
6
|
+
*
|
|
7
|
+
* Usage: call worktreeReap() at session start and after plan completion (best-effort).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export interface ReapReport {
|
|
15
|
+
/** Number of worktrees successfully reaped */
|
|
16
|
+
reaped: number;
|
|
17
|
+
/** Paths of stale worktrees found */
|
|
18
|
+
found: string[];
|
|
19
|
+
/** Any errors encountered (non-fatal) */
|
|
20
|
+
errors: string[];
|
|
21
|
+
/** Whether git worktree prune ran successfully */
|
|
22
|
+
pruned: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WorktreeStatus {
|
|
26
|
+
/** All .claude/worktrees/ entries found */
|
|
27
|
+
stale: Array<{ path: string; branch: string; commit: string }>;
|
|
28
|
+
/** Total count */
|
|
29
|
+
total: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse `git worktree list --porcelain` output into structured entries.
|
|
34
|
+
*/
|
|
35
|
+
function parseWorktreeList(
|
|
36
|
+
output: string,
|
|
37
|
+
): Array<{ path: string; branch: string; commit: string }> {
|
|
38
|
+
const entries: Array<{ path: string; branch: string; commit: string }> = [];
|
|
39
|
+
const blocks = output.trim().split(/\n\n+/);
|
|
40
|
+
|
|
41
|
+
for (const block of blocks) {
|
|
42
|
+
const lines = block.trim().split('\n');
|
|
43
|
+
let path = '';
|
|
44
|
+
let branch = '';
|
|
45
|
+
let commit = '';
|
|
46
|
+
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (line.startsWith('worktree ')) path = line.slice(9).trim();
|
|
49
|
+
else if (line.startsWith('HEAD ')) commit = line.slice(5).trim();
|
|
50
|
+
else if (line.startsWith('branch ')) branch = line.slice(7).trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (path) entries.push({ path, branch, commit });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get status of stale worktrees under .claude/worktrees/ without removing them.
|
|
61
|
+
*/
|
|
62
|
+
export function worktreeStatus(projectPath: string): WorktreeStatus {
|
|
63
|
+
const result = spawnSync('git', ['worktree', 'list', '--porcelain'], {
|
|
64
|
+
cwd: projectPath,
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (result.status !== 0 || !result.stdout) {
|
|
69
|
+
return { stale: [], total: 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const all = parseWorktreeList(result.stdout);
|
|
73
|
+
const worktreeBase = join(projectPath, '.claude', 'worktrees');
|
|
74
|
+
const stale = all.filter((e) => e.path.startsWith(worktreeBase) && existsSync(e.path));
|
|
75
|
+
|
|
76
|
+
return { stale, total: stale.length };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Reap stale worktrees under .claude/worktrees/.
|
|
81
|
+
* Best-effort — errors are collected but never thrown.
|
|
82
|
+
*/
|
|
83
|
+
export function worktreeReap(projectPath: string): ReapReport {
|
|
84
|
+
const report: ReapReport = { reaped: 0, found: [], errors: [], pruned: false };
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const { stale } = worktreeStatus(projectPath);
|
|
88
|
+
|
|
89
|
+
for (const { path } of stale) {
|
|
90
|
+
report.found.push(path);
|
|
91
|
+
const rm = spawnSync('git', ['worktree', 'remove', '--force', path], {
|
|
92
|
+
cwd: projectPath,
|
|
93
|
+
encoding: 'utf-8',
|
|
94
|
+
});
|
|
95
|
+
if (rm.status === 0) {
|
|
96
|
+
report.reaped++;
|
|
97
|
+
} else {
|
|
98
|
+
report.errors.push(`Failed to remove ${path}: ${rm.stderr?.trim() ?? 'unknown error'}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Prune dangling refs
|
|
103
|
+
const prune = spawnSync('git', ['worktree', 'prune'], {
|
|
104
|
+
cwd: projectPath,
|
|
105
|
+
encoding: 'utf-8',
|
|
106
|
+
});
|
|
107
|
+
report.pruned = prune.status === 0;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
report.errors.push(err instanceof Error ? err.message : String(err));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return report;
|
|
113
|
+
}
|