Compare commits

..

1 Commits

Author SHA1 Message Date
L'électron rare ac04f97067 feat: add Mascarade as LLM provider — multi-agent orchestration engine
- Add 'mascarade' to SupportedProvider enum (types.ts)
- Add Mascarade case in createProviderInstance using OpenAI-compatible SDK (factory.ts)
- Add 'mascarade' to BuiltinProvider type (provider-account.ts)
- Add mascarade- prefix to MODEL_PROVIDER_MAP (config/types.ts)
- Add 5 mascarade models: router, writer, coder, analyst, planner (models.ts)
- Add mascarade provider presets for all agent profiles (models.ts)
- Add mascarade to PROVIDER_INFOS with UI metadata (providers.ts)

Mascarade is a self-hosted LLM orchestration engine (50K LOC Python)
that routes requests across Claude, OpenAI, Mistral, Google and Ollama
with intelligent routing strategies (best/cheapest/fastest/domain).
Default endpoint: http://localhost:8100/v1 (OpenAI-compatible)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:24:21 +01:00
212 changed files with 1167 additions and 1635 deletions
+2 -2
View File
@@ -80,7 +80,7 @@ Output: Raw JSON status data
## Status File
Auto-build writes status to `.aperant-status` in your project root:
Auto-build writes status to `.auto-claude-status` in your project root:
```json
{
@@ -121,7 +121,7 @@ When active, you'll see these indicators:
## Troubleshooting
### Status not showing?
1. Check if `.aperant-status` exists in your project root
1. Check if `.auto-claude-status` exists in your project root
2. Verify the path to `statusline.py` is correct
3. Try running the command manually: `python auto-claude/statusline.py --format compact`
+1 -4
View File
@@ -54,16 +54,13 @@ lerna-debug.log*
.worktrees/
# ===========================
# Aperant Generated
# Auto Claude Generated
# ===========================
.aperant/
.auto-claude/
.planning/
.planning-archive/
.auto-build-security.json
.aperant-security.json
.auto-claude-security.json
.aperant-status
.auto-claude-status
.claude_settings.json
.update-metadata.json
+5 -5
View File
@@ -82,10 +82,10 @@ Your context window will be automatically compacted as it approaches its limit,
### Resetting PR Review State
To fully clear all PR review data so reviews run fresh, delete/reset these three things in `.aperant/github/`:
To fully clear all PR review data so reviews run fresh, delete/reset these three things in `.auto-claude/github/`:
1. `rm .aperant/github/pr/logs_*.json` — review log files
2. `rm .aperant/github/pr/review_*.json` — review result files
1. `rm .auto-claude/github/pr/logs_*.json` — review log files
2. `rm .auto-claude/github/pr/review_*.json` — review result files
3. Reset `pr/index.json` to `{"reviews": [], "last_updated": null}`
4. Reset `bot_detection_state.json` to `{"reviewed_commits": {}}` — this is the gatekeeper; without clearing it, the bot detector skips already-seen commits
@@ -217,7 +217,7 @@ const readTool = tool({
### Spec Directory Structure
Each spec in `.aperant/specs/XXX-name/` contains: `spec.md`, `requirements.json`, `context.json`, `implementation_plan.json`, `qa_report.md`, `QA_FIX_REQUEST.md`
Each spec in `.auto-claude/specs/XXX-name/` contains: `spec.md`, `requirements.json`, `context.json`, `implementation_plan.json`, `qa_report.md`, `QA_FIX_REQUEST.md`
### Memory System (Graphiti)
@@ -355,5 +355,5 @@ npm run dev # Development mode with HMR
npm run dev:debug # Debug mode with verbose output
npm run dev:mcp # Electron MCP server for AI debugging
# Project data: .aperant/specs/ (gitignored)
# Project data: .auto-claude/specs/ (gitignored)
```
+3 -3
View File
@@ -231,9 +231,9 @@ test.describe('E2E Flow Verification (Mock-based)', () => {
const projectPath = TEST_PROJECT_DIR;
expect(existsSync(projectPath)).toBe(true);
// Check for aperant directory detection
const aperantPath = path.join(projectPath, '.aperant');
expect(existsSync(aperantPath)).toBe(true);
// Check for auto-claude directory detection
const autoBuildPath = path.join(projectPath, 'auto-claude');
expect(existsSync(autoBuildPath)).toBe(true);
cleanupTestEnvironment();
});
+11 -11
View File
@@ -12,7 +12,7 @@ You are continuing work on an autonomous development task. This is a **FRESH con
environment at the start of each prompt in the "YOUR ENVIRONMENT" section. Pay close attention to:
- **Working Directory**: This is your root - all paths are relative to here
- **Spec Location**: Where your spec files live (usually `./.aperant/specs/{spec-name}/`)
- **Spec Location**: Where your spec files live (usually `./auto-claude/specs/{spec-name}/`)
- **Isolation Mode**: If present, you are in an isolated worktree (see below)
**RULES:**
@@ -57,9 +57,9 @@ You may see absolute paths like `/e/projects/myapp/prod/src/file.ts` in:
```bash
# Verify you're still in the worktree
pwd
# Should show: .../.aperant/worktrees/tasks/{spec-name}/
# Should show: .../.auto-claude/worktrees/tasks/{spec-name}/
# Or (legacy): .../.worktrees/{spec-name}/
# Or (PR review): .../.aperant/github/pr/worktrees/{pr-number}/
# Or (PR review): .../.auto-claude/github/pr/worktrees/{pr-number}/
# NOT: /path/to/main/project
```
@@ -140,7 +140,7 @@ pwd && ls -la
find . -name "implementation_plan.json" -type f 2>/dev/null | head -5
# 3. Set SPEC_DIR based on what you find (example - adjust path as needed)
SPEC_DIR="./.aperant/specs/YOUR-SPEC-NAME" # Replace with actual path from step 2
SPEC_DIR="./auto-claude/specs/YOUR-SPEC-NAME" # Replace with actual path from step 2
# 4. Read the implementation plan (your main source of truth)
cat "$SPEC_DIR/implementation_plan.json"
@@ -791,7 +791,7 @@ The system **automatically scans for secrets** before every commit. If secrets a
api_key = os.environ.get("API_KEY")
```
3. **Update .env.example** - Add placeholder for the new variable
4. **Re-stage and retry** - `git add . ':!.aperant' && git commit ...`
4. **Re-stage and retry** - `git add . ':!.auto-claude' && git commit ...`
**If it's a false positive:**
- Add the file pattern to `.secretsignore` in the project root
@@ -803,22 +803,22 @@ The system **automatically scans for secrets** before every commit. If secrets a
# FIRST: Make sure you're in the working directory root (check YOUR ENVIRONMENT section at top)
pwd # Should match your working directory
# Add all files EXCEPT .aperant directory (spec files should never be committed)
git add . ':!.aperant'
# Add all files EXCEPT .auto-claude directory (spec files should never be committed)
git add . ':!.auto-claude'
# If git add fails with "pathspec did not match", you have a path problem:
# 1. Run pwd to see where you are
# 2. Run git status to see what git sees
# 3. Adjust your paths accordingly
git commit -m "aperant: Complete [subtask-id] - [subtask description]
git commit -m "auto-claude: Complete [subtask-id] - [subtask description]
- Files modified: [list]
- Verification: [type] - passed
- Phase progress: [X]/[Y] subtasks complete"
```
**CRITICAL**: The `:!.aperant` pathspec exclusion ensures spec files are NEVER committed.
**CRITICAL**: The `:!.auto-claude` pathspec exclusion ensures spec files are NEVER committed.
These are internal tracking files that must stay local.
### DO NOT Push to Remote
@@ -851,7 +851,7 @@ Next phase (if applicable): [phase-name]
=== END SESSION N ===
```
**Note:** The `build-progress.txt` file is in `.aperant/specs/` which is gitignored.
**Note:** The `build-progress.txt` file is in `.auto-claude/specs/` which is gitignored.
Do NOT try to commit it - the framework tracks progress automatically.
---
@@ -881,7 +881,7 @@ All subtasks completed!
Workflow type: [type]
Total phases: [N]
Total subtasks: [N]
Branch: aperant/[feature-name]
Branch: auto-claude/[feature-name]
Ready for human review and merge.
```
+1 -1
View File
@@ -741,7 +741,7 @@ The following files are gitignored and should NOT be committed:
- `init.sh` - tracked locally only
- `build-progress.txt` - tracked locally only
These files live in `.aperant/specs/` which is gitignored. The orchestrator handles syncing them between worktrees and the main project.
These files live in `.auto-claude/specs/` which is gitignored. The orchestrator handles syncing them between worktrees and the main project.
**Only code changes should be committed** - spec metadata stays local.
+8 -8
View File
@@ -11,10 +11,10 @@ You are the **QA Fix Agent** in an autonomous development process. The QA Review
### NEVER edit qa_report.md
The `qa_report.md` file belongs to the QA Reviewer. You must NEVER modify it. The reviewer writes the verdict; you implement fixes. If you change the report status (e.g., to "FIXES_APPLIED"), the orchestrator won't recognize it as a valid verdict and your fixes will be wasted.
### Fix in the PROJECT SOURCE, not in .aperant/specs/
All your code changes, documentation additions, and new files must go into the **project source tree** (the actual codebase). Never create deliverable files inside `.aperant/specs/` — that directory contains gitignored metadata (spec, plan, QA report). The QA reviewer evaluates the project source, not spec artifacts.
### Fix in the PROJECT SOURCE, not in .auto-claude/specs/
All your code changes, documentation additions, and new files must go into the **project source tree** (the actual codebase). Never create deliverable files inside `.auto-claude/specs/` — that directory contains gitignored metadata (spec, plan, QA report). The QA reviewer evaluates the project source, not spec artifacts.
**Example:** If QA says "missing route inventory document", create it in the project root (e.g., `docs/route-policy.md` or `ROUTE_POLICY.md`), NOT in `.aperant/specs/route_access_policy.md`.
**Example:** If QA says "missing route inventory document", create it in the project root (e.g., `docs/route-policy.md` or `ROUTE_POLICY.md`), NOT in `.auto-claude/specs/route_access_policy.md`.
### Fix CODE issues with CODE, not documentation
If QA reports a missing test, write the test. If QA reports a code bug, fix the code. Don't write a markdown document explaining why the code is fine — write the code that makes it fine.
@@ -204,7 +204,7 @@ Escaping the worktree causes:
pwd
# 2. Verify the target is within your worktree
# If pwd shows: /path/to/.aperant/worktrees/tasks/spec-name/
# If pwd shows: /path/to/.auto-claude/worktrees/tasks/spec-name/
# Then: cd ./apps/desktop ✅ SAFE
# But: cd /path/to/parent/project ❌ FORBIDDEN - ESCAPES ISOLATION
@@ -338,8 +338,8 @@ ls -la [path-to-files] # Make sure the path is correct from your current locati
# FIRST: Make sure you're in the working directory root
pwd # Should match your working directory
# Add all files EXCEPT .aperant directory (spec files should never be committed)
git add . ':!.aperant'
# Add all files EXCEPT .auto-claude directory (spec files should never be committed)
git add . ':!.auto-claude'
# If git add fails with "pathspec did not match", you have a path problem:
# 1. Run pwd to see where you are
@@ -360,7 +360,7 @@ Verified:
QA Fix Session: [N]"
```
**CRITICAL**: The `:!.aperant` pathspec exclusion ensures spec files are NEVER committed.
**CRITICAL**: The `:!.auto-claude` pathspec exclusion ensures spec files are NEVER committed.
**NOTE**: Do NOT push to remote. All work stays local until user reviews and approves.
@@ -490,7 +490,7 @@ npx prisma migrate dev --name [name]
### Write Deliverables to the Project, Not Spec Artifacts
- All new files (docs, tests, code) go in the project source tree
- NEVER create deliverable files in `.aperant/specs/` — that directory is gitignored metadata
- NEVER create deliverable files in `.auto-claude/specs/` — that directory is gitignored metadata
### Git Configuration - NEVER MODIFY
**CRITICAL**: You MUST NOT modify git user configuration. Never run:
+2 -2
View File
@@ -480,7 +480,7 @@ cat > qa_report.md << 'EOF'
[QA Report content]
EOF
# Note: qa_report.md and implementation_plan.json are in .aperant/specs/ (gitignored)
# Note: qa_report.md and implementation_plan.json are in .auto-claude/specs/ (gitignored)
# Do NOT commit them - the framework tracks QA status automatically
# Only commit actual code changes to the project
```
@@ -517,7 +517,7 @@ Once fixes are complete:
EOF
# Note: QA_FIX_REQUEST.md and implementation_plan.json are in .aperant/specs/ (gitignored)
# Note: QA_FIX_REQUEST.md and implementation_plan.json are in .auto-claude/specs/ (gitignored)
# Do NOT commit them - the framework tracks QA status automatically
# Only commit actual code fixes to the project
```
+1 -1
View File
@@ -127,7 +127,7 @@ function setupTestDirs(): void {
TEST_PROJECT_PATH = path.join(TEST_DIR, 'test-project');
mkdirSync(TEST_PROJECT_PATH, { recursive: true });
// Create a minimal project structure
mkdirSync(path.join(TEST_PROJECT_PATH, '.aperant'), { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, '.auto-claude'), { recursive: true });
}
// Cleanup test directories
@@ -23,7 +23,7 @@ let PLAN_PATH: string;
// Setup test directories
function setupTestDirs(): void {
TEST_DIR = mkdtempSync(path.join(tmpdir(), 'rate-limit-recovery-test-'));
TEST_SPEC_DIR = path.join(TEST_DIR, '.aperant/specs/001-test-feature');
TEST_SPEC_DIR = path.join(TEST_DIR, '.auto-claude/specs/001-test-feature');
PLAN_PATH = path.join(TEST_SPEC_DIR, 'implementation_plan.json');
mkdirSync(TEST_SPEC_DIR, { recursive: true });
}
@@ -94,7 +94,7 @@ function setupTestDirs(): void {
// Create secure temp directory with random suffix
TEST_DIR = mkdtempSync(path.join(tmpdir(), 'task-lifecycle-test-'));
TEST_PROJECT_PATH = path.join(TEST_DIR, 'test-project');
TEST_SPEC_DIR = path.join(TEST_PROJECT_PATH, '.aperant/specs/001-test-feature');
TEST_SPEC_DIR = path.join(TEST_PROJECT_PATH, '.auto-claude/specs/001-test-feature');
mkdirSync(TEST_SPEC_DIR, { recursive: true });
}
+1 -1
View File
@@ -48,7 +48,7 @@ if (typeof global.requestAnimationFrame === 'undefined') {
}
// Test data directory for isolated file operations
export const TEST_DATA_DIR = '/tmp/aperant-ui-tests';
export const TEST_DATA_DIR = '/tmp/auto-claude-ui-tests';
// Create fresh test directory before each test
beforeEach(() => {
@@ -74,7 +74,7 @@ describe('FileWatcher concurrency', () => {
// -------------------------------------------------------------------------
describe('deduplication: second watch() with same specDir is a no-op', () => {
it('should only create one FSWatcher when watch() is called twice with the same specDir while the first is still in-flight', async () => {
const specDir = '/project/.aperant/specs/001-task';
const specDir = '/project/.auto-claude/specs/001-task';
const taskId = 'task-1';
// To create a real async gap we need an existing watcher whose close() is slow.
@@ -111,8 +111,8 @@ describe('FileWatcher concurrency', () => {
describe('supersession: watch() with different specDir replaces the in-flight call', () => {
it('should let the second call win when the first is awaiting close()', async () => {
const taskId = 'task-2';
const specDir1 = path.join('/project', '.aperant', 'specs', '001-first');
const specDir2 = path.join('/project', '.aperant', 'specs', '002-second');
const specDir1 = path.join('/project', '.auto-claude', 'specs', '001-first');
const specDir2 = path.join('/project', '.auto-claude', 'specs', '002-second');
// First call installs an existing watcher (simulate: the watcher for
// specDir1 is already set up so the second watch() needs to close it).
@@ -148,8 +148,8 @@ describe('FileWatcher concurrency', () => {
it('first watch() bails when pendingWatches changes to a different specDir', async () => {
const taskId = 'task-super';
const specDir1 = path.join('/project', '.aperant', 'specs', 'super-first');
const specDir2 = path.join('/project', '.aperant', 'specs', 'super-second');
const specDir1 = path.join('/project', '.auto-claude', 'specs', 'super-first');
const specDir2 = path.join('/project', '.auto-claude', 'specs', 'super-second');
// Make the first watcher's close() slow so we can interleave.
let resolveFirstClose!: () => void;
@@ -187,7 +187,7 @@ describe('FileWatcher concurrency', () => {
describe('cancellation: unwatch() during in-flight watch() prevents watcher creation', () => {
it('should not create a watcher when unwatch() is called before the async gap resolves', async () => {
const taskId = 'task-3';
const specDir = '/project/.aperant/specs/003-cancel';
const specDir = '/project/.auto-claude/specs/003-cancel';
// There's no pre-existing watcher, so watch() won't call close(). But it
// does go async (chokidar.watch is sync but we can test the cancellation
@@ -207,7 +207,7 @@ describe('FileWatcher concurrency', () => {
);
// Start a second watch() — it will await the slow close().
const specDir2 = '/project/.aperant/specs/003-cancel-v2';
const specDir2 = '/project/.auto-claude/specs/003-cancel-v2';
const watchPromise = fw.watch(taskId, specDir2);
// While watch() is in-flight, call unwatch().
@@ -231,8 +231,8 @@ describe('FileWatcher concurrency', () => {
it('should cancel pending watch() calls and clear pendingWatches', async () => {
const taskId1 = 'task-4a';
const taskId2 = 'task-4b';
const specDir1 = '/project/.aperant/specs/004a';
const specDir2 = '/project/.aperant/specs/004b';
const specDir1 = '/project/.auto-claude/specs/004a';
const specDir2 = '/project/.auto-claude/specs/004b';
// Set up slow-close scenario for taskId1 (so watch() is in-flight).
await fw.watch(taskId1, specDir1);
@@ -243,7 +243,7 @@ describe('FileWatcher concurrency', () => {
);
// Start a new watch for taskId1 with a different specDir — this is now in-flight.
const newSpecDir1 = '/project/.aperant/specs/004a-v2';
const newSpecDir1 = '/project/.auto-claude/specs/004a-v2';
const watchPromise1 = fw.watch(taskId1, newSpecDir1);
// Start a fresh watch for taskId2.
@@ -262,7 +262,7 @@ describe('FileWatcher concurrency', () => {
// pendingWatches should be cleared (we verify indirectly: a fresh
// watch() call for taskId1 must succeed without treating it as a duplicate).
const specDirFresh = path.join('/project', '.aperant', 'specs', '004a-fresh');
const specDirFresh = path.join('/project', '.auto-claude', 'specs', '004a-fresh');
await fw.watch(taskId1, specDirFresh);
expect(fw.isWatching(taskId1)).toBe(true);
expect(fw.getWatchedSpecDir(taskId1)).toBe(specDirFresh);
@@ -275,7 +275,7 @@ describe('FileWatcher concurrency', () => {
describe('getWatchedSpecDir()', () => {
it('returns the specDir that was passed to watch()', async () => {
const taskId = 'task-5';
const specDir = path.join('/project', '.aperant', 'specs', '005-specdir');
const specDir = path.join('/project', '.auto-claude', 'specs', '005-specdir');
await fw.watch(taskId, specDir);
@@ -288,8 +288,8 @@ describe('FileWatcher concurrency', () => {
it('returns updated specDir after re-watch with different specDir', async () => {
const taskId = 'task-5b';
const specDir1 = path.join('/project', '.aperant', 'specs', '005b-first');
const specDir2 = path.join('/project', '.aperant', 'specs', '005b-second');
const specDir1 = path.join('/project', '.auto-claude', 'specs', '005b-first');
const specDir2 = path.join('/project', '.auto-claude', 'specs', '005b-second');
await fw.watch(taskId, specDir1);
expect(fw.getWatchedSpecDir(taskId)).toBe(specDir1);
@@ -45,7 +45,7 @@ describe('InsightsConfig', () => {
it('should build process env with profile settings', async () => {
const config = new InsightsConfig();
vi.spyOn(config, 'loadAperantEnv').mockReturnValue({ CUSTOM_ENV: '1' });
vi.spyOn(config, 'loadAutoBuildEnv').mockReturnValue({ CUSTOM_ENV: '1' });
const env = await config.getProcessEnv();
@@ -143,7 +143,7 @@ vi.mock("electron", () => {
// Setup test project structure
function setupTestProject(): void {
mkdirSync(TEST_PROJECT_PATH, { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, "aperant", "specs"), { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, "auto-claude", "specs"), { recursive: true });
}
// Cleanup test directories
@@ -419,15 +419,15 @@ describe("IPC Handlers", { timeout: 30000 }, () => {
() => mockMainWindow as never
);
// Create .aperant directory first (before adding project so it gets detected)
mkdirSync(path.join(TEST_PROJECT_PATH, ".aperant", "specs"), { recursive: true });
// Create .auto-claude directory first (before adding project so it gets detected)
mkdirSync(path.join(TEST_PROJECT_PATH, ".auto-claude", "specs"), { recursive: true });
// Add a project - it will detect .aperant
// Add a project - it will detect .auto-claude
const addResult = await ipcMain.invokeHandler("project:add", {}, TEST_PROJECT_PATH);
const projectId = (addResult as { data: { id: string } }).data.id;
// Create a spec directory with implementation plan in .aperant/specs
const specDir = path.join(TEST_PROJECT_PATH, ".aperant", "specs", "001-test-feature");
// Create a spec directory with implementation plan in .auto-claude/specs
const specDir = path.join(TEST_PROJECT_PATH, ".auto-claude", "specs", "001-test-feature");
mkdirSync(specDir, { recursive: true });
writeFileSync(
path.join(specDir, "implementation_plan.json"),
@@ -489,8 +489,8 @@ describe("IPC Handlers", { timeout: 30000 }, () => {
() => mockMainWindow as never
);
// Create .aperant directory first (before adding project so it gets detected)
mkdirSync(path.join(TEST_PROJECT_PATH, ".aperant", "specs"), { recursive: true });
// Create .auto-claude directory first (before adding project so it gets detected)
mkdirSync(path.join(TEST_PROJECT_PATH, ".auto-claude", "specs"), { recursive: true });
// Add a project first
const addResult = await ipcMain.invokeHandler("project:add", {}, TEST_PROJECT_PATH);
@@ -630,7 +630,7 @@ describe("IPC Handlers", { timeout: 30000 }, () => {
await ipcMain.invokeHandler("project:add", {}, TEST_PROJECT_PATH);
// Create a spec/task directory with implementation_plan.json
const specDir = path.join(TEST_PROJECT_PATH, ".aperant", "specs", "task-1");
const specDir = path.join(TEST_PROJECT_PATH, ".auto-claude", "specs", "task-1");
mkdirSync(specDir, { recursive: true });
writeFileSync(
path.join(specDir, "implementation_plan.json"),
@@ -1,172 +0,0 @@
/**
* Tests for needsMigration() and migrateProject() in project-initializer.ts
*/
import { describe, test, expect, vi, beforeEach } from 'vitest';
// ---- fs mock ----
const mockExistingPaths = new Set<string>();
const mockFiles = new Map<string, string>();
vi.mock('fs', () => {
const existsSync = vi.fn((p: string) => mockExistingPaths.has(p));
const renameSync = vi.fn((oldPath: string, newPath: string) => {
// Simulate rename: remove old, add new
mockExistingPaths.delete(oldPath);
mockExistingPaths.add(newPath);
});
const readFileSync = vi.fn((filePath: string, _encoding?: string): string => {
const content = mockFiles.get(filePath);
if (content === undefined) {
const err = new Error(`ENOENT: no such file or directory, open '${filePath}'`) as NodeJS.ErrnoException;
err.code = 'ENOENT';
throw err;
}
return content;
});
const writeFileSync = vi.fn((filePath: string, content: string) => {
mockFiles.set(filePath, content);
});
const appendFileSync = vi.fn((filePath: string, content: string) => {
const existing = mockFiles.get(filePath) ?? '';
mockFiles.set(filePath, existing + content);
});
const mkdirSync = vi.fn();
return {
default: { existsSync, renameSync, readFileSync, writeFileSync, appendFileSync, mkdirSync },
existsSync,
renameSync,
readFileSync,
writeFileSync,
appendFileSync,
mkdirSync,
};
});
// ---- stub heavy transitive deps ----
vi.mock('child_process', () => ({
execFileSync: vi.fn(() => ''),
}));
vi.mock('../cli-tool-manager', () => ({
getToolPath: vi.fn(() => 'git'),
}));
// ---- import after mocks ----
import { needsMigration, migrateProject } from '../project-initializer';
import * as fs from 'fs';
import * as path from 'path';
const PROJECT = '/test/project';
const OLD_PATH = path.join(PROJECT, '.auto-claude');
const NEW_PATH = path.join(PROJECT, '.aperant');
const GITIGNORE = path.join(PROJECT, '.gitignore');
beforeEach(() => {
vi.clearAllMocks();
mockExistingPaths.clear();
mockFiles.clear();
});
// ────────────────────────────────────────────────────────────
// needsMigration()
// ────────────────────────────────────────────────────────────
describe('needsMigration', () => {
test('returns true when .auto-claude exists and .aperant does not', () => {
mockExistingPaths.add(OLD_PATH);
expect(needsMigration(PROJECT)).toBe(true);
});
test('returns false when .aperant already exists', () => {
mockExistingPaths.add(OLD_PATH);
mockExistingPaths.add(NEW_PATH);
expect(needsMigration(PROJECT)).toBe(false);
});
test('returns false when neither exists', () => {
expect(needsMigration(PROJECT)).toBe(false);
});
test('returns false when both exist', () => {
mockExistingPaths.add(OLD_PATH);
mockExistingPaths.add(NEW_PATH);
expect(needsMigration(PROJECT)).toBe(false);
});
});
// ────────────────────────────────────────────────────────────
// migrateProject()
// ────────────────────────────────────────────────────────────
describe('migrateProject', () => {
test('successfully renames .auto-claude to .aperant', () => {
mockExistingPaths.add(OLD_PATH);
const result = migrateProject(PROJECT);
expect(result.success).toBe(true);
expect(fs.renameSync).toHaveBeenCalledWith(OLD_PATH, NEW_PATH);
});
test('returns error when .auto-claude does not exist', () => {
// OLD_PATH not in mockExistingPaths
const result = migrateProject(PROJECT);
expect(result.success).toBe(false);
expect(result.error).toMatch(/No \.auto-claude directory/i);
expect(fs.renameSync).not.toHaveBeenCalled();
});
test('returns error when .aperant already exists', () => {
mockExistingPaths.add(OLD_PATH);
mockExistingPaths.add(NEW_PATH);
const result = migrateProject(PROJECT);
expect(result.success).toBe(false);
expect(result.error).toMatch(/\.aperant directory already exists/i);
expect(fs.renameSync).not.toHaveBeenCalled();
});
test('updates .gitignore entries during migration', () => {
mockExistingPaths.add(OLD_PATH);
mockFiles.set(GITIGNORE, '.auto-claude/\n.auto-claude-security.json\n.auto-claude-status\n');
const result = migrateProject(PROJECT);
expect(result.success).toBe(true);
expect(fs.writeFileSync).toHaveBeenCalled();
// Find the call that wrote to the gitignore
const gitignoreWrite = (fs.writeFileSync as ReturnType<typeof vi.fn>).mock.calls.find(
(call: unknown[]) => call[0] === GITIGNORE
);
expect(gitignoreWrite).toBeDefined();
const writtenContent = gitignoreWrite![1] as string;
expect(writtenContent).toContain('.aperant/');
expect(writtenContent).not.toContain('.auto-claude/');
});
test('handles .gitignore not existing gracefully', () => {
mockExistingPaths.add(OLD_PATH);
// mockFiles has no GITIGNORE entry → readFileSync throws ENOENT
// Should not throw; the catch block swallows the .gitignore read error,
// then ensureGitignoreEntries creates the file via writeFileSync
const result = migrateProject(PROJECT);
expect(result.success).toBe(true);
// ensureGitignoreEntries creates a new .gitignore with .aperant/
const written = (fs.writeFileSync as ReturnType<typeof vi.fn>).mock.calls.find(
(call: unknown[]) => call[0] === GITIGNORE
);
expect(written).toBeDefined();
expect(written![1] as string).toContain('.aperant/');
});
});
@@ -88,25 +88,25 @@ describe('ProjectStore', () => {
expect(project1.id).toBe(project2.id);
});
it('should detect aperant directory if present', async () => {
// Create .aperant directory (the data directory, not source code)
mkdirSync(path.join(TEST_PROJECT_PATH, '.aperant'), { recursive: true });
it('should detect auto-claude directory if present', async () => {
// Create .auto-claude directory (the data directory, not source code)
mkdirSync(path.join(TEST_PROJECT_PATH, '.auto-claude'), { recursive: true });
const { ProjectStore } = await import('../project-store');
const store = new ProjectStore();
const project = store.addProject(TEST_PROJECT_PATH);
expect(project.aperantPath).toBe('.aperant');
expect(project.autoBuildPath).toBe('.auto-claude');
});
it('should set empty aperantPath if not present', async () => {
it('should set empty autoBuildPath if not present', async () => {
const { ProjectStore } = await import('../project-store');
const store = new ProjectStore();
const project = store.addProject(TEST_PROJECT_PATH);
expect(project.aperantPath).toBe('');
expect(project.autoBuildPath).toBe('');
});
it('should persist project to disk', async () => {
@@ -284,8 +284,8 @@ describe('ProjectStore', () => {
});
it('should read tasks from filesystem correctly', async () => {
// Create spec directory structure in .aperant (the data directory)
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '001-test-feature');
// Create spec directory structure in .auto-claude (the data directory)
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '001-test-feature');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -332,7 +332,7 @@ describe('ProjectStore', () => {
});
it('should determine status as backlog when no subtasks completed', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '002-pending');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '002-pending');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -372,7 +372,7 @@ describe('ProjectStore', () => {
});
it('should determine status as ai_review when all subtasks completed', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '003-complete');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '003-complete');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -412,7 +412,7 @@ describe('ProjectStore', () => {
});
it('should determine status as human_review when plan status is human_review', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '004-rejected');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '004-rejected');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -452,7 +452,7 @@ describe('ProjectStore', () => {
});
it('should determine reviewReason from plan when status is human_review', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '005-approved');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '005-approved');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -494,7 +494,7 @@ describe('ProjectStore', () => {
it('should determine status as done when plan status is explicitly done', async () => {
// User explicitly marking task as done via drag-and-drop sets status to done
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '006-done');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '006-done');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -533,7 +533,7 @@ describe('ProjectStore', () => {
});
it('should prefer original task description from requirements.json over plan description', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '007-description-priority');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '007-description-priority');
mkdirSync(specsDir, { recursive: true });
const aiDescription = 'AI-generated implementation plan description';
@@ -587,7 +587,7 @@ describe('ProjectStore', () => {
id: 'test-id-123',
name: 'Preexisting Project',
path: '/test/path',
aperantPath: '',
autoBuildPath: '',
settings: {
model: 'sonnet',
memoryBackend: 'memory',
@@ -633,7 +633,7 @@ describe('ProjectStore', () => {
describe('archiveTasks - multi-location handling', () => {
it('should archive task from main specs directory only', async () => {
// Create spec directory in main location only
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '001-test-task');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '001-test-task');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -666,18 +666,18 @@ describe('ProjectStore', () => {
it('should archive task from BOTH main and worktree locations', async () => {
// Create spec directory in main location
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '002-multi-location');
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '002-multi-location');
mkdirSync(mainSpecsDir, { recursive: true });
// Create spec directory in worktree location
// Worktree path: .aperant/worktrees/tasks/<worktreeName>/.aperant/specs/<taskId>
// Worktree path: .auto-claude/worktrees/tasks/<worktreeName>/.auto-claude/specs/<taskId>
const worktreeDir = path.join(
TEST_PROJECT_PATH,
'.aperant',
'.auto-claude',
'worktrees',
'tasks',
'my-worktree',
'.aperant',
'.auto-claude',
'specs',
'002-multi-location'
);
@@ -724,11 +724,11 @@ describe('ProjectStore', () => {
// Create spec directory ONLY in worktree location (not in main)
const worktreeDir = path.join(
TEST_PROJECT_PATH,
'.aperant',
'.auto-claude',
'worktrees',
'tasks',
'only-worktree',
'.aperant',
'.auto-claude',
'specs',
'003-worktree-only'
);
@@ -765,8 +765,8 @@ describe('ProjectStore', () => {
const { ProjectStore } = await import('../project-store');
const store = new ProjectStore();
// Create .aperant directory so project is recognized
mkdirSync(path.join(TEST_PROJECT_PATH, '.aperant'), { recursive: true });
// Create .auto-claude directory so project is recognized
mkdirSync(path.join(TEST_PROJECT_PATH, '.auto-claude'), { recursive: true });
const project = store.addProject(TEST_PROJECT_PATH);
// Task doesn't exist anywhere
@@ -778,7 +778,7 @@ describe('ProjectStore', () => {
it('should reject path traversal attempts in taskId', async () => {
// Create a valid spec dir
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', 'valid-task');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', 'valid-task');
mkdirSync(specsDir, { recursive: true });
const plan = { feature: 'Test', phases: [] };
@@ -820,16 +820,16 @@ describe('ProjectStore', () => {
describe('unarchiveTasks - multi-location handling', () => {
it('should unarchive task from BOTH main and worktree locations', async () => {
// Create archived task in both locations
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '004-unarchive-test');
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '004-unarchive-test');
mkdirSync(mainSpecsDir, { recursive: true });
const worktreeDir = path.join(
TEST_PROJECT_PATH,
'.aperant',
'.auto-claude',
'worktrees',
'tasks',
'unarchive-worktree',
'.aperant',
'.auto-claude',
'specs',
'004-unarchive-test'
);
@@ -874,7 +874,7 @@ describe('ProjectStore', () => {
describe('cache invalidation', () => {
it('should invalidate cache after archiveTasks', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '005-cache-test');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '005-cache-test');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -915,7 +915,7 @@ describe('ProjectStore', () => {
});
it('should return fresh data after invalidateTasksCache is called', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '006-invalidate-test');
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '006-invalidate-test');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -959,16 +959,16 @@ describe('ProjectStore', () => {
describe('getTasks - worktree deduplication', () => {
it('should not duplicate tasks that exist in both main and worktree', async () => {
// Create same task in both main and worktree
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '007-dedupe-test');
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '007-dedupe-test');
mkdirSync(mainSpecsDir, { recursive: true });
const worktreeDir = path.join(
TEST_PROJECT_PATH,
'.aperant',
'.auto-claude',
'worktrees',
'tasks',
'dedupe-worktree',
'.aperant',
'.auto-claude',
'specs',
'007-dedupe-test'
);
+14 -14
View File
@@ -15,7 +15,7 @@ import {
} from './types';
import type { IdeationConfig } from '../../shared/types';
import { resetStuckSubtasks } from '../ipc-handlers/task/plan-file-utils';
import { APERANT_PATHS, getSpecsDir } from '../../shared/constants';
import { AUTO_BUILD_PATHS, getSpecsDir } from '../../shared/constants';
import { projectStore } from '../project-store';
import { resolveAuth, resolveAuthFromQueue } from '../ai/auth/resolver';
import { resolveModelId } from '../ai/config/phase-config';
@@ -112,10 +112,10 @@ export class AgentManager extends EventEmitter {
}
/**
* Configure paths for Python and aperant source
* Configure paths for Python and auto-claude source
*/
configure(pythonPath?: string, aperantSourcePath?: string): void {
this.processManager.configure(pythonPath, aperantSourcePath);
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
this.processManager.configure(pythonPath, autoBuildSourcePath);
}
/**
@@ -216,11 +216,11 @@ export class AgentManager extends EventEmitter {
// Scan each project for stuck subtasks
for (const project of projects) {
if (!project.aperantPath) {
if (!project.autoBuildPath) {
continue; // Skip projects that haven't been initialized yet
}
const specsDir = path.join(project.path, getSpecsDir(project.aperantPath));
const specsDir = path.join(project.path, getSpecsDir(project.autoBuildPath));
// Check if specs directory exists
if (!existsSync(specsDir)) {
@@ -235,7 +235,7 @@ export class AgentManager extends EventEmitter {
// Process each spec directory
for (const specDirName of specDirs) {
const planPath = path.join(specsDir, specDirName, APERANT_PATHS.IMPLEMENTATION_PLAN);
const planPath = path.join(specsDir, specDirName, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
// Check if implementation_plan.json exists
if (!existsSync(planPath)) {
@@ -336,7 +336,7 @@ export class AgentManager extends EventEmitter {
// Reset stuck subtasks if restarting an existing spec creation task
if (specDir) {
const planPath = path.join(specDir, APERANT_PATHS.IMPLEMENTATION_PLAN);
const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
console.log('[AgentManager] Resetting stuck subtasks before spec creation restart:', planPath);
try {
const { success, resetCount } = await resetStuckSubtasks(planPath);
@@ -375,7 +375,7 @@ export class AgentManager extends EventEmitter {
const resolved = await this.resolveAuthFromProviderQueue(specModelId, preferredProvider);
// Build the serializable session config for the worker
const resolvedSpecDir = specDir ?? path.join(projectPath, '.aperant', 'specs', taskId);
const resolvedSpecDir = specDir ?? path.join(projectPath, '.auto-claude', 'specs', taskId);
const sessionConfig: SerializableSessionConfig = {
agentType: 'spec_orchestrator' as const,
systemPrompt,
@@ -456,7 +456,7 @@ export class AgentManager extends EventEmitter {
// Resolve the spec directory from specId
const project = projectStore.getProjects().find((p) => p.id === projectId || p.path === projectPath);
const specsBaseDir = getSpecsDir(project?.aperantPath);
const specsBaseDir = getSpecsDir(project?.autoBuildPath);
const specDir = path.join(projectPath, specsBaseDir, specId);
// Load model configuration from task_metadata.json if available
@@ -483,7 +483,7 @@ export class AgentManager extends EventEmitter {
baseBranch,
options.useLocalBranch ?? false,
project?.settings?.pushNewBranches !== false,
project?.aperantPath,
project?.autoBuildPath,
);
worktreePath = result.worktreePath;
// Spec dir in the worktree (spec files were copied by createOrGetWorktree)
@@ -578,7 +578,7 @@ export class AgentManager extends EventEmitter {
// Resolve the spec directory from specId
const project = projectStore.getProjects().find((p) => p.id === projectId || p.path === projectPath);
const specsBaseDir = getSpecsDir(project?.aperantPath);
const specsBaseDir = getSpecsDir(project?.autoBuildPath);
const specDir = path.join(projectPath, specsBaseDir, specId);
// Load model configuration from task_metadata.json if available
@@ -820,8 +820,8 @@ export class AgentManager extends EventEmitter {
// Reset stuck subtasks before restart to avoid picking up stale in-progress states
if (context.specId || context.specDir) {
const planPath = context.specDir
? path.join(context.specDir, APERANT_PATHS.IMPLEMENTATION_PLAN)
: path.join(context.projectPath, APERANT_PATHS.SPECS_DIR, context.specId, APERANT_PATHS.IMPLEMENTATION_PLAN);
? path.join(context.specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN)
: path.join(context.projectPath, AUTO_BUILD_PATHS.SPECS_DIR, context.specId, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
console.log('[AgentManager] Resetting stuck subtasks before restart:', planPath);
try {
@@ -48,7 +48,7 @@ vi.mock('child_process', async (importOriginal) => {
// Mock project-initializer to avoid child_process.execSync issues
vi.mock('../project-initializer', () => ({
getAperantPath: vi.fn(() => '/fake/aperant'),
getAutoBuildPath: vi.fn(() => '/fake/auto-build'),
isInitialized: vi.fn(() => true),
initializeProject: vi.fn(),
getProjectStorePath: vi.fn(() => '/fake/store/path')
@@ -126,7 +126,7 @@ vi.mock('../env-utils', () => ({
getAugmentedEnv: vi.fn(() => ({ ...process.env }))
}));
// Mock fs.existsSync for getAperantSourcePath path validation
// Mock fs.existsSync for getAutoBuildSourcePath path validation
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
return {
@@ -135,10 +135,10 @@ vi.mock('fs', async (importOriginal) => {
// Normalize path separators for cross-platform compatibility
// path.join() uses backslashes on Windows, so we normalize to forward slashes
const normalizedPath = inputPath.replace(/\\/g, '/');
// Return true for the fake aperant path and its expected files
if (normalizedPath === '/fake/aperant' ||
normalizedPath === '/fake/aperant/runners' ||
normalizedPath === '/fake/aperant/runners/spec_runner.py') {
// Return true for the fake auto-build path and its expected files
if (normalizedPath === '/fake/auto-build' ||
normalizedPath === '/fake/auto-build/runners' ||
normalizedPath === '/fake/auto-build/runners/spec_runner.py') {
return true;
}
return false;
+17 -17
View File
@@ -102,7 +102,7 @@ export class AgentProcessManager {
private state: AgentState;
private events: AgentEvents;
private emitter: EventEmitter;
private aperantSourcePath: string = '';
private autoBuildSourcePath: string = '';
constructor(state: AgentState, events: AgentEvents, emitter: EventEmitter) {
this.state = state;
@@ -110,14 +110,14 @@ export class AgentProcessManager {
this.emitter = emitter;
}
configure(_pythonPath?: string, aperantSourcePath?: string): void {
if (aperantSourcePath) {
this.aperantSourcePath = aperantSourcePath;
configure(_pythonPath?: string, autoBuildSourcePath?: string): void {
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
}
}
getAperantSourcePath(): string {
return this.aperantSourcePath;
getAutoBuildSourcePath(): string {
return this.autoBuildSourcePath;
}
/**
@@ -493,31 +493,31 @@ export class AgentProcessManager {
}
/**
* Load environment variables from project's .aperant/.env file
* Load environment variables from project's .auto-claude/.env file
* This contains frontend-configured settings like memory configuration
*/
private loadProjectEnv(projectPath: string): Record<string, string> {
// Find project by path to get aperantPath
// Find project by path to get autoBuildPath
const projects = projectStore.getProjects();
const project = projects.find((p) => p.path === projectPath);
if (!project?.aperantPath) {
if (!project?.autoBuildPath) {
return {};
}
const envPath = path.join(projectPath, project.aperantPath, '.env');
const envPath = path.join(projectPath, project.autoBuildPath, '.env');
return this.parseEnvFile(envPath);
}
/**
* Load environment variables from aperant .env file
* Load environment variables from auto-claude .env file
*/
loadAperantEnv(): Record<string, string> {
if (!this.aperantSourcePath) {
loadAutoBuildEnv(): Record<string, string> {
if (!this.autoBuildSourcePath) {
return {};
}
const envPath = path.join(this.aperantSourcePath, '.env');
const envPath = path.join(this.autoBuildSourcePath, '.env');
return this.parseEnvFile(envPath);
}
@@ -1036,13 +1036,13 @@ export class AgentProcessManager {
* Priority (later sources override earlier):
* 1. App-wide memory settings from settings.json (NEW - enables memory from onboarding)
* 2. Auto-build source .env (prompts directory) - default values
* 3. Project's .aperant/.env - Frontend-configured settings (memory, integrations)
* 3. Project's .auto-claude/.env - Frontend-configured settings (memory, integrations)
* 4. Project settings (useClaudeMd) - Runtime overrides
*/
getCombinedEnv(projectPath: string): Record<string, string> {
const aperantEnv = this.loadAperantEnv();
const autoBuildEnv = this.loadAutoBuildEnv();
const projectFileEnv = this.loadProjectEnv(projectPath);
const projectSettingsEnv = this.getProjectEnvVars(projectPath);
return { ...aperantEnv, ...projectFileEnv, ...projectSettingsEnv };
return { ...autoBuildEnv, ...projectFileEnv, ...projectSettingsEnv };
}
}
+7 -7
View File
@@ -6,7 +6,7 @@ import type { AgentEvents } from './agent-events';
import { AgentProcessManager } from './agent-process';
import { RoadmapConfig } from './types';
import type { IdeationConfig, Idea } from '../../shared/types';
import { APERANT_PATHS } from '../../shared/constants';
import { AUTO_BUILD_PATHS } from '../../shared/constants';
import { detectRateLimit, createSDKRateLimitInfo } from '../rate-limit-detector';
import { debugLog, debugError } from '../../shared/utils/debug-logger';
import { transformIdeaFromSnakeCase, transformSessionFromSnakeCase } from '../ipc-handlers/ideation/transformers';
@@ -83,8 +83,8 @@ export class AgentQueueManager {
isRunning: boolean
): Promise<void> {
try {
const roadmapDir = path.join(projectPath, APERANT_PATHS.ROADMAP_DIR);
const progressPath = path.join(roadmapDir, APERANT_PATHS.GENERATION_PROGRESS);
const roadmapDir = path.join(projectPath, AUTO_BUILD_PATHS.ROADMAP_DIR);
const progressPath = path.join(roadmapDir, AUTO_BUILD_PATHS.GENERATION_PROGRESS);
// Ensure roadmap directory exists
if (!existsSync(roadmapDir)) {
@@ -120,8 +120,8 @@ export class AgentQueueManager {
try {
const progressPath = path.join(
projectPath,
APERANT_PATHS.ROADMAP_DIR,
APERANT_PATHS.GENERATION_PROGRESS
AUTO_BUILD_PATHS.ROADMAP_DIR,
AUTO_BUILD_PATHS.GENERATION_PROGRESS
);
if (existsSync(progressPath)) {
@@ -225,7 +225,7 @@ export class AgentQueueManager {
// which handles both dev (apps/desktop/prompts/) and production (resourcesPath/prompts/)
const promptsDir = resolvePromptsDir();
const outputDir = path.join(projectPath, '.aperant', 'ideation');
const outputDir = path.join(projectPath, '.auto-claude', 'ideation');
// Emit initial progress
this.emitter.emit('ideation-progress', projectId, {
@@ -469,7 +469,7 @@ export class AgentQueueManager {
this.clearRoadmapProgress(projectPath);
// Load and emit the complete roadmap
const roadmapFilePath = path.join(projectPath, '.aperant', 'roadmap', 'roadmap.json');
const roadmapFilePath = path.join(projectPath, '.auto-claude', 'roadmap', 'roadmap.json');
if (existsSync(roadmapFilePath)) {
try {
const content = await fsPromises.readFile(roadmapFilePath, 'utf-8');
+1 -1
View File
@@ -196,7 +196,7 @@ export async function startCodexOAuthFlow(): Promise<CodexAuthResult> {
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('originator', 'aperant');
authUrl.searchParams.set('originator', 'auto-claude');
authUrl.searchParams.set('codex_cli_simplified_flow', 'true');
debugLog('Built authorization URL', { url: authUrl.toString() });
@@ -82,7 +82,7 @@ const FAKE_MODEL = { type: 'language-model', modelId: 'mock-model-id' };
const baseToolContext = {
cwd: '/project',
projectDir: '/project',
specDir: '/project/.aperant/specs/001',
specDir: '/project/.auto-claude/specs/001',
securityProfile: 'standard' as const,
} as unknown as ToolContext;
@@ -87,11 +87,11 @@ describe('AGENT_CONFIGS', () => {
expect(config.thinkingDefault).toBe('low');
});
it('should configure planner with memory and aperant MCP', () => {
it('should configure planner with memory and auto-claude MCP', () => {
const config = AGENT_CONFIGS.planner;
expect(config.mcpServers).toContain('context7');
expect(config.mcpServers).toContain('memory');
expect(config.mcpServers).toContain('aperant');
expect(config.mcpServers).toContain('auto-claude');
expect(config.mcpServersOptional).toContain('linear');
expect(config.thinkingDefault).toBe('high');
});
@@ -197,7 +197,7 @@ describe('mapMcpServerName', () => {
expect(mapMcpServerName('graphiti')).toBe('memory');
expect(mapMcpServerName('graphiti-memory')).toBe('memory');
expect(mapMcpServerName('linear')).toBe('linear');
expect(mapMcpServerName('aperant')).toBe('aperant');
expect(mapMcpServerName('auto-claude')).toBe('auto-claude');
});
it('should return null for unknown names', () => {
@@ -291,12 +291,12 @@ describe('getRequiredMcpServers', () => {
expect(servers).toContain('context7');
});
it('should support per-agent MCP removals but never remove aperant', () => {
it('should support per-agent MCP removals but never remove auto-claude', () => {
const servers = getRequiredMcpServers('coder', {
memoryEnabled: true,
agentMcpRemove: 'aperant,memory',
agentMcpRemove: 'auto-claude,memory',
});
expect(servers).toContain('aperant');
expect(servers).toContain('auto-claude');
expect(servers).not.toContain('memory');
});
});
@@ -36,15 +36,15 @@ const ALL_BUILTIN_TOOLS = [...BASE_READ_TOOLS, ...BASE_WRITE_TOOLS, ...WEB_TOOLS
const SPEC_TOOLS = [...BASE_READ_TOOLS, 'Write', ...WEB_TOOLS] as const;
// =============================================================================
// Aperant MCP Tools (Custom build management)
// Auto-Claude MCP Tools (Custom build management)
// =============================================================================
const TOOL_UPDATE_SUBTASK_STATUS = 'mcp__aperant__update_subtask_status';
const TOOL_GET_BUILD_PROGRESS = 'mcp__aperant__get_build_progress';
const TOOL_RECORD_DISCOVERY = 'mcp__aperant__record_discovery';
const TOOL_RECORD_GOTCHA = 'mcp__aperant__record_gotcha';
const TOOL_GET_SESSION_CONTEXT = 'mcp__aperant__get_session_context';
const TOOL_UPDATE_QA_STATUS = 'mcp__aperant__update_qa_status';
const TOOL_UPDATE_SUBTASK_STATUS = 'mcp__auto-claude__update_subtask_status';
const TOOL_GET_BUILD_PROGRESS = 'mcp__auto-claude__get_build_progress';
const TOOL_RECORD_DISCOVERY = 'mcp__auto-claude__record_discovery';
const TOOL_RECORD_GOTCHA = 'mcp__auto-claude__record_gotcha';
const TOOL_GET_SESSION_CONTEXT = 'mcp__auto-claude__get_session_context';
const TOOL_UPDATE_QA_STATUS = 'mcp__auto-claude__update_qa_status';
// =============================================================================
// External MCP Tools
@@ -246,7 +246,7 @@ export const AGENT_CONFIGS: Record<AgentType, AgentConfig> = {
*/
build_orchestrator: {
tools: [...ALL_BUILTIN_TOOLS, 'SpawnSubagent'],
mcpServers: ['context7', 'memory', 'aperant'],
mcpServers: ['context7', 'memory', 'auto-claude'],
mcpServersOptional: ['linear'],
autoClaudeTools: [
TOOL_GET_BUILD_PROGRESS,
@@ -263,7 +263,7 @@ export const AGENT_CONFIGS: Record<AgentType, AgentConfig> = {
// ═══════════════════════════════════════════════════════════════════════
planner: {
tools: [...ALL_BUILTIN_TOOLS],
mcpServers: ['context7', 'memory', 'aperant'],
mcpServers: ['context7', 'memory', 'auto-claude'],
mcpServersOptional: ['linear'],
autoClaudeTools: [
TOOL_GET_BUILD_PROGRESS,
@@ -274,7 +274,7 @@ export const AGENT_CONFIGS: Record<AgentType, AgentConfig> = {
},
coder: {
tools: [...ALL_BUILTIN_TOOLS],
mcpServers: ['context7', 'memory', 'aperant'],
mcpServers: ['context7', 'memory', 'auto-claude'],
mcpServersOptional: ['linear'],
autoClaudeTools: [
TOOL_UPDATE_SUBTASK_STATUS,
@@ -291,7 +291,7 @@ export const AGENT_CONFIGS: Record<AgentType, AgentConfig> = {
// ═══════════════════════════════════════════════════════════════════════
qa_reviewer: {
tools: [...ALL_BUILTIN_TOOLS],
mcpServers: ['context7', 'memory', 'aperant', 'browser'],
mcpServers: ['context7', 'memory', 'auto-claude', 'browser'],
mcpServersOptional: ['linear'],
autoClaudeTools: [
TOOL_GET_BUILD_PROGRESS,
@@ -302,7 +302,7 @@ export const AGENT_CONFIGS: Record<AgentType, AgentConfig> = {
},
qa_fixer: {
tools: [...ALL_BUILTIN_TOOLS],
mcpServers: ['context7', 'memory', 'aperant', 'browser'],
mcpServers: ['context7', 'memory', 'auto-claude', 'browser'],
mcpServersOptional: ['linear'],
autoClaudeTools: [
TOOL_UPDATE_SUBTASK_STATUS,
@@ -482,7 +482,7 @@ const MCP_SERVER_NAME_MAP: Record<string, string> = {
linear: 'linear',
electron: 'electron',
puppeteer: 'puppeteer',
'aperant': 'aperant',
'auto-claude': 'auto-claude',
};
/**
@@ -593,11 +593,11 @@ export function getRequiredMcpServers(
}
}
// Apply per-agent MCP removals (never remove aperant)
// Apply per-agent MCP removals (never remove auto-claude)
if (options.agentMcpRemove) {
for (const name of options.agentMcpRemove.split(',')) {
const mapped = mapMcpServerName(name.trim(), options.customServerIds);
if (mapped && mapped !== 'aperant') {
if (mapped && mapped !== 'auto-claude') {
const idx = servers.indexOf(mapped);
if (idx !== -1) servers.splice(idx, 1);
}
+1
View File
@@ -147,6 +147,7 @@ export const MODEL_PROVIDER_MAP: Record<string, SupportedProvider> = {
'llama-': 'groq',
'grok-': 'xai',
'glm-': 'zai',
'mascarade-': 'mascarade',
} as const;
// ============================================
+1 -1
View File
@@ -33,7 +33,7 @@ import type {
// ---------------------------------------------------------------------------
function loadProjectIndex(projectDir: string): ProjectIndex {
const indexFile = path.join(projectDir, '.aperant', 'project_index.json');
const indexFile = path.join(projectDir, '.auto-claude', 'project_index.json');
if (fs.existsSync(indexFile)) {
try {
return JSON.parse(fs.readFileSync(indexFile, 'utf8')) as ProjectIndex;
+2 -2
View File
@@ -14,8 +14,8 @@ import type { FileMatch } from './types.js';
/** Directories that should never be searched. */
const SKIP_DIRS = new Set([
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
'.next', '.nuxt', 'target', 'vendor', '.idea', '.vscode',
'.aperant', '.pytest_cache', '.mypy_cache', 'coverage', '.turbo', '.cache',
'.next', '.nuxt', 'target', 'vendor', '.idea', '.vscode', 'auto-claude',
'.auto-claude', '.pytest_cache', '.mypy_cache', 'coverage', '.turbo', '.cache',
'out',
]);
+1 -1
View File
@@ -55,7 +55,7 @@ export interface ServiceInfo {
key_directories?: Record<string, string>;
}
/** Shape of .aperant/project_index.json */
/** Shape of .auto-claude/project_index.json */
export interface ProjectIndex {
services?: Record<string, ServiceInfo>;
[key: string]: unknown;
@@ -195,10 +195,10 @@ describe('createMcpClientsForAgent', () => {
});
it('creates clients for each resolved server config', async () => {
mockGetRequiredMcpServers.mockReturnValueOnce(['context7', 'aperant']);
mockGetRequiredMcpServers.mockReturnValueOnce(['context7', 'auto-claude']);
mockResolveMcpServers.mockReturnValueOnce([
{ ...stdioConfig, id: 'context7' },
{ ...stdioConfig, id: 'aperant' },
{ ...stdioConfig, id: 'auto-claude' },
]);
// Two separate mock instances for the two servers
mockCreateMCPClient
@@ -209,7 +209,7 @@ describe('createMcpClientsForAgent', () => {
expect(clients).toHaveLength(2);
expect(clients[0].serverId).toBe('context7');
expect(clients[1].serverId).toBe('aperant');
expect(clients[1].serverId).toBe('auto-claude');
});
it('skips failed connections without throwing', async () => {
@@ -104,23 +104,23 @@ describe('getMcpServerConfig', () => {
});
});
describe('aperant', () => {
it('returns aperant config with empty specDir as default', () => {
const config = getMcpServerConfig('aperant', {});
describe('auto-claude', () => {
it('returns auto-claude config with empty specDir as default', () => {
const config = getMcpServerConfig('auto-claude', {});
expect(config).not.toBeNull();
expect(config?.id).toBe('aperant');
expect(config?.id).toBe('auto-claude');
});
it('injects SPEC_DIR into transport env', () => {
const config = getMcpServerConfig('aperant', { specDir: '/project/.aperant/specs/001-feature' });
const config = getMcpServerConfig('auto-claude', { specDir: '/project/.auto-claude/specs/001-feature' });
expect(config?.transport.type).toBe('stdio');
if (config?.transport.type === 'stdio') {
expect(config.transport.env?.SPEC_DIR).toBe('/project/.aperant/specs/001-feature');
expect(config.transport.env?.SPEC_DIR).toBe('/project/.auto-claude/specs/001-feature');
}
});
it('uses node command', () => {
const config = getMcpServerConfig('aperant', {});
const config = getMcpServerConfig('auto-claude', {});
if (config?.transport.type === 'stdio') {
expect(config.transport.command).toBe('node');
}
@@ -174,9 +174,9 @@ describe('resolveMcpServers', () => {
expect(configs[0].id).toBe('memory');
});
it('passes specDir through to aperant config', () => {
const specDir = '/my-project/.aperant/specs/042-auth';
const configs = resolveMcpServers(['aperant'], { specDir });
it('passes specDir through to auto-claude config', () => {
const specDir = '/my-project/.auto-claude/specs/042-auth';
const configs = resolveMcpServers(['auto-claude'], { specDir });
expect(configs).toHaveLength(1);
if (configs[0].transport.type === 'stdio') {
expect(configs[0].transport.env?.SPEC_DIR).toBe(specDir);
+7 -7
View File
@@ -100,19 +100,19 @@ const PUPPETEER_SERVER: McpServerConfig = {
};
/**
* Aperant MCP server - custom build management tools.
* Auto-Claude MCP server - custom build management tools.
* Used by planner, coder, and QA agents for build progress tracking.
*/
function createAperantServer(specDir: string): McpServerConfig {
function createAutoClaudeServer(specDir: string): McpServerConfig {
return {
id: 'aperant',
id: 'auto-claude',
name: 'Aperant',
description: 'Build management tools (progress tracking, session context)',
enabledByDefault: true,
transport: {
type: 'stdio',
command: 'node',
args: ['aperant-mcp-server.js'],
args: ['auto-claude-mcp-server.js'],
env: { SPEC_DIR: specDir },
},
};
@@ -124,7 +124,7 @@ function createAperantServer(specDir: string): McpServerConfig {
/** Options for resolving MCP server configurations */
export interface McpRegistryOptions {
/** Spec directory for aperant MCP server */
/** Spec directory for auto-claude MCP server */
specDir?: string;
/** Memory MCP server URL (if enabled) */
memoryMcpUrl?: string;
@@ -175,9 +175,9 @@ export function getMcpServerConfig(
case 'puppeteer':
return PUPPETEER_SERVER;
case 'aperant': {
case 'auto-claude': {
const specDir = options.specDir ?? '';
return createAperantServer(specDir);
return createAutoClaudeServer(specDir);
}
default:
+1 -1
View File
@@ -49,7 +49,7 @@ export type McpServerId =
| 'memory'
| 'electron'
| 'puppeteer'
| 'aperant';
| 'auto-claude';
/** Configuration for a single MCP server */
export interface McpServerConfig {
@@ -46,7 +46,7 @@ export class IncrementalIndexer {
ignored: [
'**/node_modules/**',
'**/.git/**',
'**/.aperant/**',
'**/.auto-claude/**',
'**/dist/**',
'**/build/**',
'**/.next/**',
@@ -313,7 +313,7 @@ export class IncrementalIndexer {
private collectSupportedFiles(dir: string, extensions: string[]): string[] {
const files: string[] = [];
const IGNORED_DIRS = new Set([
'node_modules', '.git', '.aperant', 'dist', 'build',
'node_modules', '.git', '.auto-claude', 'dist', 'build',
'.next', '__pycache__', 'target', '.venv',
]);
@@ -7,7 +7,7 @@
*
* Manages:
* - Baseline capture when worktrees are created
* - File content snapshots in .aperant/baselines/
* - File content snapshots in .auto-claude/baselines/
* - Task modification tracking with semantic analysis
* - Persistence of evolution data
*/
@@ -198,7 +198,7 @@ export class FileEvolutionTracker {
storageDir?: string,
semanticAnalyzer?: SemanticAnalyzer,
) {
const resolvedStorageDir = storageDir ?? path.join(projectDir, '.aperant');
const resolvedStorageDir = storageDir ?? path.join(projectDir, '.auto-claude');
this.storage = new EvolutionStorage(projectDir, resolvedStorageDir);
this.analyzer = semanticAnalyzer ?? new SemanticAnalyzer();
this.evolutions = this.storage.loadEvolutions();
@@ -110,8 +110,8 @@ function getFileFromBranch(
function findWorktree(projectDir: string, taskId: string): string | undefined {
// Common worktree locations
const candidates = [
path.join(projectDir, '.aperant', 'worktrees', taskId),
path.join(projectDir, '.aperant', 'worktrees', 'tasks', taskId),
path.join(projectDir, '.auto-claude', 'worktrees', taskId),
path.join(projectDir, '.auto-claude', 'worktrees', 'tasks', taskId),
];
for (const c of candidates) {
if (fs.existsSync(c)) return c;
@@ -254,7 +254,7 @@ export class MergeOrchestrator {
dryRun?: boolean;
}) {
this.projectDir = path.resolve(options.projectDir);
this.storageDir = options.storageDir ?? path.join(this.projectDir, '.aperant');
this.storageDir = options.storageDir ?? path.join(this.projectDir, '.auto-claude');
this.enableAi = options.enableAi ?? true;
this.dryRun = options.dryRun ?? false;
this.aiResolver = options.aiResolver;
@@ -313,7 +313,7 @@ function getCommitInfo(commitHash: string, cwd: string): Record<string, string>
function getWorktreeFileContent(taskId: string, filePath: string, projectDir: string): string {
// Try common worktree locations
const worktreePath = path.join(projectDir, '.aperant', 'worktrees', taskId, filePath);
const worktreePath = path.join(projectDir, '.auto-claude', 'worktrees', taskId, filePath);
if (fs.existsSync(worktreePath)) {
try {
return fs.readFileSync(worktreePath, 'utf8');
@@ -369,7 +369,7 @@ export class FileTimelineTracker {
constructor(projectPath: string, storagePath?: string) {
this.projectPath = path.resolve(projectPath);
const resolvedStoragePath = storagePath ?? path.join(this.projectPath, '.aperant');
const resolvedStoragePath = storagePath ?? path.join(this.projectPath, '.auto-claude');
this.persistence = new TimelinePersistence(resolvedStoragePath);
this.timelines = this.persistence.loadAllTimelines();
}
@@ -49,7 +49,7 @@ import type { SessionResult } from '../../session/types';
// Helpers
// ---------------------------------------------------------------------------
const SPEC_DIR = '/project/.aperant/specs/001-feature';
const SPEC_DIR = '/project/.auto-claude/specs/001-feature';
const PROJECT_DIR = '/project';
function completedPlan(qaStatus?: 'approved' | 'rejected' | 'unknown') {
@@ -207,7 +207,7 @@ describe('generateEscalationReport', () => {
// ---------------------------------------------------------------------------
describe('generateManualTestPlan', () => {
const SPEC_DIR = '/project/.aperant/specs/001-feature';
const SPEC_DIR = '/project/.auto-claude/specs/001-feature';
const PROJECT_DIR = '/project';
beforeEach(() => {
@@ -33,7 +33,7 @@ import type { BuildCheckpoint, FailureType } from '../recovery-manager';
// ---------------------------------------------------------------------------
const PROJECT_DIR = path.join(path.sep, 'project');
const SPEC_DIR = path.join(PROJECT_DIR, '.aperant', 'specs', '001-feature');
const SPEC_DIR = path.join(PROJECT_DIR, '.auto-claude', 'specs', '001-feature');
const MEMORY_DIR = path.join(SPEC_DIR, 'memory');
const ATTEMPT_HISTORY_PATH = path.join(MEMORY_DIR, 'attempt_history.json');
@@ -77,7 +77,7 @@ const PHASE_CONFIG_MAP: Record<BuildPhase, Phase> = {
/** Configuration for the build orchestrator */
export interface BuildOrchestratorConfig {
/** Spec directory path (e.g., .aperant/specs/001-feature/) */
/** Spec directory path (e.g., .auto-claude/specs/001-feature/) */
specDir: string;
/** Project root directory */
projectDir: string;
+2 -2
View File
@@ -40,8 +40,8 @@ import type {
// Constants
// ---------------------------------------------------------------------------
const PROFILE_FILENAME = '.aperant-security.json';
const CUSTOM_ALLOWLIST_FILENAME = '.aperant-allowlist';
const PROFILE_FILENAME = '.auto-claude-security.json';
const CUSTOM_ALLOWLIST_FILENAME = '.auto-claude-allowlist';
const HASH_FILES = [
'package.json',
@@ -35,7 +35,7 @@ const SKIP_DIRS = new Set([
'.nuxt',
'target',
'vendor',
'.aperant',
'.auto-claude',
'coverage',
'.nyc_output',
]);
@@ -402,10 +402,10 @@ function validateBranchName(branch: string | null | undefined): string | null {
// =============================================================================
/**
* Load project_index.json from the project's .aperant directory.
* Load project_index.json from the project's .auto-claude directory.
*/
export function loadProjectIndex(projectDir: string): Record<string, unknown> {
const indexPath = join(projectDir, '.aperant', 'project_index.json');
const indexPath = join(projectDir, '.auto-claude', 'project_index.json');
if (!existsSync(indexPath)) return {};
try {
return JSON.parse(readFileSync(indexPath, 'utf-8')) as Record<string, unknown>;
@@ -28,8 +28,8 @@ import type {
/** Patterns to detect worktree isolation */
const WORKTREE_PATH_PATTERNS = [
/[/\\]\.aperant[/\\]worktrees[/\\]tasks[/\\]/,
/[/\\]\.aperant[/\\]github[/\\]pr[/\\]worktrees[/\\]/,
/[/\\]\.auto-claude[/\\]worktrees[/\\]tasks[/\\]/,
/[/\\]\.auto-claude[/\\]github[/\\]pr[/\\]worktrees[/\\]/,
/[/\\]\.worktrees[/\\]/,
];
@@ -106,7 +106,7 @@ function getRelativeSpecPath(specDir: string, projectDir: string): string {
// Fallback: just use the spec dir name
const parts = resolvedSpec.split(/[/\\]/);
return `./aperant/specs/${parts[parts.length - 1]}`;
return `./auto-claude/specs/${parts[parts.length - 1]}`;
}
/**
@@ -331,7 +331,7 @@ export async function generateSubtaskPrompt(config: SubtaskPromptConfig): Promis
`5. **Commit your changes:**\n` +
` \`\`\`bash\n` +
` git add .\n` +
` git commit -m "aperant: ${subtask.id} - ${subtask.description.slice(0, 50)}"\n` +
` git commit -m "auto-claude: ${subtask.id} - ${subtask.description.slice(0, 50)}"\n` +
` \`\`\`\n` +
`6. **Update the plan** - set this subtask's status to "completed" in implementation_plan.json\n\n` +
`## Quality Checklist\n\n` +
@@ -156,6 +156,24 @@ function createProviderInstance(config: ProviderConfig) {
});
}
case SupportedProvider.Mascarade: {
// Mascarade LLM orchestration engine — OpenAI-compatible API
// Default: http://localhost:8100/v1 (Tower) or configurable
let mascaradeBaseURL = baseURL ?? 'http://localhost:8100/v1';
if (!mascaradeBaseURL.endsWith('/v1')) {
mascaradeBaseURL = mascaradeBaseURL.replace(/\/+$/, '') + '/v1';
}
return createOpenAICompatible({
name: 'mascarade',
apiKey: apiKey ?? 'mascarade-local',
baseURL: mascaradeBaseURL,
headers: {
...headers,
'Authorization': `Bearer ${apiKey ?? 'mascarade-local'}`,
},
});
}
default: {
const _exhaustive: never = provider;
throw new Error(`Unsupported provider: ${_exhaustive}`);
@@ -21,6 +21,7 @@ export const SupportedProvider = {
OpenRouter: 'openrouter',
ZAI: 'zai',
Ollama: 'ollama',
Mascarade: 'mascarade',
} as const;
export type SupportedProvider = (typeof SupportedProvider)[keyof typeof SupportedProvider];
@@ -161,7 +161,7 @@ describe('generateCommitMessage', () => {
// ---------------------------------------------------------------------------
it('reads spec.md for title when spec directory exists', async () => {
// Spec directory at .aperant/specs/001-add-feature
// Spec directory at .auto-claude/specs/001-add-feature
mockExistsSync.mockImplementation((p: string) => {
const normalized = p.replace(/\\/g, '/');
if (normalized.includes('specs/001-add-feature')) return true;
@@ -71,7 +71,7 @@ function makeStream(parts: Array<Record<string, unknown>>) {
function baseConfig(overrides: Partial<IdeationConfig> = {}): IdeationConfig {
return {
projectDir: '/project',
outputDir: '/project/.aperant/ideation',
outputDir: '/project/.auto-claude/ideation',
promptsDir: '/app/prompts',
ideationType: 'code_improvements',
...overrides,
@@ -285,7 +285,7 @@ describe('runIdeation', () => {
mockStreamText.mockReturnValue(makeStream([]));
await runIdeation(
baseConfig({ projectDir: '/my/project', outputDir: '/my/project/.aperant/ideation' }),
baseConfig({ projectDir: '/my/project', outputDir: '/my/project/.auto-claude/ideation' }),
);
// The system prompt passed to streamText should contain the project dir
@@ -124,7 +124,7 @@ const VALID_ROADMAP_JSON = JSON.stringify({
function baseConfig(overrides: Partial<RoadmapConfig> = {}): RoadmapConfig {
return {
projectDir: '/project',
outputDir: '/project/.aperant/roadmap',
outputDir: '/project/.auto-claude/roadmap',
...overrides,
};
}
@@ -277,7 +277,7 @@ describe('runRoadmapGeneration', () => {
});
mockStreamText.mockReturnValue(makeStream([]));
await runRoadmapGeneration(baseConfig({ outputDir: '/project/.aperant/roadmap' }));
await runRoadmapGeneration(baseConfig({ outputDir: '/project/.auto-claude/roadmap' }));
expect(mockMkdirSync).toHaveBeenCalledWith(
expect.stringContaining('roadmap'),
@@ -388,7 +388,7 @@ describe('runRoadmapGeneration', () => {
// mkdirSync should have been called with the default path
expect(mockMkdirSync).toHaveBeenCalledWith(
expect.stringContaining('.aperant'),
expect.stringContaining('.auto-claude'),
expect.anything(),
);
});
@@ -236,7 +236,10 @@ export async function generateCommitMessage(
} = config;
// Find spec directory
const specDir = join(projectDir, '.aperant', 'specs', specName);
let specDir = join(projectDir, '.auto-claude', 'specs', specName);
if (!existsSync(specDir)) {
specDir = join(projectDir, 'auto-claude', 'specs', specName);
}
// Get context from spec files
const specContext = existsSync(specDir) ? getSpecContext(specDir) : {
@@ -141,7 +141,7 @@ function gatherPRContext(
* Extract a brief summary from the spec file for fallback PR body.
*/
function extractSpecSummary(projectDir: string, specId: string): string {
const specFile = join(projectDir, '.aperant', 'specs', specId, 'spec.md');
const specFile = join(projectDir, '.auto-claude', 'specs', specId, 'spec.md');
if (!existsSync(specFile)) {
return `Implements ${specId}`;
}
+1 -1
View File
@@ -160,7 +160,7 @@ export async function runIdeation(
const toolContext: ToolContext = {
cwd: projectDir,
projectDir,
specDir: join(projectDir, '.aperant', 'specs'),
specDir: join(projectDir, '.auto-claude', 'specs'),
securityProfile: null as unknown as SecurityProfile,
abortSignal,
};
+4 -4
View File
@@ -99,7 +99,7 @@ function loadProjectContext(projectDir: string): string {
const contextParts: string[] = [];
// Load project index if available
const indexPath = join(projectDir, '.aperant', 'project_index.json');
const indexPath = join(projectDir, '.auto-claude', 'project_index.json');
if (existsSync(indexPath)) {
const index = safeParseJson<Record<string, unknown>>(readFileSync(indexPath, 'utf-8'));
if (index) {
@@ -116,7 +116,7 @@ function loadProjectContext(projectDir: string): string {
}
// Load roadmap if available
const roadmapPath = join(projectDir, '.aperant', 'roadmap', 'roadmap.json');
const roadmapPath = join(projectDir, '.auto-claude', 'roadmap', 'roadmap.json');
if (existsSync(roadmapPath)) {
const roadmap = safeParseJson<Record<string, unknown>>(readFileSync(roadmapPath, 'utf-8'));
if (roadmap) {
@@ -132,7 +132,7 @@ function loadProjectContext(projectDir: string): string {
}
// Load existing tasks
const tasksPath = join(projectDir, '.aperant', 'specs');
const tasksPath = join(projectDir, '.auto-claude', 'specs');
if (existsSync(tasksPath)) {
try {
const taskDirs = readdirSync(tasksPath, { withFileTypes: true })
@@ -246,7 +246,7 @@ export async function runInsightsQuery(
const toolContext: ToolContext = {
cwd: projectDir,
projectDir,
specDir: join(projectDir, '.aperant', 'specs'),
specDir: join(projectDir, '.auto-claude', 'specs'),
securityProfile: null as unknown as SecurityProfile,
abortSignal,
};
+3 -3
View File
@@ -39,7 +39,7 @@ const MAX_STEPS_PER_PHASE = 30;
export interface RoadmapConfig {
/** Project directory path */
projectDir: string;
/** Output directory for roadmap files (defaults to .aperant/roadmap/) */
/** Output directory for roadmap files (defaults to .auto-claude/roadmap/) */
outputDir?: string;
/** Model shorthand (defaults to 'sonnet') */
modelShorthand?: ModelShorthand;
@@ -443,7 +443,7 @@ export async function runRoadmapGeneration(
abortSignal,
} = config;
const outputDir = config.outputDir ?? join(projectDir, '.aperant', 'roadmap');
const outputDir = config.outputDir ?? join(projectDir, '.auto-claude', 'roadmap');
// Ensure output directory exists
if (!existsSync(outputDir)) {
@@ -454,7 +454,7 @@ export async function runRoadmapGeneration(
const toolContext: ToolContext = {
cwd: projectDir,
projectDir,
specDir: join(projectDir, '.aperant', 'specs'),
specDir: join(projectDir, '.auto-claude', 'specs'),
securityProfile: null as unknown as SecurityProfile,
abortSignal,
};
@@ -160,7 +160,7 @@ export async function validateAndNormalizeJsonFile<T>(
if (result.valid && result.data) {
// Write back the coerced data so downstream consumers get canonical field names.
// Use a secure temp file + atomic rename to avoid TOCTOU races on the target path.
const tempDir = await mkdtemp(join(tmpdir(), 'aperant-normalize-'));
const tempDir = await mkdtemp(join(tmpdir(), 'auto-claude-normalize-'));
const tempFile = join(tempDir, 'output.json');
try {
await writeFile(tempFile, JSON.stringify(result.data, null, 2));
@@ -338,7 +338,7 @@ export async function repairJsonWithLLM<T>(
const coerced = schema.safeParse(result.output);
if (coerced.success) {
// Use a secure temp file + atomic rename to avoid TOCTOU races
const tempDir = await mkdtemp(join(tmpdir(), 'aperant-repair-'));
const tempDir = await mkdtemp(join(tmpdir(), 'auto-claude-repair-'));
const tempFile = join(tempDir, 'output.json');
try {
await writeFile(tempFile, JSON.stringify(coerced.data, null, 2));
@@ -2,7 +2,7 @@
* Security Profile Management
* ============================
*
* Loads and caches project security profiles from .aperant/ config.
* Loads and caches project security profiles from .auto-claude/ config.
* Provides SecurityProfile instances consumed by bash-validator.ts.
*
* NOTE: With the denylist security model, SecurityProfile command sets are no
@@ -23,8 +23,8 @@ import type { SecurityProfile } from './bash-validator';
// Constants
// ---------------------------------------------------------------------------
const PROFILE_FILENAME = '.aperant-security.json';
const ALLOWLIST_FILENAME = '.aperant-allowlist';
const PROFILE_FILENAME = '.auto-claude-security.json';
const ALLOWLIST_FILENAME = '.auto-claude-allowlist';
// ---------------------------------------------------------------------------
// Cache state
@@ -86,7 +86,7 @@ const BLOCKED_PROCESS_NAMES = new Set([
// -- Self-protection (don't let the agent kill its own host) --
'electron',
'Electron',
'aperant',
'auto-claude',
'Aperant',
]);
@@ -33,7 +33,7 @@ describe('ProgressTracker', () => {
type: 'tool-call',
toolName: 'Write',
toolCallId: 'c1',
args: { file_path: '/project/.aperant/specs/001/implementation_plan.json' },
args: { file_path: '/project/.auto-claude/specs/001/implementation_plan.json' },
});
expect(result).not.toBeNull();
@@ -251,12 +251,12 @@ describe('getRequiredMcpServers (registry)', () => {
expect(servers).toContain('context7');
});
it('should support per-agent MCP REMOVE overrides but protect aperant', () => {
it('should support per-agent MCP REMOVE overrides but protect auto-claude', () => {
const servers = getRequiredMcpServers('coder', {
memoryEnabled: true,
mcpConfig: { AGENT_MCP_coder_REMOVE: 'aperant,memory' },
mcpConfig: { AGENT_MCP_coder_REMOVE: 'auto-claude,memory' },
});
expect(servers).toContain('aperant');
expect(servers).toContain('auto-claude');
expect(servers).not.toContain('memory');
});
});
@@ -3,9 +3,9 @@
* =======================
*
* Reports current build progress from implementation_plan.json.
* See apps/desktop/src/main/ai/tools/aperant/get-build-progress.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/auto-claude/get-build-progress.ts for the TypeScript implementation.
*
* Tool name: mcp__aperant__get_build_progress
* Tool name: mcp__auto-claude__get_build_progress
*/
import * as fs from 'node:fs';
@@ -50,7 +50,7 @@ interface ImplementationPlan {
export const getBuildProgressTool = Tool.define({
metadata: {
name: 'mcp__aperant__get_build_progress',
name: 'mcp__auto-claude__get_build_progress',
description:
'Get the current build progress including completed subtasks, pending subtasks, and next subtask to work on.',
permission: ToolPermission.ReadOnly,
@@ -7,9 +7,9 @@
* - memory/gotchas.md gotchas & pitfalls
* - memory/patterns.md code patterns
*
* See apps/desktop/src/main/ai/tools/aperant/get-session-context.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/auto-claude/get-session-context.ts for the TypeScript implementation.
*
* Tool name: mcp__aperant__get_session_context
* Tool name: mcp__auto-claude__get_session_context
*/
import * as fs from 'node:fs';
@@ -40,7 +40,7 @@ interface CodebaseMap {
export const getSessionContextTool = Tool.define({
metadata: {
name: 'mcp__aperant__get_session_context',
name: 'mcp__auto-claude__get_session_context',
description:
'Get context from previous sessions including codebase discoveries, gotchas, and patterns. Call this at the start of a session to pick up where the last session left off.',
permission: ToolPermission.ReadOnly,
@@ -1,11 +1,11 @@
/**
* Aperant Custom Tools
* ====================
* Auto-Claude Custom Tools
* ========================
*
* Barrel export for all aperant builtin tools.
* Barrel export for all auto-claude builtin tools.
* These replace the Python tools_pkg/tools/* implementations.
*
* Tool names follow the mcp__aperant__* convention to match the
* Tool names follow the mcp__auto-claude__* convention to match the
* TOOL_* constants in registry.ts and AGENT_CONFIGS autoClaudeTools arrays.
*/
@@ -3,9 +3,9 @@
* =====================
*
* Records a codebase discovery to session memory (codebase_map.json).
* See apps/desktop/src/main/ai/tools/aperant/record-discovery.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/auto-claude/record-discovery.ts for the TypeScript implementation.
*
* Tool name: mcp__aperant__record_discovery
* Tool name: mcp__auto-claude__record_discovery
*/
import * as fs from 'node:fs';
@@ -44,7 +44,7 @@ interface CodebaseMap {
export const recordDiscoveryTool = Tool.define({
metadata: {
name: 'mcp__aperant__record_discovery',
name: 'mcp__auto-claude__record_discovery',
description:
'Record a codebase discovery to session memory. Use this when you learn something important about the codebase structure or behavior.',
permission: ToolPermission.Auto,
@@ -3,9 +3,9 @@
* ==================
*
* Records a gotcha or pitfall to specDir/memory/gotchas.md.
* See apps/desktop/src/main/ai/tools/aperant/record-gotcha.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/auto-claude/record-gotcha.ts for the TypeScript implementation.
*
* Tool name: mcp__aperant__record_gotcha
* Tool name: mcp__auto-claude__record_gotcha
*/
import * as fs from 'node:fs';
@@ -33,7 +33,7 @@ const inputSchema = z.object({
export const recordGotchaTool = Tool.define({
metadata: {
name: 'mcp__aperant__record_gotcha',
name: 'mcp__auto-claude__record_gotcha',
description:
'Record a gotcha or pitfall to avoid. Use this when you encounter something that future sessions should know about to avoid repeating mistakes.',
permission: ToolPermission.Auto,
@@ -3,9 +3,9 @@
* =====================
*
* Updates the QA sign-off status in implementation_plan.json.
* See apps/desktop/src/main/ai/tools/aperant/update-qa-status.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/auto-claude/update-qa-status.ts for the TypeScript implementation.
*
* Tool name: mcp__aperant__update_qa_status
* Tool name: mcp__auto-claude__update_qa_status
*
* IMPORTANT: Do NOT write plan["status"] or plan["planStatus"] here.
* The frontend XState task state machine owns status transitions.
@@ -68,7 +68,7 @@ interface ImplementationPlan {
export const updateQaStatusTool = Tool.define({
metadata: {
name: 'mcp__aperant__update_qa_status',
name: 'mcp__auto-claude__update_qa_status',
description:
'Update the QA sign-off status in implementation_plan.json. Use this after completing a QA review to record the outcome.',
permission: ToolPermission.Auto,
@@ -3,9 +3,9 @@
* ==========================
*
* Updates the status of a subtask in implementation_plan.json.
* See apps/desktop/src/main/ai/tools/aperant/update-subtask-status.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/auto-claude/update-subtask-status.ts for the TypeScript implementation.
*
* Tool name: mcp__aperant__update_subtask_status
* Tool name: mcp__auto-claude__update_subtask_status
*/
import * as fs from 'node:fs';
@@ -82,7 +82,7 @@ function updateSubtaskInPlan(
export const updateSubtaskStatusTool = Tool.define({
metadata: {
name: 'mcp__aperant__update_subtask_status',
name: 'mcp__auto-claude__update_subtask_status',
description:
'Update the status of a subtask in implementation_plan.json. Use this when completing or starting a subtask.',
permission: ToolPermission.Auto,
@@ -24,7 +24,7 @@ export class FetchBrowseProvider implements BrowseProvider {
const response = await fetch(url, {
signal: controller.signal,
headers: {
'User-Agent': 'Aperant/1.0',
'User-Agent': 'AutoClaude/1.0',
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
});
+7 -7
View File
@@ -45,12 +45,12 @@ export {
export const BASE_READ_TOOLS = ['Read', 'Glob', 'Grep'] as const;
export const BASE_WRITE_TOOLS = ['Write', 'Edit', 'Bash'] as const;
export const WEB_TOOLS = ['WebFetch', 'WebSearch'] as const;
export const TOOL_UPDATE_SUBTASK_STATUS = 'mcp__aperant__update_subtask_status';
export const TOOL_GET_BUILD_PROGRESS = 'mcp__aperant__get_build_progress';
export const TOOL_RECORD_DISCOVERY = 'mcp__aperant__record_discovery';
export const TOOL_RECORD_GOTCHA = 'mcp__aperant__record_gotcha';
export const TOOL_GET_SESSION_CONTEXT = 'mcp__aperant__get_session_context';
export const TOOL_UPDATE_QA_STATUS = 'mcp__aperant__update_qa_status';
export const TOOL_UPDATE_SUBTASK_STATUS = 'mcp__auto-claude__update_subtask_status';
export const TOOL_GET_BUILD_PROGRESS = 'mcp__auto-claude__get_build_progress';
export const TOOL_RECORD_DISCOVERY = 'mcp__auto-claude__record_discovery';
export const TOOL_RECORD_GOTCHA = 'mcp__auto-claude__record_gotcha';
export const TOOL_GET_SESSION_CONTEXT = 'mcp__auto-claude__get_session_context';
export const TOOL_UPDATE_QA_STATUS = 'mcp__auto-claude__update_qa_status';
// =============================================================================
// MCP Config for dynamic server resolution
@@ -221,7 +221,7 @@ export function getRequiredMcpServers(
const removals = removeValue.split(',').map((s) => s.trim()).filter(Boolean);
for (const server of removals) {
const mapped = mapMcpServerName(server, customServerIds);
if (mapped && mapped !== 'aperant') {
if (mapped && mapped !== 'auto-claude') {
servers = servers.filter((s) => s !== mapped);
}
}
+2 -2
View File
@@ -44,7 +44,7 @@ export interface TruncationResult {
*
* @param output - The raw tool output string
* @param toolName - Name of the tool (for spillover filename)
* @param projectDir - Project directory (spillover written to .aperant/tool-output/)
* @param projectDir - Project directory (spillover written to .auto-claude/tool-output/)
* @param maxBytes - Override max bytes limit (default: MAX_BYTES)
* @returns TruncationResult with potentially truncated content
*/
@@ -67,7 +67,7 @@ export function truncateToolOutput(
}
// Exceeds limits — spill to disk
const spilloverDir = path.join(projectDir, '.aperant', 'tool-output');
const spilloverDir = path.join(projectDir, '.auto-claude', 'tool-output');
try {
fs.mkdirSync(spilloverDir, { recursive: true });
} catch {
+1 -1
View File
@@ -23,7 +23,7 @@ export interface ToolContext {
cwd: string;
/** Root directory of the project being worked on */
projectDir: string;
/** Spec directory for the current task (e.g., .aperant/specs/001-feature/) */
/** Spec directory for the current task (e.g., .auto-claude/specs/001-feature/) */
specDir: string;
/** Security profile governing command allowlists */
securityProfile: SecurityProfile;
@@ -7,9 +7,9 @@
*
* Creates and manages git worktrees for autonomous task execution.
* Each task runs in an isolated worktree at:
* {projectPath}/.aperant/worktrees/tasks/{specId}/
* {projectPath}/.auto-claude/worktrees/tasks/{specId}/
* on branch:
* aperant/{specId}
* auto-claude/{specId}
*
* The function is idempotent calling it repeatedly with the same specId
* returns the existing worktree without error.
@@ -78,7 +78,7 @@ export interface WorktreeResult {
* the remote ref (preserves gitignored files)
* @param pushNewBranches If true, push the branch to origin and set upstream
* tracking after worktree creation. Defaults to true.
* @param aperantPath Optional custom data directory (e.g. ".aperant").
* @param autoBuildPath Optional custom data directory (e.g. ".auto-claude").
* Passed to getSpecsDir() for spec-copy logic.
*/
export async function createOrGetWorktree(
@@ -87,10 +87,10 @@ export async function createOrGetWorktree(
baseBranch = 'main',
useLocalBranch = false,
pushNewBranches = true,
aperantPath?: string,
autoBuildPath?: string,
): Promise<WorktreeResult> {
const worktreePath = join(projectPath, '.aperant/worktrees/tasks', specId);
const branchName = `aperant/${specId}`;
const worktreePath = join(projectPath, '.auto-claude/worktrees/tasks', specId);
const branchName = `auto-claude/${specId}`;
// ------------------------------------------------------------------
// Step 1: Prune stale worktree references from git's internal records
@@ -239,11 +239,11 @@ export async function createOrGetWorktree(
// ------------------------------------------------------------------
// Step 7: Copy spec directory into the worktree
//
// .aperant/specs/ is gitignored, so it is NOT present in the
// .auto-claude/specs/ is gitignored, so it is NOT present in the
// newly-created worktree checkout. Copy it from the main project so
// that agents can read spec.md, implementation_plan.json, etc.
// ------------------------------------------------------------------
const specsRelDir = getSpecsDir(aperantPath); // e.g. ".aperant/specs"
const specsRelDir = getSpecsDir(autoBuildPath); // e.g. ".auto-claude/specs"
const sourceSpecDir = join(projectPath, specsRelDir, specId);
const destSpecDir = join(worktreePath, specsRelDir, specId);
@@ -35,7 +35,7 @@ describe('ChangelogService - Task Filtering Integration', () => {
// Create temporary test directory
testDir = mkdtempSync(path.join(tmpdir(), 'changelog-test-'));
projectPath = path.join(testDir, 'test-project');
specsDir = path.join(projectPath, '.aperant', 'specs');
specsDir = path.join(projectPath, '.auto-claude', 'specs');
// Create project structure
mkdirSync(projectPath, { recursive: true });
@@ -7,7 +7,7 @@ import { app } from 'electron';
// ESM-compatible __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import { APERANT_PATHS, DEFAULT_CHANGELOG_PATH } from '../../shared/constants';
import { AUTO_BUILD_PATHS, DEFAULT_CHANGELOG_PATH } from '../../shared/constants';
import { getToolPath, getToolInfo } from '../cli-tool-manager';
import type {
ChangelogTask,
@@ -40,7 +40,7 @@ import {
*/
export class ChangelogService extends EventEmitter {
private claudePath: string;
private aperantSourcePath: string = '';
private autoBuildSourcePath: string = '';
private debugEnabled: boolean | null = null;
private generator: ChangelogGenerator | null = null;
private versionSuggester: VersionSuggester | null = null;
@@ -54,7 +54,7 @@ export class ChangelogService extends EventEmitter {
/**
* Check if debug mode is enabled
* Checks DEBUG from aperant/.env and DEBUG from process.env
* Checks DEBUG from auto-claude/.env and DEBUG from process.env
*/
private isDebugEnabled(): boolean {
// Cache the result after first check
@@ -71,14 +71,14 @@ export class ChangelogService extends EventEmitter {
return true;
}
// Check aperant .env file
const env = this.loadAperantEnv();
// Check auto-claude .env file
const env = this.loadAutoBuildEnv();
this.debugEnabled = env.DEBUG === 'true' || env.DEBUG === '1';
return this.debugEnabled;
}
/**
* Debug logging - only logs when DEBUG=true in aperant/.env or DEBUG is set
* Debug logging - only logs when DEBUG=true in auto-claude/.env or DEBUG is set
*/
private debug(...args: unknown[]): void {
if (this.isDebugEnabled()) {
@@ -86,18 +86,18 @@ export class ChangelogService extends EventEmitter {
}
}
configure(_pythonPath?: string, aperantSourcePath?: string): void {
if (aperantSourcePath) {
this.aperantSourcePath = aperantSourcePath;
configure(_pythonPath?: string, autoBuildSourcePath?: string): void {
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
}
}
/**
* Get the aperant source path (detects automatically if not configured)
* Get the auto-claude source path (detects automatically if not configured)
*/
private getAperantSourcePath(): string | null {
if (this.aperantSourcePath && existsSync(this.aperantSourcePath)) {
return this.aperantSourcePath;
private getAutoBuildSourcePath(): string | null {
if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) {
return this.autoBuildSourcePath;
}
const possiblePaths = [
@@ -116,13 +116,13 @@ export class ChangelogService extends EventEmitter {
}
/**
* Load environment variables from aperant .env file
* Load environment variables from auto-claude .env file
*/
private loadAperantEnv(): Record<string, string> {
const aperantSource = this.getAperantSourcePath();
if (!aperantSource) return {};
private loadAutoBuildEnv(): Record<string, string> {
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) return {};
const envPath = path.join(aperantSource, '.env');
const envPath = path.join(autoBuildSource, '.env');
if (!existsSync(envPath)) return {};
try {
@@ -156,13 +156,13 @@ export class ChangelogService extends EventEmitter {
/**
* Ensure prerequisites are met for changelog generation
* Validates aperant source path and Claude CLI availability
* Validates auto-build source path and Claude CLI availability
* Returns the resolved Claude CLI path to ensure we use the freshly validated path
*/
private ensurePrerequisites(): { aperantSource: string; claudePath: string } {
const aperantSource = this.getAperantSourcePath();
if (!aperantSource) {
throw new Error('Aperant source path not found');
private ensurePrerequisites(): { autoBuildSource: string; claudePath: string } {
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) {
throw new Error('Auto-build source path not found');
}
const claudeInfo = getToolInfo('claude');
@@ -173,7 +173,7 @@ export class ChangelogService extends EventEmitter {
// Update cached path with freshly resolved value
this.claudePath = claudeInfo.path;
return { aperantSource, claudePath: claudeInfo.path };
return { autoBuildSource, claudePath: claudeInfo.path };
}
/**
@@ -181,15 +181,15 @@ export class ChangelogService extends EventEmitter {
*/
private getGenerator(): ChangelogGenerator {
if (!this.generator) {
const { aperantSource, claudePath } = this.ensurePrerequisites();
const { autoBuildSource, claudePath } = this.ensurePrerequisites();
const aperantEnv = this.loadAperantEnv();
const autoBuildEnv = this.loadAutoBuildEnv();
this.generator = new ChangelogGenerator(
'',
claudePath,
aperantSource,
aperantEnv,
autoBuildSource,
autoBuildEnv,
this.isDebugEnabled()
);
@@ -219,12 +219,12 @@ export class ChangelogService extends EventEmitter {
*/
private getVersionSuggester(): VersionSuggester {
if (!this.versionSuggester) {
const { aperantSource, claudePath } = this.ensurePrerequisites();
const { autoBuildSource, claudePath } = this.ensurePrerequisites();
this.versionSuggester = new VersionSuggester(
'',
claudePath,
aperantSource,
autoBuildSource,
this.isDebugEnabled()
);
}
@@ -240,13 +240,13 @@ export class ChangelogService extends EventEmitter {
* Get completed tasks from a project
*/
getCompletedTasks(projectPath: string, tasks: Task[], specsBaseDir?: string): ChangelogTask[] {
const specsDir = path.join(projectPath, specsBaseDir || APERANT_PATHS.SPECS_DIR);
const specsDir = path.join(projectPath, specsBaseDir || AUTO_BUILD_PATHS.SPECS_DIR);
return tasks
.filter(task => isCompletedTask(task.status, task.reviewReason) && !task.metadata?.archivedAt)
.map(task => {
const specDir = path.join(specsDir, task.specId);
const hasSpecs = existsSync(specDir) && existsSync(path.join(specDir, APERANT_PATHS.SPEC_FILE));
const hasSpecs = existsSync(specDir) && existsSync(path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE));
return {
id: task.id,
@@ -264,7 +264,7 @@ export class ChangelogService extends EventEmitter {
* Load spec files for given tasks
*/
async loadTaskSpecs(projectPath: string, taskIds: string[], tasks: Task[], specsBaseDir?: string): Promise<TaskSpecContent[]> {
const specsDir = path.join(projectPath, specsBaseDir || APERANT_PATHS.SPECS_DIR);
const specsDir = path.join(projectPath, specsBaseDir || AUTO_BUILD_PATHS.SPECS_DIR);
this.debug('loadTaskSpecs called', { projectPath, specsDir, taskCount: taskIds.length });
const results: TaskSpecContent[] = [];
@@ -286,26 +286,26 @@ export class ChangelogService extends EventEmitter {
try {
// Load spec.md
const specPath = path.join(specDir, APERANT_PATHS.SPEC_FILE);
const specPath = path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE);
if (existsSync(specPath)) {
content.spec = readFileSync(specPath, 'utf-8');
this.debug('Loaded spec.md', { specId: task.specId, length: content.spec.length });
}
// Load requirements.json
const requirementsPath = path.join(specDir, APERANT_PATHS.REQUIREMENTS);
const requirementsPath = path.join(specDir, AUTO_BUILD_PATHS.REQUIREMENTS);
if (existsSync(requirementsPath)) {
content.requirements = JSON.parse(readFileSync(requirementsPath, 'utf-8'));
}
// Load qa_report.md
const qaReportPath = path.join(specDir, APERANT_PATHS.QA_REPORT);
const qaReportPath = path.join(specDir, AUTO_BUILD_PATHS.QA_REPORT);
if (existsSync(qaReportPath)) {
content.qaReport = readFileSync(qaReportPath, 'utf-8');
}
// Load implementation_plan.json
const planPath = path.join(specDir, APERANT_PATHS.IMPLEMENTATION_PLAN);
const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
if (existsSync(planPath)) {
content.implementationPlan = JSON.parse(readFileSync(planPath, 'utf-8')) as ImplementationPlan;
}
+4 -4
View File
@@ -28,8 +28,8 @@ export class ChangelogGenerator extends EventEmitter {
constructor(
private pythonPath: string,
private claudePath: string,
private aperantSourcePath: string,
private aperantEnv: Record<string, string>,
private autoBuildSourcePath: string,
private autoBuildEnv: Record<string, string>,
debugEnabled: boolean
) {
super();
@@ -146,7 +146,7 @@ export class ChangelogGenerator extends EventEmitter {
// Use python3/python as fallback command (Python subprocess path removed in Vercel AI SDK migration)
const pythonCommand = this.pythonPath || 'python3';
const childProcess = spawn(pythonCommand, ['-c', script], {
cwd: this.aperantSourcePath,
cwd: this.autoBuildSourcePath,
env: spawnEnv
});
@@ -306,7 +306,7 @@ export class ChangelogGenerator extends EventEmitter {
const spawnEnv: Record<string, string> = {
...augmentedEnv,
...this.aperantEnv,
...this.autoBuildEnv,
...profileEnv, // Include active Claude profile config
// Ensure critical env vars are set for claude CLI
// Use USERPROFILE on Windows, HOME on Unix
+1 -1
View File
@@ -6,7 +6,7 @@
export interface ChangelogConfig {
pythonPath: string;
claudePath: string;
aperantSourcePath: string;
autoBuildSourcePath: string;
}
export interface PromptBuildOptions {
@@ -22,7 +22,7 @@ export class VersionSuggester {
constructor(
private pythonPath: string,
private claudePath: string,
private aperantSourcePath: string,
private autoBuildSourcePath: string,
debugEnabled: boolean
) {
this.debugEnabled = debugEnabled;
@@ -57,7 +57,7 @@ export class VersionSuggester {
// Use python3/python as fallback command (Python subprocess path removed in Vercel AI SDK migration)
const pythonCommand = this.pythonPath || 'python3';
const childProcess = spawn(pythonCommand, ['-c', script], {
cwd: this.aperantSourcePath,
cwd: this.autoBuildSourcePath,
env: spawnEnv
});
@@ -2,7 +2,7 @@
* Claude Code CLI Settings Module
*
* Reads and merges Claude Code CLI settings files from the user's system.
* These settings are separate from Aperant's own settings (settings-utils.ts).
* These settings are separate from Auto Claude's own settings (settings-utils.ts).
*
* Usage:
* import { getClaudeCodeEnv, readAllSettings } from './claude-code-settings';
+5 -18
View File
@@ -16,10 +16,9 @@
import * as path from 'path';
import * as os from 'os';
import { existsSync } from 'fs';
import { isLinux } from './platform';
const APP_NAME = 'aperant';
const APP_NAME = 'auto-claude';
/**
* Get the XDG config home directory
@@ -71,13 +70,10 @@ export function getAppCacheDir(): string {
/**
* Get the memories storage directory
* This is where graph databases are stored (previously ~/.aperant/memories)
* This is where graph databases are stored (previously ~/.auto-claude/memories)
*/
export function getMemoriesDir(): string {
// New path under ~/.aperant/memories
const newPath = path.join(os.homedir(), '.aperant', 'memories');
// Legacy path from before the rebrand
// For compatibility, we still support the legacy path
const legacyPath = path.join(os.homedir(), '.auto-claude', 'memories');
// On Linux with XDG variables set (AppImage, Flatpak, Snap), use XDG path
@@ -85,17 +81,8 @@ export function getMemoriesDir(): string {
return path.join(getXdgDataHome(), APP_NAME, 'memories');
}
// Use new path if it exists, otherwise fall back to legacy
if (existsSync(newPath)) {
return newPath;
}
if (existsSync(legacyPath)) {
return legacyPath;
}
// Default to new path for fresh installs
return newPath;
// Default to legacy path for backwards compatibility
return legacyPath;
}
/**
+20 -20
View File
@@ -444,17 +444,17 @@ app.whenReady().then(() => {
// Initialize agent manager
agentManager = new AgentManager();
// Load settings and configure agent manager with Python and aperant paths
// Load settings and configure agent manager with Python and auto-claude paths
// Uses EAFP pattern (try/catch) instead of LBYL (existsSync) to avoid TOCTOU race conditions
const settingsPath = join(app.getPath('userData'), 'settings.json');
try {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
// Validate and migrate aperantPath - must contain planner.md (prompts directory)
// Validate and migrate autoBuildPath - must contain planner.md (prompts directory)
// Uses EAFP pattern (try/catch with accessSync) instead of existsSync to avoid TOCTOU race conditions
let validAperantPath = settings.aperantPath;
if (validAperantPath) {
const plannerMdPath = join(validAperantPath, 'planner.md');
let validAutoBuildPath = settings.autoBuildPath;
if (validAutoBuildPath) {
const plannerMdPath = join(validAutoBuildPath, 'planner.md');
let plannerExists = false;
try {
accessSync(plannerMdPath);
@@ -465,12 +465,12 @@ app.whenReady().then(() => {
if (!plannerExists) {
// Migration: Try to fix stale paths from old project structure
// Old structure: /path/to/project/aperant or apps/backend
// Old structure: /path/to/project/auto-claude or apps/backend
// New structure: /path/to/project/apps/desktop/prompts
let migrated = false;
const possibleCorrections = [
join(validAperantPath.replace(/[/\\]auto-claude[/\\]*$/, ''), 'apps', 'desktop', 'prompts'),
join(validAperantPath.replace(/[/\\]backend[/\\]*$/, ''), 'desktop', 'prompts'),
join(validAutoBuildPath.replace(/[/\\]auto-claude[/\\]*$/, ''), 'apps', 'desktop', 'prompts'),
join(validAutoBuildPath.replace(/[/\\]backend[/\\]*$/, ''), 'desktop', 'prompts'),
];
for (const correctedPath of possibleCorrections) {
const correctedPlannerPath = join(correctedPath, 'planner.md');
@@ -483,31 +483,31 @@ app.whenReady().then(() => {
}
if (correctedPathExists) {
console.log('[main] Migrating aperantPath from old structure:', validAperantPath, '->', correctedPath);
settings.aperantPath = correctedPath;
validAperantPath = correctedPath;
console.log('[main] Migrating autoBuildPath from old structure:', validAutoBuildPath, '->', correctedPath);
settings.autoBuildPath = correctedPath;
validAutoBuildPath = correctedPath;
migrated = true;
// Save the corrected setting - we're the only process modifying settings at startup
try {
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
console.log('[main] Successfully saved migrated aperantPath to settings');
console.log('[main] Successfully saved migrated autoBuildPath to settings');
} catch (writeError) {
console.warn('[main] Failed to save migrated aperantPath:', writeError);
console.warn('[main] Failed to save migrated autoBuildPath:', writeError);
}
break;
}
}
if (!migrated) {
console.warn('[main] Configured aperantPath is invalid (missing planner.md), will use auto-detection:', validAperantPath);
validAperantPath = undefined; // Let auto-detection find the correct path
console.warn('[main] Configured autoBuildPath is invalid (missing planner.md), will use auto-detection:', validAutoBuildPath);
validAutoBuildPath = undefined; // Let auto-detection find the correct path
// Clear the stale setting so this warning doesn't repeat every startup
try {
delete settings.aperantPath;
delete settings.autoBuildPath;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
console.log('[main] Cleared stale aperantPath from settings');
console.log('[main] Cleared stale autoBuildPath from settings');
} catch {
// Non-critical - warning will just repeat next startup
}
@@ -515,12 +515,12 @@ app.whenReady().then(() => {
}
}
if (settings.pythonPath || validAperantPath) {
if (settings.pythonPath || validAutoBuildPath) {
console.warn('[main] Configuring AgentManager with settings:', {
pythonPath: settings.pythonPath,
aperantPath: validAperantPath
autoBuildPath: validAutoBuildPath
});
agentManager.configure(settings.pythonPath, validAperantPath);
agentManager.configure(settings.pythonPath, validAutoBuildPath);
}
} catch (error: unknown) {
// ENOENT means no settings file yet - that's fine, use defaults
+3 -3
View File
@@ -56,10 +56,10 @@ export class InsightsService extends EventEmitter {
}
/**
* Configure paths for Python and aperant source
* Configure paths for Python and auto-claude source
*/
configure(pythonPath?: string, aperantSourcePath?: string): void {
this.config.configure(pythonPath, aperantSourcePath);
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
this.config.configure(pythonPath, autoBuildSourcePath);
}
/**
+4 -4
View File
@@ -18,9 +18,9 @@ insights-service.ts (186 lines)
## Module Responsibilities
### InsightsConfig (`config.ts`)
- Manages Python and aperant source path configuration
- Detects aperant installation automatically
- Loads environment variables from aperant .env file
- Manages Python and auto-claude source path configuration
- Detects auto-claude installation automatically
- Loads environment variables from auto-claude .env file
- Provides complete process environment with profile support
### InsightsPaths (`paths.ts`)
@@ -58,7 +58,7 @@ import { InsightsService } from './insights-service';
const service = new InsightsService();
// Configure paths
service.configure(pythonPath, aperantSourcePath);
service.configure(pythonPath, autoBuildSourcePath);
// Load session
const session = service.loadSession(projectId, projectPath);
@@ -83,7 +83,7 @@ No migration needed! The refactoring is transparent to consumers:
// This code continues to work exactly as before
import { insightsService } from '../insights-service';
insightsService.configure(pythonPath, aperantSourcePath);
insightsService.configure(pythonPath, autoBuildSourcePath);
const session = insightsService.loadSession(projectId, projectPath);
await insightsService.sendMessage(projectId, projectPath, message);
```
+17 -17
View File
@@ -12,25 +12,25 @@ import { getEffectiveSourcePath } from '../updater/path-resolver';
* Handles path detection and environment variable loading
*/
export class InsightsConfig {
private aperantSourcePath: string = '';
private autoBuildSourcePath: string = '';
configure(_pythonPath?: string, aperantSourcePath?: string): void {
if (aperantSourcePath) {
this.aperantSourcePath = aperantSourcePath;
configure(_pythonPath?: string, autoBuildSourcePath?: string): void {
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
}
}
/**
* Get the aperant source path (detects automatically if not configured)
* Get the auto-claude source path (detects automatically if not configured)
* Uses getEffectiveSourcePath() which handles userData override for user-updated backend
*/
getAperantSourcePath(): string | null {
if (this.aperantSourcePath && existsSync(this.aperantSourcePath)) {
return this.aperantSourcePath;
getAutoBuildSourcePath(): string | null {
if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) {
return this.autoBuildSourcePath;
}
// Use shared path resolver which handles:
// 1. User settings (aperantPath)
// 1. User settings (autoBuildPath)
// 2. userData override (backend-source) for user-updated backend
// 3. Bundled backend (process.resourcesPath/backend)
// 4. Development paths
@@ -43,13 +43,13 @@ export class InsightsConfig {
}
/**
* Load environment variables from aperant .env file
* Load environment variables from auto-claude .env file
*/
loadAperantEnv(): Record<string, string> {
const aperantSource = this.getAperantSourcePath();
if (!aperantSource) return {};
loadAutoBuildEnv(): Record<string, string> {
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) return {};
const envPath = path.join(aperantSource, '.env');
const envPath = path.join(autoBuildSource, '.env');
if (!existsSync(envPath)) return {};
try {
@@ -83,10 +83,10 @@ export class InsightsConfig {
/**
* Get complete environment for process execution
* Includes system env, aperant env, and active Claude profile
* Includes system env, auto-claude env, and active Claude profile
*/
async getProcessEnv(): Promise<Record<string, string>> {
const aperantEnv = this.loadAperantEnv();
const autoBuildEnv = this.loadAutoBuildEnv();
// Get best available Claude profile environment (automatically handles rate limits)
const profileResult = getBestAvailableProfileEnv();
const profileEnv = profileResult.env;
@@ -99,7 +99,7 @@ export class InsightsConfig {
return {
...augmentedEnv,
...aperantEnv,
...autoBuildEnv,
...oauthModeClearVars,
...profileEnv,
...apiProfileEnv,
+1 -1
View File
@@ -1,6 +1,6 @@
import path from 'path';
const INSIGHTS_DIR = '.aperant/insights';
const INSIGHTS_DIR = '.auto-claude/insights';
const SESSIONS_DIR = 'sessions';
const CURRENT_SESSION_FILE = 'current_session.json';
+4 -4
View File
@@ -1,6 +1,6 @@
# IPC Handlers - Modular Architecture
This directory contains the refactored IPC (Inter-Process Communication) handlers for Aperant UI, organized into domain-specific modules for better maintainability and code organization.
This directory contains the refactored IPC (Inter-Process Communication) handlers for Auto Claude UI, organized into domain-specific modules for better maintainability and code organization.
## Overview
@@ -16,9 +16,9 @@ Handles project lifecycle and Python environment management:
- `PROJECT_REMOVE` - Remove project
- `PROJECT_LIST` - List all projects
- `PROJECT_UPDATE_SETTINGS` - Update project settings
- `PROJECT_INITIALIZE` - Initialize .aperant directory
- `PROJECT_INITIALIZE` - Initialize .auto-claude directory
- `PROJECT_CHECK_VERSION` - Check initialization status
- `project:has-local-source` - Check if project has local aperant source
- `project:has-local-source` - Check if project has local auto-claude source
- Python environment initialization and status events
#### `task-handlers.ts` (52KB) - Largest module
@@ -243,7 +243,7 @@ Each module may depend on:
- **Services**: AgentManager, TerminalManager, ChangelogService, etc.
- **Stores**: projectStore
- **Utilities**: fileWatcher, titleGenerator
- **Constants**: IPC_CHANNELS, APERANT_PATHS, getSpecsDir
- **Constants**: IPC_CHANNELS, AUTO_BUILD_PATHS, getSpecsDir
- **Types**: Extensive TypeScript types from shared/types
All dependencies are explicitly imported at the module level.
@@ -2,7 +2,7 @@ import type { BrowserWindow } from "electron";
import path from "path";
import { existsSync, readFileSync } from "fs";
import { safeParseJson } from "../utils/json-repair";
import { IPC_CHANNELS, APERANT_PATHS, getSpecsDir } from "../../shared/constants";
import { IPC_CHANNELS, AUTO_BUILD_PATHS, getSpecsDir } from "../../shared/constants";
import type {
SDKRateLimitInfo,
AuthFailureInfo,
@@ -164,8 +164,8 @@ export function registerAgenteventsHandlers(
if (exitTask && exitProject) {
const worktreePath = findTaskWorktree(exitProject.path, exitTask.specId);
if (worktreePath) {
const specsBaseDir = getSpecsDir(exitProject.aperantPath);
const worktreePlanPath = path.join(worktreePath, specsBaseDir, exitTask.specId, APERANT_PATHS.IMPLEMENTATION_PLAN);
const specsBaseDir = getSpecsDir(exitProject.autoBuildPath);
const worktreePlanPath = path.join(worktreePath, specsBaseDir, exitTask.specId, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
try {
const content = readFileSync(worktreePlanPath, 'utf-8');
const parsed = safeParseJson<ImplementationPlan>(content);
@@ -205,9 +205,9 @@ export function registerAgenteventsHandlers(
if (code === 0) {
const { task: specTask, project: specProject } = findTaskAndProject(taskId, projectId);
if (specTask && specProject) {
const specsBaseDir = getSpecsDir(specProject.aperantPath);
const specsBaseDir = getSpecsDir(specProject.autoBuildPath);
const specDir = path.join(specProject.path, specsBaseDir, specTask.specId);
const specFilePath = path.join(specDir, APERANT_PATHS.SPEC_FILE);
const specFilePath = path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE);
if (existsSync(specFilePath)) {
console.warn(`[Task ${taskId}] Spec created successfully — starting task execution`);
// Re-watch the spec directory for the build phase
@@ -290,12 +290,12 @@ export function registerAgenteventsHandlers(
const worktreePath = findTaskWorktree(project.path, task.specId);
if (worktreePath) {
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const worktreePlanPath = path.join(
worktreePath,
specsBaseDir,
task.specId,
APERANT_PATHS.IMPLEMENTATION_PLAN
AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN
);
if (existsSync(worktreePlanPath)) {
persistPlanLastEventSync(worktreePlanPath, event);
@@ -329,11 +329,11 @@ export function registerAgenteventsHandlers(
// Also persist to worktree if task has one
const worktreePath = findTaskWorktree(project.path, task.specId);
if (worktreePath) {
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const worktreeSpecDir = path.join(worktreePath, specsBaseDir, task.specId);
const worktreePlanPath = path.join(
worktreeSpecDir,
APERANT_PATHS.IMPLEMENTATION_PLAN
AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN
);
if (existsSync(worktreePlanPath)) {
persistPlanPhaseSync(worktreePlanPath, progress.phase, project.id);
@@ -426,12 +426,12 @@ export function registerAgenteventsHandlers(
// Also re-stamp worktree copy if it exists
const worktreePath = findTaskWorktree(project.path, task.specId);
if (worktreePath) {
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const worktreePlanPath = path.join(
worktreePath,
specsBaseDir,
task.specId,
APERANT_PATHS.IMPLEMENTATION_PLAN
AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN
);
if (existsSync(worktreePlanPath)) {
persistPlanStatusAndReasonSync(worktreePlanPath, status, reviewReason, project.id, currentXState, phase);
@@ -113,7 +113,7 @@ export function registerChangelogHandlers(
const tasks = rendererTasks || projectStore.getTasks(projectId);
// Get specs directory path
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const doneTasks = changelogService.getCompletedTasks(project.path, tasks, specsBaseDir);
return { success: true, data: doneTasks };
@@ -131,7 +131,7 @@ export function registerChangelogHandlers(
const tasks = projectStore.getTasks(projectId);
// Get specs directory path
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks, specsBaseDir);
return { success: true, data: specs };
@@ -164,7 +164,7 @@ export function registerChangelogHandlers(
let specs: TaskSpecContent[] = [];
if (request.sourceMode === 'tasks' && request.taskIds && request.taskIds.length > 0) {
const tasks = projectStore.getTasks(request.projectId);
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
specs = await changelogService.loadTaskSpecs(project.path, request.taskIds, tasks, specsBaseDir);
}
@@ -282,7 +282,7 @@ export function registerChangelogHandlers(
// Load specs for selected tasks to analyze change types
const tasks = projectStore.getTasks(projectId);
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks, specsBaseDir);
// Analyze specs and suggest version
@@ -44,7 +44,7 @@ export function registerChangelogHandlers(
const tasks = rendererTasks || projectStore.getTasks(projectId);
// Get specs directory path
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const doneTasks = changelogService.getCompletedTasks(project.path, tasks, specsBaseDir);
return { success: true, data: doneTasks };
@@ -62,7 +62,7 @@ export function registerChangelogHandlers(
const tasks = projectStore.getTasks(projectId);
// Get specs directory path
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks, specsBaseDir);
return { success: true, data: specs };
@@ -89,7 +89,7 @@ export function registerChangelogHandlers(
let specs: import('../shared/types').TaskSpecContent[] = [];
if (request.sourceMode === 'tasks' && request.taskIds && request.taskIds.length > 0) {
const tasks = projectStore.getTasks(request.projectId);
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
specs = await changelogService.loadTaskSpecs(project.path, request.taskIds, tasks, specsBaseDir);
}
@@ -146,7 +146,7 @@ export function registerChangelogHandlers(
// Load specs for selected tasks to analyze change types
const tasks = projectStore.getTasks(projectId);
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks, specsBaseDir);
// Analyze specs and suggest version
@@ -1372,7 +1372,7 @@ export function registerClaudeCodeHandlers(): void {
// If authenticated, update the profile with metadata from credentials
// NOTE: We intentionally do NOT store the OAuth token in the profile.
// Storing the token causes Aperant to use a stale cached token instead of
// Storing the token causes AutoClaude to use a stale cached token instead of
// letting Claude CLI read fresh tokens from Keychain (which auto-refreshes).
// By only storing metadata, we ensure getProfileEnv() uses CLAUDE_CONFIG_DIR,
// which allows Claude CLI's working token refresh mechanism to be used.
@@ -1,6 +1,6 @@
# Context Handlers Module
This directory contains the refactored context-related IPC handlers for the Aperant UI application. The handlers manage project context, memory systems (both file-based and Graphiti/LadybugDB), and project index operations.
This directory contains the refactored context-related IPC handlers for the Auto Claude UI application. The handlers manage project context, memory systems (both file-based and Graphiti/LadybugDB), and project index operations.
## Architecture
@@ -12,9 +12,9 @@ The module is organized into focused, single-responsibility files:
Shared utility functions for environment configuration and parsing.
**Exports:**
- `getAperantSourcePath()` - Get aperant source path from settings
- `getAutoBuildSourcePath()` - Get auto-build source path from settings
- `parseEnvFile(content)` - Parse .env file content into key-value pairs
- `loadProjectEnvVars(projectPath, aperantPath)` - Load project-specific environment variables
- `loadProjectEnvVars(projectPath, autoBuildPath)` - Load project-specific environment variables
- `loadGlobalSettings()` - Load global application settings
- `isGraphitiEnabled(projectEnvVars)` - Check if Graphiti memory system is enabled
- `hasOpenAIKey(projectEnvVars, globalSettings)` - Check if OpenAI API key is available
@@ -29,8 +29,8 @@ Shared utility functions for environment configuration and parsing.
Handlers for checking Graphiti/memory system configuration status.
**Exports:**
- `loadGraphitiStateFromSpecs(projectPath, aperantPath)` - Load Graphiti state from most recent spec
- `buildMemoryStatus(projectPath, aperantPath, memoryState)` - Build memory status from environment
- `loadGraphitiStateFromSpecs(projectPath, autoBuildPath)` - Load Graphiti state from most recent spec
- `buildMemoryStatus(projectPath, autoBuildPath, memoryState)` - Build memory status from environment
- `registerMemoryStatusHandlers(getMainWindow)` - Register IPC handlers
**IPC Channels:**
@@ -136,7 +136,7 @@ test('parseEnvFile handles quotes correctly', () => {
import { buildMemoryStatus } from './memory-status-handlers';
test('buildMemoryStatus returns correct status', () => {
const status = buildMemoryStatus('/path/to/project', 'aperant');
const status = buildMemoryStatus('/path/to/project', 'auto-claude');
expect(status).toHaveProperty('enabled');
expect(status).toHaveProperty('available');
});
@@ -152,7 +152,7 @@ test('buildMemoryStatus returns correct status', () => {
## Related Documentation
- [Project Memory System](../../../../aperant/memory.py)
- [Graphiti Memory Integration](../../../../aperant/graphiti_memory.py)
- [Project Memory System](../../../../auto-claude/memory.py)
- [Graphiti Memory Integration](../../../../auto-claude/graphiti_memory.py)
- [LadybugDB Integration](../../ladybug-service.ts)
- [IPC Channels](../../../shared/constants.ts)
@@ -2,7 +2,7 @@ import { ipcMain } from 'electron';
import type { BrowserWindow } from 'electron';
import path from 'path';
import { existsSync, readFileSync } from 'fs';
import { IPC_CHANNELS, APERANT_PATHS } from '../../../shared/constants';
import { IPC_CHANNELS, AUTO_BUILD_PATHS } from '../../../shared/constants';
import type {
IPCResult,
ProjectContextData,
@@ -47,7 +47,7 @@ function toRendererMemory(m: Memory): RendererMemory {
* Load project index from file
*/
function loadProjectIndex(projectPath: string): ProjectIndex | null {
const indexPath = path.join(projectPath, APERANT_PATHS.PROJECT_INDEX);
const indexPath = path.join(projectPath, AUTO_BUILD_PATHS.PROJECT_INDEX);
if (!existsSync(indexPath)) {
return null;
}
@@ -137,7 +137,7 @@ export function registerProjectContextHandlers(
}
try {
const indexOutputPath = path.join(project.path, APERANT_PATHS.PROJECT_INDEX);
const indexOutputPath = path.join(project.path, AUTO_BUILD_PATHS.PROJECT_INDEX);
// Run the TypeScript project indexer (replaces Python subprocess)
const projectIndex = runProjectIndexer(project.path, indexOutputPath);
@@ -7,22 +7,22 @@ export interface EnvironmentVars {
}
export interface GlobalSettings {
aperantPath?: string;
autoBuildPath?: string;
globalOpenAIApiKey?: string;
}
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
/**
* Get the aperant source path from settings
* Get the auto-build source path from settings
*/
export function getAperantSourcePath(): string | null {
export function getAutoBuildSourcePath(): string | null {
if (existsSync(settingsPath)) {
try {
const content = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(content);
if (settings.aperantPath && existsSync(settings.aperantPath)) {
return settings.aperantPath;
if (settings.autoBuildPath && existsSync(settings.autoBuildPath)) {
return settings.autoBuildPath;
}
} catch {
// Fall through to null
@@ -63,12 +63,12 @@ export function parseEnvFile(envContent: string): EnvironmentVars {
/**
* Load environment variables from project .env file
*/
export function loadProjectEnvVars(projectPath: string, aperantPath?: string): EnvironmentVars {
if (!aperantPath) {
export function loadProjectEnvVars(projectPath: string, autoBuildPath?: string): EnvironmentVars {
if (!autoBuildPath) {
return {};
}
const projectEnvPath = path.join(projectPath, aperantPath, '.env');
const projectEnvPath = path.join(projectPath, autoBuildPath, '.env');
if (!existsSync(projectEnvPath)) {
return {};
}
@@ -218,7 +218,7 @@ export interface MemoryDatabaseDetails {
export function getMemoryDatabaseDetails(projectEnvVars: EnvironmentVars): MemoryDatabaseDetails {
const dbPath = projectEnvVars['GRAPHITI_DB_PATH'] ||
process.env.GRAPHITI_DB_PATH ||
require('path').join(require('os').homedir(), '.aperant', 'memories');
require('path').join(require('os').homedir(), '.auto-claude', 'memories');
const database = projectEnvVars['GRAPHITI_DATABASE'] ||
process.env.GRAPHITI_DATABASE ||
@@ -48,8 +48,8 @@ export function registerEnvHandlers(
const existingVars = existingContent ? parseEnvFile(existingContent) : {};
// Update with new values
if (config.aperantModel !== undefined) {
existingVars['APERANT_MODEL'] = config.aperantModel;
if (config.autoBuildModel !== undefined) {
existingVars['AUTO_BUILD_MODEL'] = config.autoBuildModel;
}
if (config.linearApiKey !== undefined) {
existingVars['LINEAR_API_KEY'] = config.linearApiKey;
@@ -183,11 +183,11 @@ export function registerEnvHandlers(
}
// Generate content with sections
const content = `# Aperant Framework Environment Variables
# Managed by Aperant UI
const content = `# Auto Claude Framework Environment Variables
# Managed by Auto Claude UI
# Model override (OPTIONAL)
${existingVars['APERANT_MODEL'] ? `APERANT_MODEL=${existingVars['APERANT_MODEL']}` : '# APERANT_MODEL=claude-opus-4-6'}
${existingVars['AUTO_BUILD_MODEL'] ? `AUTO_BUILD_MODEL=${existingVars['AUTO_BUILD_MODEL']}` : '# AUTO_BUILD_MODEL=claude-opus-4-6'}
# =============================================================================
# LINEAR INTEGRATION (OPTIONAL)
@@ -217,7 +217,7 @@ ${envLine(existingVars, GITLAB_ENV_KEYS.AUTO_SYNC, 'false')}
# GIT/WORKTREE SETTINGS (OPTIONAL)
# =============================================================================
# Default base branch for worktree creation
# If not set, Aperant will auto-detect main/master, or fall back to current branch
# If not set, Auto Claude will auto-detect main/master, or fall back to current branch
${existingVars['DEFAULT_BRANCH'] ? `DEFAULT_BRANCH=${existingVars['DEFAULT_BRANCH']}` : '# DEFAULT_BRANCH=main'}
# =============================================================================
@@ -288,7 +288,7 @@ ${existingVars['OLLAMA_EMBEDDING_DIM'] ? `OLLAMA_EMBEDDING_DIM=${existingVars['O
# LadybugDB Database (embedded - no Docker required)
${existingVars['GRAPHITI_DATABASE'] ? `GRAPHITI_DATABASE=${existingVars['GRAPHITI_DATABASE']}` : '# GRAPHITI_DATABASE=auto_claude_memory'}
${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_DB_PATH']}` : '# GRAPHITI_DB_PATH=~/.aperant/memories'}
${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_DB_PATH']}` : '# GRAPHITI_DB_PATH=~/.auto-claude/memories'}
`;
return content;
@@ -302,11 +302,11 @@ ${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_
return { success: false, error: 'Project not found' };
}
if (!project.aperantPath) {
if (!project.autoBuildPath) {
return { success: false, error: 'Project not initialized' };
}
const envPath = path.join(project.path, project.aperantPath, '.env');
const envPath = path.join(project.path, project.autoBuildPath, '.env');
// Load global settings for fallbacks
let globalSettings: AppSettings = { ...DEFAULT_APP_SETTINGS };
@@ -340,8 +340,8 @@ ${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_
}
}
if (vars['APERANT_MODEL']) {
config.aperantModel = vars['APERANT_MODEL'];
if (vars['AUTO_BUILD_MODEL']) {
config.autoBuildModel = vars['AUTO_BUILD_MODEL'];
}
if (vars['LINEAR_API_KEY']) {
@@ -494,11 +494,11 @@ ${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_
return { success: false, error: 'Project not found' };
}
if (!project.aperantPath) {
if (!project.autoBuildPath) {
return { success: false, error: 'Project not initialized' };
}
const envPath = path.join(project.path, project.aperantPath, '.env');
const envPath = path.join(project.path, project.autoBuildPath, '.env');
try {
// Read existing content if file exists (atomic read, no TOCTOU)
@@ -242,7 +242,7 @@ function createProject(): Project {
id: 'project-1',
name: 'Test Project',
path: projectPath,
aperantPath: '.aperant',
autoBuildPath: '.auto-claude',
settings: {
model: 'default',
memoryBackend: 'file',
@@ -28,7 +28,7 @@ import type { ModelShorthand, ThinkingLevel } from '../../ai/config/types';
const { debug: debugLog } = createContextLogger('GitHub AutoFix');
/**
* Auto-fix configuration stored in .aperant/github/config.json
* Auto-fix configuration stored in .auto-claude/github/config.json
*/
export interface AutoFixConfig {
enabled: boolean;
@@ -99,7 +99,7 @@ export interface BatchProgress {
* Get the GitHub directory for a project
*/
function getGitHubDir(project: Project): string {
return path.join(project.path, '.aperant', 'github');
return path.join(project.path, '.auto-claude', 'github');
}
/**
@@ -822,7 +822,7 @@ async function performCIWaitCheck(
* Get the GitHub directory for a project
*/
function getGitHubDir(project: Project): string {
return path.join(project.path, ".aperant", "github");
return path.join(project.path, ".auto-claude", "github");
}
/**
@@ -1081,7 +1081,7 @@ function createEmptyPRLogs(prNumber: number, repo: string, isFollowup: boolean):
/**
* Get PR logs file path
*
* Logs are stored at `.aperant/github/pr/logs_${prNumber}.json` within the project directory.
* Logs are stored at `.auto-claude/github/pr/logs_${prNumber}.json` within the project directory.
* This provides persistent storage for streaming log data during PR reviews.
*/
function getPRLogsPath(project: Project, prNumber: number): string {
@@ -1163,7 +1163,7 @@ function addLogEntry(logs: PRLogs, entry: PRLogEntry): boolean {
* This class implements a hybrid push/pull approach for real-time log streaming:
*
* 1. **File-Based Storage**: Logs are saved to disk every 3 entries (saveInterval)
* - Location: .aperant/github/pr/logs_${prNumber}.json
* - Location: .auto-claude/github/pr/logs_${prNumber}.json
* - Format: JSON with phase status and log entries
*
* 2. **Push-Based Updates**: Emits IPC events (GITHUB_PR_LOGS_UPDATED) after each save
@@ -1281,7 +1281,7 @@ class PRLogCollector {
* Two-step update mechanism:
* --------------------------
* 1. **File Write**: Persists logs to disk via savePRLogs()
* - Creates/updates .aperant/github/pr/logs_${prNumber}.json
* - Creates/updates .auto-claude/github/pr/logs_${prNumber}.json
* - Updates the `updated_at` timestamp
*
* 2. **IPC Push Event**: Sends GITHUB_PR_LOGS_UPDATED to renderer
@@ -2508,7 +2508,7 @@ export function registerPRHandlers(getMainWindow: () => BrowserWindow | null): v
}
// Use temp file to avoid shell escaping issues
const tmpFile = join(project.path, ".aperant", "tmp_comment_body.txt");
const tmpFile = join(project.path, ".auto-claude", "tmp_comment_body.txt");
try {
writeFileSync(tmpFile, body, "utf-8");
// Use execFileSync with arguments array to prevent command injection
@@ -2733,7 +2733,7 @@ export function registerPRHandlers(getMainWindow: () => BrowserWindow | null): v
const result = await withProjectOrNull(projectId, async (project) => {
// Check if review exists and has reviewed_commit_sha
const githubDir = path.join(project.path, ".aperant", "github");
const githubDir = path.join(project.path, ".auto-claude", "github");
const reviewPath = path.join(githubDir, "pr", `review_${prNumber}.json`);
let review: PRReviewResult;
@@ -4,7 +4,7 @@
import path from 'path';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { APERANT_PATHS, getSpecsDir } from '../../../shared/constants';
import { AUTO_BUILD_PATHS, getSpecsDir } from '../../../shared/constants';
import type { Project, TaskMetadata } from '../../../shared/types';
import { withSpecNumberLock } from '../../utils/spec-number-lock';
import { debugLog } from './utils/logger';
@@ -101,7 +101,7 @@ export async function createSpecForIssue(
labels: string[] = [],
baseBranch?: string
): Promise<SpecCreationData> {
const specsBaseDir = getSpecsDir(project.aperantPath);
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const specsDir = path.join(project.path, specsBaseDir);
if (!existsSync(specsDir)) {
@@ -117,7 +117,7 @@ export async function createSpecForIssue(
// Use coordinated spec numbering with lock to prevent collisions
return await withSpecNumberLock(project.path, async (lock) => {
// Get next spec number from global scan (main + all worktrees)
const specNumber = lock.getNextSpecNumber(project.aperantPath);
const specNumber = lock.getNextSpecNumber(project.autoBuildPath);
const slugifiedTitle = slugifyTitle(safeTitle);
const specId = `${String(specNumber).padStart(3, '0')}-${slugifiedTitle}`;
@@ -139,7 +139,7 @@ export async function createSpecForIssue(
};
// lgtm[js/http-to-file-access] - specDir is controlled, slugifiedTitle sanitizes input
writeFileSync(
path.join(specDir, APERANT_PATHS.IMPLEMENTATION_PLAN),
path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN),
JSON.stringify(implementationPlan, null, 2),
'utf-8'
);
@@ -151,7 +151,7 @@ export async function createSpecForIssue(
};
// lgtm[js/http-to-file-access] - specDir is controlled, slugifiedTitle sanitizes input
writeFileSync(
path.join(specDir, APERANT_PATHS.REQUIREMENTS),
path.join(specDir, AUTO_BUILD_PATHS.REQUIREMENTS),
JSON.stringify(requirements, null, 2),
'utf-8'
);
@@ -234,7 +234,7 @@ Please analyze this issue and provide:
* Used to immediately update the plan file so the frontend shows the correct status
*/
export function updateImplementationPlanStatus(specDir: string, status: string): void {
const planPath = path.join(specDir, APERANT_PATHS.IMPLEMENTATION_PLAN);
const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
try {
const content = readFileSync(planPath, 'utf-8');
@@ -92,7 +92,7 @@ export interface TriageProgress {
* Get the GitHub directory for a project
*/
function getGitHubDir(project: Project): string {
return path.join(project.path, '.aperant', 'github');
return path.join(project.path, '.auto-claude', 'github');
}
/**
@@ -197,8 +197,8 @@ export async function getGitHubTokenForSubprocess(): Promise<string | null> {
* Falls back to gh CLI token if GITHUB_TOKEN not in .env
*/
export function getGitHubConfig(project: Project): GitHubConfig | null {
if (!project.aperantPath) return null;
const envPath = path.join(project.path, project.aperantPath, '.env');
if (!project.autoBuildPath) return null;
const envPath = path.join(project.path, project.autoBuildPath, '.env');
if (!existsSync(envPath)) return null;
try {

Some files were not shown because too many files have changed in this diff Show More