Compare commits

...

5 Commits

Author SHA1 Message Date
AndyMik90 cba050243e fix(test): use path.join in migration tests for Windows compatibility
Path constants used string interpolation with forward slashes, but the
implementation uses path.join() which produces backslashes on Windows.
The mock existsSync did exact string matching and never found the paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 19:13:48 +01:00
André Mikalsen baf6e4bb35 Merge branch 'develop' into fix/rename 2026-03-18 14:39:46 +01:00
AndyMik90 4c832e6a35 refactor(core): rename auto-claude references to aperant across codebase
Continues the rename initiated in 18453adb1. Updates all remaining
references in tools, components, handlers, types, tests, and docs
from auto-claude/AutoBuild to aperant/Aperant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:46:31 +01:00
AndyMik90 f6bf6d4ada fix(core): resolve MCP tool naming, IPC security, and migration tests
Pre-PR validation fixes for the .auto-claude → .aperant rename:
- Align MCP tool name constants (mcp__aperant__*) across registry and
  tool definitions to prevent silent agent tool call failures
- Use IPC_CHANNELS constants and projectId-based lookup instead of raw
  strings and paths in migration handlers (security)
- Add 9 unit tests for needsMigration() and migrateProject()
- Replace console.error with debugLog in migration code
- Add ensureGitignoreEntries fallback in migrateProject()
- Clear migrateError state on dialog close
2026-03-15 13:47:54 +01:00
AndyMik90 18453adb1d feat(core): rename .auto-claude data folder to .aperant
Renames the project data directory from .auto-claude to .aperant across
the entire codebase (113 files) to align with the Aperant rebrand.

- All path constants, configs, worktree paths, branch prefixes updated
- MCP server ID changed from 'auto-claude' to 'aperant'
- Migration dialog added: detects .auto-claude folder and offers rename
- .gitignore includes both .auto-claude/ and .aperant/ for compatibility
- i18n strings updated (en + fr)
- All 207 test files pass (4603 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 13:33:19 +01:00
206 changed files with 1634 additions and 1127 deletions
+2 -2
View File
@@ -80,7 +80,7 @@ Output: Raw JSON status data
## Status File
Auto-build writes status to `.auto-claude-status` in your project root:
Auto-build writes status to `.aperant-status` in your project root:
```json
{
@@ -121,7 +121,7 @@ When active, you'll see these indicators:
## Troubleshooting
### Status not showing?
1. Check if `.auto-claude-status` exists in your project root
1. Check if `.aperant-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`
+4 -1
View File
@@ -54,13 +54,16 @@ lerna-debug.log*
.worktrees/
# ===========================
# Auto Claude Generated
# Aperant 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 `.auto-claude/github/`:
To fully clear all PR review data so reviews run fresh, delete/reset these three things in `.aperant/github/`:
1. `rm .auto-claude/github/pr/logs_*.json` — review log files
2. `rm .auto-claude/github/pr/review_*.json` — review result files
1. `rm .aperant/github/pr/logs_*.json` — review log files
2. `rm .aperant/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 `.auto-claude/specs/XXX-name/` contains: `spec.md`, `requirements.json`, `context.json`, `implementation_plan.json`, `qa_report.md`, `QA_FIX_REQUEST.md`
Each spec in `.aperant/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: .auto-claude/specs/ (gitignored)
# Project data: .aperant/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 auto-claude directory detection
const autoBuildPath = path.join(projectPath, 'auto-claude');
expect(existsSync(autoBuildPath)).toBe(true);
// Check for aperant directory detection
const aperantPath = path.join(projectPath, '.aperant');
expect(existsSync(aperantPath)).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 `./auto-claude/specs/{spec-name}/`)
- **Spec Location**: Where your spec files live (usually `./.aperant/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: .../.auto-claude/worktrees/tasks/{spec-name}/
# Should show: .../.aperant/worktrees/tasks/{spec-name}/
# Or (legacy): .../.worktrees/{spec-name}/
# Or (PR review): .../.auto-claude/github/pr/worktrees/{pr-number}/
# Or (PR review): .../.aperant/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="./auto-claude/specs/YOUR-SPEC-NAME" # Replace with actual path from step 2
SPEC_DIR="./.aperant/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 . ':!.auto-claude' && git commit ...`
4. **Re-stage and retry** - `git add . ':!.aperant' && 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 .auto-claude directory (spec files should never be committed)
git add . ':!.auto-claude'
# Add all files EXCEPT .aperant directory (spec files should never be committed)
git add . ':!.aperant'
# 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 "auto-claude: Complete [subtask-id] - [subtask description]
git commit -m "aperant: Complete [subtask-id] - [subtask description]
- Files modified: [list]
- Verification: [type] - passed
- Phase progress: [X]/[Y] subtasks complete"
```
**CRITICAL**: The `:!.auto-claude` pathspec exclusion ensures spec files are NEVER committed.
**CRITICAL**: The `:!.aperant` 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 `.auto-claude/specs/` which is gitignored.
**Note:** The `build-progress.txt` file is in `.aperant/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: auto-claude/[feature-name]
Branch: aperant/[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 `.auto-claude/specs/` which is gitignored. The orchestrator handles syncing them between worktrees and the main project.
These files live in `.aperant/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 .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.
### 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.
**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`.
**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`.
### 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/.auto-claude/worktrees/tasks/spec-name/
# If pwd shows: /path/to/.aperant/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 .auto-claude directory (spec files should never be committed)
git add . ':!.auto-claude'
# Add all files EXCEPT .aperant directory (spec files should never be committed)
git add . ':!.aperant'
# 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 `:!.auto-claude` pathspec exclusion ensures spec files are NEVER committed.
**CRITICAL**: The `:!.aperant` 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 `.auto-claude/specs/` — that directory is gitignored metadata
- NEVER create deliverable files in `.aperant/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 .auto-claude/specs/ (gitignored)
# Note: qa_report.md and implementation_plan.json are in .aperant/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 .auto-claude/specs/ (gitignored)
# Note: QA_FIX_REQUEST.md and implementation_plan.json are in .aperant/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, '.auto-claude'), { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, '.aperant'), { 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, '.auto-claude/specs/001-test-feature');
TEST_SPEC_DIR = path.join(TEST_DIR, '.aperant/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, '.auto-claude/specs/001-test-feature');
TEST_SPEC_DIR = path.join(TEST_PROJECT_PATH, '.aperant/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/auto-claude-ui-tests';
export const TEST_DATA_DIR = '/tmp/aperant-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/.auto-claude/specs/001-task';
const specDir = '/project/.aperant/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', '.auto-claude', 'specs', '001-first');
const specDir2 = path.join('/project', '.auto-claude', 'specs', '002-second');
const specDir1 = path.join('/project', '.aperant', 'specs', '001-first');
const specDir2 = path.join('/project', '.aperant', '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', '.auto-claude', 'specs', 'super-first');
const specDir2 = path.join('/project', '.auto-claude', 'specs', 'super-second');
const specDir1 = path.join('/project', '.aperant', 'specs', 'super-first');
const specDir2 = path.join('/project', '.aperant', '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/.auto-claude/specs/003-cancel';
const specDir = '/project/.aperant/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/.auto-claude/specs/003-cancel-v2';
const specDir2 = '/project/.aperant/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/.auto-claude/specs/004a';
const specDir2 = '/project/.auto-claude/specs/004b';
const specDir1 = '/project/.aperant/specs/004a';
const specDir2 = '/project/.aperant/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/.auto-claude/specs/004a-v2';
const newSpecDir1 = '/project/.aperant/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', '.auto-claude', 'specs', '004a-fresh');
const specDirFresh = path.join('/project', '.aperant', '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', '.auto-claude', 'specs', '005-specdir');
const specDir = path.join('/project', '.aperant', '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', '.auto-claude', 'specs', '005b-first');
const specDir2 = path.join('/project', '.auto-claude', 'specs', '005b-second');
const specDir1 = path.join('/project', '.aperant', 'specs', '005b-first');
const specDir2 = path.join('/project', '.aperant', '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, 'loadAutoBuildEnv').mockReturnValue({ CUSTOM_ENV: '1' });
vi.spyOn(config, 'loadAperantEnv').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, "auto-claude", "specs"), { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, "aperant", "specs"), { recursive: true });
}
// Cleanup test directories
@@ -419,15 +419,15 @@ describe("IPC Handlers", { timeout: 30000 }, () => {
() => mockMainWindow as never
);
// Create .auto-claude directory first (before adding project so it gets detected)
mkdirSync(path.join(TEST_PROJECT_PATH, ".auto-claude", "specs"), { recursive: true });
// Create .aperant directory first (before adding project so it gets detected)
mkdirSync(path.join(TEST_PROJECT_PATH, ".aperant", "specs"), { recursive: true });
// Add a project - it will detect .auto-claude
// Add a project - it will detect .aperant
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 .auto-claude/specs
const specDir = path.join(TEST_PROJECT_PATH, ".auto-claude", "specs", "001-test-feature");
// Create a spec directory with implementation plan in .aperant/specs
const specDir = path.join(TEST_PROJECT_PATH, ".aperant", "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 .auto-claude directory first (before adding project so it gets detected)
mkdirSync(path.join(TEST_PROJECT_PATH, ".auto-claude", "specs"), { recursive: true });
// Create .aperant directory first (before adding project so it gets detected)
mkdirSync(path.join(TEST_PROJECT_PATH, ".aperant", "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, ".auto-claude", "specs", "task-1");
const specDir = path.join(TEST_PROJECT_PATH, ".aperant", "specs", "task-1");
mkdirSync(specDir, { recursive: true });
writeFileSync(
path.join(specDir, "implementation_plan.json"),
@@ -0,0 +1,172 @@
/**
* 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 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 });
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 });
const { ProjectStore } = await import('../project-store');
const store = new ProjectStore();
const project = store.addProject(TEST_PROJECT_PATH);
expect(project.autoBuildPath).toBe('.auto-claude');
expect(project.aperantPath).toBe('.aperant');
});
it('should set empty autoBuildPath if not present', async () => {
it('should set empty aperantPath if not present', async () => {
const { ProjectStore } = await import('../project-store');
const store = new ProjectStore();
const project = store.addProject(TEST_PROJECT_PATH);
expect(project.autoBuildPath).toBe('');
expect(project.aperantPath).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 .auto-claude (the data directory)
const specsDir = path.join(TEST_PROJECT_PATH, '.auto-claude', 'specs', '001-test-feature');
// Create spec directory structure in .aperant (the data directory)
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '002-pending');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '003-complete');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '004-rejected');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '005-approved');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '006-done');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '007-description-priority');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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',
autoBuildPath: '',
aperantPath: '',
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, '.auto-claude', 'specs', '001-test-task');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '002-multi-location');
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '002-multi-location');
mkdirSync(mainSpecsDir, { recursive: true });
// Create spec directory in worktree location
// Worktree path: .auto-claude/worktrees/tasks/<worktreeName>/.auto-claude/specs/<taskId>
// Worktree path: .aperant/worktrees/tasks/<worktreeName>/.aperant/specs/<taskId>
const worktreeDir = path.join(
TEST_PROJECT_PATH,
'.auto-claude',
'.aperant',
'worktrees',
'tasks',
'my-worktree',
'.auto-claude',
'.aperant',
'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,
'.auto-claude',
'.aperant',
'worktrees',
'tasks',
'only-worktree',
'.auto-claude',
'.aperant',
'specs',
'003-worktree-only'
);
@@ -765,8 +765,8 @@ describe('ProjectStore', () => {
const { ProjectStore } = await import('../project-store');
const store = new ProjectStore();
// Create .auto-claude directory so project is recognized
mkdirSync(path.join(TEST_PROJECT_PATH, '.auto-claude'), { recursive: true });
// Create .aperant directory so project is recognized
mkdirSync(path.join(TEST_PROJECT_PATH, '.aperant'), { 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, '.auto-claude', 'specs', 'valid-task');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '004-unarchive-test');
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '004-unarchive-test');
mkdirSync(mainSpecsDir, { recursive: true });
const worktreeDir = path.join(
TEST_PROJECT_PATH,
'.auto-claude',
'.aperant',
'worktrees',
'tasks',
'unarchive-worktree',
'.auto-claude',
'.aperant',
'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, '.auto-claude', 'specs', '005-cache-test');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '006-invalidate-test');
const specsDir = path.join(TEST_PROJECT_PATH, '.aperant', '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, '.auto-claude', 'specs', '007-dedupe-test');
const mainSpecsDir = path.join(TEST_PROJECT_PATH, '.aperant', 'specs', '007-dedupe-test');
mkdirSync(mainSpecsDir, { recursive: true });
const worktreeDir = path.join(
TEST_PROJECT_PATH,
'.auto-claude',
'.aperant',
'worktrees',
'tasks',
'dedupe-worktree',
'.auto-claude',
'.aperant',
'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 { AUTO_BUILD_PATHS, getSpecsDir } from '../../shared/constants';
import { APERANT_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 auto-claude source
* Configure paths for Python and aperant source
*/
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
this.processManager.configure(pythonPath, autoBuildSourcePath);
configure(pythonPath?: string, aperantSourcePath?: string): void {
this.processManager.configure(pythonPath, aperantSourcePath);
}
/**
@@ -216,11 +216,11 @@ export class AgentManager extends EventEmitter {
// Scan each project for stuck subtasks
for (const project of projects) {
if (!project.autoBuildPath) {
if (!project.aperantPath) {
continue; // Skip projects that haven't been initialized yet
}
const specsDir = path.join(project.path, getSpecsDir(project.autoBuildPath));
const specsDir = path.join(project.path, getSpecsDir(project.aperantPath));
// 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, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
const planPath = path.join(specsDir, specDirName, APERANT_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, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
const planPath = path.join(specDir, APERANT_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, '.auto-claude', 'specs', taskId);
const resolvedSpecDir = specDir ?? path.join(projectPath, '.aperant', '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?.autoBuildPath);
const specsBaseDir = getSpecsDir(project?.aperantPath);
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?.autoBuildPath,
project?.aperantPath,
);
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?.autoBuildPath);
const specsBaseDir = getSpecsDir(project?.aperantPath);
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, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN)
: path.join(context.projectPath, AUTO_BUILD_PATHS.SPECS_DIR, context.specId, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
? path.join(context.specDir, APERANT_PATHS.IMPLEMENTATION_PLAN)
: path.join(context.projectPath, APERANT_PATHS.SPECS_DIR, context.specId, APERANT_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', () => ({
getAutoBuildPath: vi.fn(() => '/fake/auto-build'),
getAperantPath: vi.fn(() => '/fake/aperant'),
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 getAutoBuildSourcePath path validation
// Mock fs.existsSync for getAperantSourcePath 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 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 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;
}
return false;
+17 -17
View File
@@ -102,7 +102,7 @@ export class AgentProcessManager {
private state: AgentState;
private events: AgentEvents;
private emitter: EventEmitter;
private autoBuildSourcePath: string = '';
private aperantSourcePath: string = '';
constructor(state: AgentState, events: AgentEvents, emitter: EventEmitter) {
this.state = state;
@@ -110,14 +110,14 @@ export class AgentProcessManager {
this.emitter = emitter;
}
configure(_pythonPath?: string, autoBuildSourcePath?: string): void {
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
configure(_pythonPath?: string, aperantSourcePath?: string): void {
if (aperantSourcePath) {
this.aperantSourcePath = aperantSourcePath;
}
}
getAutoBuildSourcePath(): string {
return this.autoBuildSourcePath;
getAperantSourcePath(): string {
return this.aperantSourcePath;
}
/**
@@ -493,31 +493,31 @@ export class AgentProcessManager {
}
/**
* Load environment variables from project's .auto-claude/.env file
* Load environment variables from project's .aperant/.env file
* This contains frontend-configured settings like memory configuration
*/
private loadProjectEnv(projectPath: string): Record<string, string> {
// Find project by path to get autoBuildPath
// Find project by path to get aperantPath
const projects = projectStore.getProjects();
const project = projects.find((p) => p.path === projectPath);
if (!project?.autoBuildPath) {
if (!project?.aperantPath) {
return {};
}
const envPath = path.join(projectPath, project.autoBuildPath, '.env');
const envPath = path.join(projectPath, project.aperantPath, '.env');
return this.parseEnvFile(envPath);
}
/**
* Load environment variables from auto-claude .env file
* Load environment variables from aperant .env file
*/
loadAutoBuildEnv(): Record<string, string> {
if (!this.autoBuildSourcePath) {
loadAperantEnv(): Record<string, string> {
if (!this.aperantSourcePath) {
return {};
}
const envPath = path.join(this.autoBuildSourcePath, '.env');
const envPath = path.join(this.aperantSourcePath, '.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 .auto-claude/.env - Frontend-configured settings (memory, integrations)
* 3. Project's .aperant/.env - Frontend-configured settings (memory, integrations)
* 4. Project settings (useClaudeMd) - Runtime overrides
*/
getCombinedEnv(projectPath: string): Record<string, string> {
const autoBuildEnv = this.loadAutoBuildEnv();
const aperantEnv = this.loadAperantEnv();
const projectFileEnv = this.loadProjectEnv(projectPath);
const projectSettingsEnv = this.getProjectEnvVars(projectPath);
return { ...autoBuildEnv, ...projectFileEnv, ...projectSettingsEnv };
return { ...aperantEnv, ...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 { AUTO_BUILD_PATHS } from '../../shared/constants';
import { APERANT_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, AUTO_BUILD_PATHS.ROADMAP_DIR);
const progressPath = path.join(roadmapDir, AUTO_BUILD_PATHS.GENERATION_PROGRESS);
const roadmapDir = path.join(projectPath, APERANT_PATHS.ROADMAP_DIR);
const progressPath = path.join(roadmapDir, APERANT_PATHS.GENERATION_PROGRESS);
// Ensure roadmap directory exists
if (!existsSync(roadmapDir)) {
@@ -120,8 +120,8 @@ export class AgentQueueManager {
try {
const progressPath = path.join(
projectPath,
AUTO_BUILD_PATHS.ROADMAP_DIR,
AUTO_BUILD_PATHS.GENERATION_PROGRESS
APERANT_PATHS.ROADMAP_DIR,
APERANT_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, '.auto-claude', 'ideation');
const outputDir = path.join(projectPath, '.aperant', '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, '.auto-claude', 'roadmap', 'roadmap.json');
const roadmapFilePath = path.join(projectPath, '.aperant', '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', 'auto-claude');
authUrl.searchParams.set('originator', 'aperant');
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/.auto-claude/specs/001',
specDir: '/project/.aperant/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 auto-claude MCP', () => {
it('should configure planner with memory and aperant MCP', () => {
const config = AGENT_CONFIGS.planner;
expect(config.mcpServers).toContain('context7');
expect(config.mcpServers).toContain('memory');
expect(config.mcpServers).toContain('auto-claude');
expect(config.mcpServers).toContain('aperant');
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('auto-claude')).toBe('auto-claude');
expect(mapMcpServerName('aperant')).toBe('aperant');
});
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 auto-claude', () => {
it('should support per-agent MCP removals but never remove aperant', () => {
const servers = getRequiredMcpServers('coder', {
memoryEnabled: true,
agentMcpRemove: 'auto-claude,memory',
agentMcpRemove: 'aperant,memory',
});
expect(servers).toContain('auto-claude');
expect(servers).toContain('aperant');
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;
// =============================================================================
// Auto-Claude MCP Tools (Custom build management)
// Aperant MCP Tools (Custom build management)
// =============================================================================
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';
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';
// =============================================================================
// External MCP Tools
@@ -246,7 +246,7 @@ export const AGENT_CONFIGS: Record<AgentType, AgentConfig> = {
*/
build_orchestrator: {
tools: [...ALL_BUILTIN_TOOLS, 'SpawnSubagent'],
mcpServers: ['context7', 'memory', 'auto-claude'],
mcpServers: ['context7', 'memory', 'aperant'],
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', 'auto-claude'],
mcpServers: ['context7', 'memory', 'aperant'],
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', 'auto-claude'],
mcpServers: ['context7', 'memory', 'aperant'],
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', 'auto-claude', 'browser'],
mcpServers: ['context7', 'memory', 'aperant', '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', 'auto-claude', 'browser'],
mcpServers: ['context7', 'memory', 'aperant', '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',
'auto-claude': 'auto-claude',
'aperant': 'aperant',
};
/**
@@ -593,11 +593,11 @@ export function getRequiredMcpServers(
}
}
// Apply per-agent MCP removals (never remove auto-claude)
// Apply per-agent MCP removals (never remove aperant)
if (options.agentMcpRemove) {
for (const name of options.agentMcpRemove.split(',')) {
const mapped = mapMcpServerName(name.trim(), options.customServerIds);
if (mapped && mapped !== 'auto-claude') {
if (mapped && mapped !== 'aperant') {
const idx = servers.indexOf(mapped);
if (idx !== -1) servers.splice(idx, 1);
}
+1 -1
View File
@@ -33,7 +33,7 @@ import type {
// ---------------------------------------------------------------------------
function loadProjectIndex(projectDir: string): ProjectIndex {
const indexFile = path.join(projectDir, '.auto-claude', 'project_index.json');
const indexFile = path.join(projectDir, '.aperant', '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', 'auto-claude',
'.auto-claude', '.pytest_cache', '.mypy_cache', 'coverage', '.turbo', '.cache',
'.next', '.nuxt', 'target', 'vendor', '.idea', '.vscode',
'.aperant', '.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 .auto-claude/project_index.json */
/** Shape of .aperant/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', 'auto-claude']);
mockGetRequiredMcpServers.mockReturnValueOnce(['context7', 'aperant']);
mockResolveMcpServers.mockReturnValueOnce([
{ ...stdioConfig, id: 'context7' },
{ ...stdioConfig, id: 'auto-claude' },
{ ...stdioConfig, id: 'aperant' },
]);
// 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('auto-claude');
expect(clients[1].serverId).toBe('aperant');
});
it('skips failed connections without throwing', async () => {
@@ -104,23 +104,23 @@ describe('getMcpServerConfig', () => {
});
});
describe('auto-claude', () => {
it('returns auto-claude config with empty specDir as default', () => {
const config = getMcpServerConfig('auto-claude', {});
describe('aperant', () => {
it('returns aperant config with empty specDir as default', () => {
const config = getMcpServerConfig('aperant', {});
expect(config).not.toBeNull();
expect(config?.id).toBe('auto-claude');
expect(config?.id).toBe('aperant');
});
it('injects SPEC_DIR into transport env', () => {
const config = getMcpServerConfig('auto-claude', { specDir: '/project/.auto-claude/specs/001-feature' });
const config = getMcpServerConfig('aperant', { specDir: '/project/.aperant/specs/001-feature' });
expect(config?.transport.type).toBe('stdio');
if (config?.transport.type === 'stdio') {
expect(config.transport.env?.SPEC_DIR).toBe('/project/.auto-claude/specs/001-feature');
expect(config.transport.env?.SPEC_DIR).toBe('/project/.aperant/specs/001-feature');
}
});
it('uses node command', () => {
const config = getMcpServerConfig('auto-claude', {});
const config = getMcpServerConfig('aperant', {});
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 auto-claude config', () => {
const specDir = '/my-project/.auto-claude/specs/042-auth';
const configs = resolveMcpServers(['auto-claude'], { specDir });
it('passes specDir through to aperant config', () => {
const specDir = '/my-project/.aperant/specs/042-auth';
const configs = resolveMcpServers(['aperant'], { 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 = {
};
/**
* Auto-Claude MCP server - custom build management tools.
* Aperant MCP server - custom build management tools.
* Used by planner, coder, and QA agents for build progress tracking.
*/
function createAutoClaudeServer(specDir: string): McpServerConfig {
function createAperantServer(specDir: string): McpServerConfig {
return {
id: 'auto-claude',
id: 'aperant',
name: 'Aperant',
description: 'Build management tools (progress tracking, session context)',
enabledByDefault: true,
transport: {
type: 'stdio',
command: 'node',
args: ['auto-claude-mcp-server.js'],
args: ['aperant-mcp-server.js'],
env: { SPEC_DIR: specDir },
},
};
@@ -124,7 +124,7 @@ function createAutoClaudeServer(specDir: string): McpServerConfig {
/** Options for resolving MCP server configurations */
export interface McpRegistryOptions {
/** Spec directory for auto-claude MCP server */
/** Spec directory for aperant 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 'auto-claude': {
case 'aperant': {
const specDir = options.specDir ?? '';
return createAutoClaudeServer(specDir);
return createAperantServer(specDir);
}
default:
+1 -1
View File
@@ -49,7 +49,7 @@ export type McpServerId =
| 'memory'
| 'electron'
| 'puppeteer'
| 'auto-claude';
| 'aperant';
/** Configuration for a single MCP server */
export interface McpServerConfig {
@@ -46,7 +46,7 @@ export class IncrementalIndexer {
ignored: [
'**/node_modules/**',
'**/.git/**',
'**/.auto-claude/**',
'**/.aperant/**',
'**/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', '.auto-claude', 'dist', 'build',
'node_modules', '.git', '.aperant', 'dist', 'build',
'.next', '__pycache__', 'target', '.venv',
]);
@@ -7,7 +7,7 @@
*
* Manages:
* - Baseline capture when worktrees are created
* - File content snapshots in .auto-claude/baselines/
* - File content snapshots in .aperant/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, '.auto-claude');
const resolvedStorageDir = storageDir ?? path.join(projectDir, '.aperant');
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, '.auto-claude', 'worktrees', taskId),
path.join(projectDir, '.auto-claude', 'worktrees', 'tasks', taskId),
path.join(projectDir, '.aperant', 'worktrees', taskId),
path.join(projectDir, '.aperant', '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, '.auto-claude');
this.storageDir = options.storageDir ?? path.join(this.projectDir, '.aperant');
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, '.auto-claude', 'worktrees', taskId, filePath);
const worktreePath = path.join(projectDir, '.aperant', '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, '.auto-claude');
const resolvedStoragePath = storagePath ?? path.join(this.projectPath, '.aperant');
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/.auto-claude/specs/001-feature';
const SPEC_DIR = '/project/.aperant/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/.auto-claude/specs/001-feature';
const SPEC_DIR = '/project/.aperant/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, '.auto-claude', 'specs', '001-feature');
const SPEC_DIR = path.join(PROJECT_DIR, '.aperant', '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., .auto-claude/specs/001-feature/) */
/** Spec directory path (e.g., .aperant/specs/001-feature/) */
specDir: string;
/** Project root directory */
projectDir: string;
+2 -2
View File
@@ -40,8 +40,8 @@ import type {
// Constants
// ---------------------------------------------------------------------------
const PROFILE_FILENAME = '.auto-claude-security.json';
const CUSTOM_ALLOWLIST_FILENAME = '.auto-claude-allowlist';
const PROFILE_FILENAME = '.aperant-security.json';
const CUSTOM_ALLOWLIST_FILENAME = '.aperant-allowlist';
const HASH_FILES = [
'package.json',
@@ -35,7 +35,7 @@ const SKIP_DIRS = new Set([
'.nuxt',
'target',
'vendor',
'.auto-claude',
'.aperant',
'coverage',
'.nyc_output',
]);
@@ -402,10 +402,10 @@ function validateBranchName(branch: string | null | undefined): string | null {
// =============================================================================
/**
* Load project_index.json from the project's .auto-claude directory.
* Load project_index.json from the project's .aperant directory.
*/
export function loadProjectIndex(projectDir: string): Record<string, unknown> {
const indexPath = join(projectDir, '.auto-claude', 'project_index.json');
const indexPath = join(projectDir, '.aperant', '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 = [
/[/\\]\.auto-claude[/\\]worktrees[/\\]tasks[/\\]/,
/[/\\]\.auto-claude[/\\]github[/\\]pr[/\\]worktrees[/\\]/,
/[/\\]\.aperant[/\\]worktrees[/\\]tasks[/\\]/,
/[/\\]\.aperant[/\\]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 `./auto-claude/specs/${parts[parts.length - 1]}`;
return `./aperant/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 "auto-claude: ${subtask.id} - ${subtask.description.slice(0, 50)}"\n` +
` git commit -m "aperant: ${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` +
@@ -161,7 +161,7 @@ describe('generateCommitMessage', () => {
// ---------------------------------------------------------------------------
it('reads spec.md for title when spec directory exists', async () => {
// Spec directory at .auto-claude/specs/001-add-feature
// Spec directory at .aperant/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/.auto-claude/ideation',
outputDir: '/project/.aperant/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/.auto-claude/ideation' }),
baseConfig({ projectDir: '/my/project', outputDir: '/my/project/.aperant/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/.auto-claude/roadmap',
outputDir: '/project/.aperant/roadmap',
...overrides,
};
}
@@ -277,7 +277,7 @@ describe('runRoadmapGeneration', () => {
});
mockStreamText.mockReturnValue(makeStream([]));
await runRoadmapGeneration(baseConfig({ outputDir: '/project/.auto-claude/roadmap' }));
await runRoadmapGeneration(baseConfig({ outputDir: '/project/.aperant/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('.auto-claude'),
expect.stringContaining('.aperant'),
expect.anything(),
);
});
@@ -236,10 +236,7 @@ export async function generateCommitMessage(
} = config;
// Find spec directory
let specDir = join(projectDir, '.auto-claude', 'specs', specName);
if (!existsSync(specDir)) {
specDir = join(projectDir, 'auto-claude', 'specs', specName);
}
const specDir = join(projectDir, '.aperant', '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, '.auto-claude', 'specs', specId, 'spec.md');
const specFile = join(projectDir, '.aperant', '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, '.auto-claude', 'specs'),
specDir: join(projectDir, '.aperant', '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, '.auto-claude', 'project_index.json');
const indexPath = join(projectDir, '.aperant', '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, '.auto-claude', 'roadmap', 'roadmap.json');
const roadmapPath = join(projectDir, '.aperant', '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, '.auto-claude', 'specs');
const tasksPath = join(projectDir, '.aperant', '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, '.auto-claude', 'specs'),
specDir: join(projectDir, '.aperant', '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 .auto-claude/roadmap/) */
/** Output directory for roadmap files (defaults to .aperant/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, '.auto-claude', 'roadmap');
const outputDir = config.outputDir ?? join(projectDir, '.aperant', '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, '.auto-claude', 'specs'),
specDir: join(projectDir, '.aperant', '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(), 'auto-claude-normalize-'));
const tempDir = await mkdtemp(join(tmpdir(), 'aperant-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(), 'auto-claude-repair-'));
const tempDir = await mkdtemp(join(tmpdir(), 'aperant-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 .auto-claude/ config.
* Loads and caches project security profiles from .aperant/ 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 = '.auto-claude-security.json';
const ALLOWLIST_FILENAME = '.auto-claude-allowlist';
const PROFILE_FILENAME = '.aperant-security.json';
const ALLOWLIST_FILENAME = '.aperant-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',
'auto-claude',
'aperant',
'Aperant',
]);
@@ -33,7 +33,7 @@ describe('ProgressTracker', () => {
type: 'tool-call',
toolName: 'Write',
toolCallId: 'c1',
args: { file_path: '/project/.auto-claude/specs/001/implementation_plan.json' },
args: { file_path: '/project/.aperant/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 auto-claude', () => {
it('should support per-agent MCP REMOVE overrides but protect aperant', () => {
const servers = getRequiredMcpServers('coder', {
memoryEnabled: true,
mcpConfig: { AGENT_MCP_coder_REMOVE: 'auto-claude,memory' },
mcpConfig: { AGENT_MCP_coder_REMOVE: 'aperant,memory' },
});
expect(servers).toContain('auto-claude');
expect(servers).toContain('aperant');
expect(servers).not.toContain('memory');
});
});
@@ -3,9 +3,9 @@
* =======================
*
* Reports current build progress from implementation_plan.json.
* See apps/desktop/src/main/ai/tools/auto-claude/get-build-progress.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/aperant/get-build-progress.ts for the TypeScript implementation.
*
* Tool name: mcp__auto-claude__get_build_progress
* Tool name: mcp__aperant__get_build_progress
*/
import * as fs from 'node:fs';
@@ -50,7 +50,7 @@ interface ImplementationPlan {
export const getBuildProgressTool = Tool.define({
metadata: {
name: 'mcp__auto-claude__get_build_progress',
name: 'mcp__aperant__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/auto-claude/get-session-context.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/aperant/get-session-context.ts for the TypeScript implementation.
*
* Tool name: mcp__auto-claude__get_session_context
* Tool name: mcp__aperant__get_session_context
*/
import * as fs from 'node:fs';
@@ -40,7 +40,7 @@ interface CodebaseMap {
export const getSessionContextTool = Tool.define({
metadata: {
name: 'mcp__auto-claude__get_session_context',
name: 'mcp__aperant__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 @@
/**
* Auto-Claude Custom Tools
* ========================
* Aperant Custom Tools
* ====================
*
* Barrel export for all auto-claude builtin tools.
* Barrel export for all aperant builtin tools.
* These replace the Python tools_pkg/tools/* implementations.
*
* Tool names follow the mcp__auto-claude__* convention to match the
* Tool names follow the mcp__aperant__* 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/auto-claude/record-discovery.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/aperant/record-discovery.ts for the TypeScript implementation.
*
* Tool name: mcp__auto-claude__record_discovery
* Tool name: mcp__aperant__record_discovery
*/
import * as fs from 'node:fs';
@@ -44,7 +44,7 @@ interface CodebaseMap {
export const recordDiscoveryTool = Tool.define({
metadata: {
name: 'mcp__auto-claude__record_discovery',
name: 'mcp__aperant__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/auto-claude/record-gotcha.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/aperant/record-gotcha.ts for the TypeScript implementation.
*
* Tool name: mcp__auto-claude__record_gotcha
* Tool name: mcp__aperant__record_gotcha
*/
import * as fs from 'node:fs';
@@ -33,7 +33,7 @@ const inputSchema = z.object({
export const recordGotchaTool = Tool.define({
metadata: {
name: 'mcp__auto-claude__record_gotcha',
name: 'mcp__aperant__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/auto-claude/update-qa-status.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/aperant/update-qa-status.ts for the TypeScript implementation.
*
* Tool name: mcp__auto-claude__update_qa_status
* Tool name: mcp__aperant__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__auto-claude__update_qa_status',
name: 'mcp__aperant__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/auto-claude/update-subtask-status.ts for the TypeScript implementation.
* See apps/desktop/src/main/ai/tools/aperant/update-subtask-status.ts for the TypeScript implementation.
*
* Tool name: mcp__auto-claude__update_subtask_status
* Tool name: mcp__aperant__update_subtask_status
*/
import * as fs from 'node:fs';
@@ -82,7 +82,7 @@ function updateSubtaskInPlan(
export const updateSubtaskStatusTool = Tool.define({
metadata: {
name: 'mcp__auto-claude__update_subtask_status',
name: 'mcp__aperant__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': 'AutoClaude/1.0',
'User-Agent': 'Aperant/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__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';
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';
// =============================================================================
// 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 !== 'auto-claude') {
if (mapped && mapped !== 'aperant') {
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 .auto-claude/tool-output/)
* @param projectDir - Project directory (spillover written to .aperant/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, '.auto-claude', 'tool-output');
const spilloverDir = path.join(projectDir, '.aperant', '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., .auto-claude/specs/001-feature/) */
/** Spec directory for the current task (e.g., .aperant/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}/.auto-claude/worktrees/tasks/{specId}/
* {projectPath}/.aperant/worktrees/tasks/{specId}/
* on branch:
* auto-claude/{specId}
* aperant/{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 autoBuildPath Optional custom data directory (e.g. ".auto-claude").
* @param aperantPath Optional custom data directory (e.g. ".aperant").
* 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,
autoBuildPath?: string,
aperantPath?: string,
): Promise<WorktreeResult> {
const worktreePath = join(projectPath, '.auto-claude/worktrees/tasks', specId);
const branchName = `auto-claude/${specId}`;
const worktreePath = join(projectPath, '.aperant/worktrees/tasks', specId);
const branchName = `aperant/${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
//
// .auto-claude/specs/ is gitignored, so it is NOT present in the
// .aperant/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(autoBuildPath); // e.g. ".auto-claude/specs"
const specsRelDir = getSpecsDir(aperantPath); // e.g. ".aperant/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, '.auto-claude', 'specs');
specsDir = path.join(projectPath, '.aperant', '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 { AUTO_BUILD_PATHS, DEFAULT_CHANGELOG_PATH } from '../../shared/constants';
import { APERANT_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 autoBuildSourcePath: string = '';
private aperantSourcePath: 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 auto-claude/.env and DEBUG from process.env
* Checks DEBUG from aperant/.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 auto-claude .env file
const env = this.loadAutoBuildEnv();
// Check aperant .env file
const env = this.loadAperantEnv();
this.debugEnabled = env.DEBUG === 'true' || env.DEBUG === '1';
return this.debugEnabled;
}
/**
* Debug logging - only logs when DEBUG=true in auto-claude/.env or DEBUG is set
* Debug logging - only logs when DEBUG=true in aperant/.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, autoBuildSourcePath?: string): void {
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
configure(_pythonPath?: string, aperantSourcePath?: string): void {
if (aperantSourcePath) {
this.aperantSourcePath = aperantSourcePath;
}
}
/**
* Get the auto-claude source path (detects automatically if not configured)
* Get the aperant source path (detects automatically if not configured)
*/
private getAutoBuildSourcePath(): string | null {
if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) {
return this.autoBuildSourcePath;
private getAperantSourcePath(): string | null {
if (this.aperantSourcePath && existsSync(this.aperantSourcePath)) {
return this.aperantSourcePath;
}
const possiblePaths = [
@@ -116,13 +116,13 @@ export class ChangelogService extends EventEmitter {
}
/**
* Load environment variables from auto-claude .env file
* Load environment variables from aperant .env file
*/
private loadAutoBuildEnv(): Record<string, string> {
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) return {};
private loadAperantEnv(): Record<string, string> {
const aperantSource = this.getAperantSourcePath();
if (!aperantSource) return {};
const envPath = path.join(autoBuildSource, '.env');
const envPath = path.join(aperantSource, '.env');
if (!existsSync(envPath)) return {};
try {
@@ -156,13 +156,13 @@ export class ChangelogService extends EventEmitter {
/**
* Ensure prerequisites are met for changelog generation
* Validates auto-build source path and Claude CLI availability
* Validates aperant source path and Claude CLI availability
* Returns the resolved Claude CLI path to ensure we use the freshly validated path
*/
private ensurePrerequisites(): { autoBuildSource: string; claudePath: string } {
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) {
throw new Error('Auto-build source path not found');
private ensurePrerequisites(): { aperantSource: string; claudePath: string } {
const aperantSource = this.getAperantSourcePath();
if (!aperantSource) {
throw new Error('Aperant 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 { autoBuildSource, claudePath: claudeInfo.path };
return { aperantSource, claudePath: claudeInfo.path };
}
/**
@@ -181,15 +181,15 @@ export class ChangelogService extends EventEmitter {
*/
private getGenerator(): ChangelogGenerator {
if (!this.generator) {
const { autoBuildSource, claudePath } = this.ensurePrerequisites();
const { aperantSource, claudePath } = this.ensurePrerequisites();
const autoBuildEnv = this.loadAutoBuildEnv();
const aperantEnv = this.loadAperantEnv();
this.generator = new ChangelogGenerator(
'',
claudePath,
autoBuildSource,
autoBuildEnv,
aperantSource,
aperantEnv,
this.isDebugEnabled()
);
@@ -219,12 +219,12 @@ export class ChangelogService extends EventEmitter {
*/
private getVersionSuggester(): VersionSuggester {
if (!this.versionSuggester) {
const { autoBuildSource, claudePath } = this.ensurePrerequisites();
const { aperantSource, claudePath } = this.ensurePrerequisites();
this.versionSuggester = new VersionSuggester(
'',
claudePath,
autoBuildSource,
aperantSource,
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 || AUTO_BUILD_PATHS.SPECS_DIR);
const specsDir = path.join(projectPath, specsBaseDir || APERANT_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, AUTO_BUILD_PATHS.SPEC_FILE));
const hasSpecs = existsSync(specDir) && existsSync(path.join(specDir, APERANT_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 || AUTO_BUILD_PATHS.SPECS_DIR);
const specsDir = path.join(projectPath, specsBaseDir || APERANT_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, AUTO_BUILD_PATHS.SPEC_FILE);
const specPath = path.join(specDir, APERANT_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, AUTO_BUILD_PATHS.REQUIREMENTS);
const requirementsPath = path.join(specDir, APERANT_PATHS.REQUIREMENTS);
if (existsSync(requirementsPath)) {
content.requirements = JSON.parse(readFileSync(requirementsPath, 'utf-8'));
}
// Load qa_report.md
const qaReportPath = path.join(specDir, AUTO_BUILD_PATHS.QA_REPORT);
const qaReportPath = path.join(specDir, APERANT_PATHS.QA_REPORT);
if (existsSync(qaReportPath)) {
content.qaReport = readFileSync(qaReportPath, 'utf-8');
}
// Load implementation_plan.json
const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
const planPath = path.join(specDir, APERANT_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 autoBuildSourcePath: string,
private autoBuildEnv: Record<string, string>,
private aperantSourcePath: string,
private aperantEnv: 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.autoBuildSourcePath,
cwd: this.aperantSourcePath,
env: spawnEnv
});
@@ -306,7 +306,7 @@ export class ChangelogGenerator extends EventEmitter {
const spawnEnv: Record<string, string> = {
...augmentedEnv,
...this.autoBuildEnv,
...this.aperantEnv,
...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;
autoBuildSourcePath: string;
aperantSourcePath: string;
}
export interface PromptBuildOptions {
@@ -22,7 +22,7 @@ export class VersionSuggester {
constructor(
private pythonPath: string,
private claudePath: string,
private autoBuildSourcePath: string,
private aperantSourcePath: 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.autoBuildSourcePath,
cwd: this.aperantSourcePath,
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 Auto Claude's own settings (settings-utils.ts).
* These settings are separate from Aperant's own settings (settings-utils.ts).
*
* Usage:
* import { getClaudeCodeEnv, readAllSettings } from './claude-code-settings';
+18 -5
View File
@@ -16,9 +16,10 @@
import * as path from 'path';
import * as os from 'os';
import { existsSync } from 'fs';
import { isLinux } from './platform';
const APP_NAME = 'auto-claude';
const APP_NAME = 'aperant';
/**
* Get the XDG config home directory
@@ -70,10 +71,13 @@ export function getAppCacheDir(): string {
/**
* Get the memories storage directory
* This is where graph databases are stored (previously ~/.auto-claude/memories)
* This is where graph databases are stored (previously ~/.aperant/memories)
*/
export function getMemoriesDir(): string {
// For compatibility, we still support the legacy path
// New path under ~/.aperant/memories
const newPath = path.join(os.homedir(), '.aperant', 'memories');
// Legacy path from before the rebrand
const legacyPath = path.join(os.homedir(), '.auto-claude', 'memories');
// On Linux with XDG variables set (AppImage, Flatpak, Snap), use XDG path
@@ -81,8 +85,17 @@ export function getMemoriesDir(): string {
return path.join(getXdgDataHome(), APP_NAME, 'memories');
}
// Default to legacy path for backwards compatibility
return legacyPath;
// 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;
}
/**
+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 auto-claude paths
// Load settings and configure agent manager with Python and aperant 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 autoBuildPath - must contain planner.md (prompts directory)
// Validate and migrate aperantPath - must contain planner.md (prompts directory)
// Uses EAFP pattern (try/catch with accessSync) instead of existsSync to avoid TOCTOU race conditions
let validAutoBuildPath = settings.autoBuildPath;
if (validAutoBuildPath) {
const plannerMdPath = join(validAutoBuildPath, 'planner.md');
let validAperantPath = settings.aperantPath;
if (validAperantPath) {
const plannerMdPath = join(validAperantPath, '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/auto-claude or apps/backend
// Old structure: /path/to/project/aperant or apps/backend
// New structure: /path/to/project/apps/desktop/prompts
let migrated = false;
const possibleCorrections = [
join(validAutoBuildPath.replace(/[/\\]auto-claude[/\\]*$/, ''), 'apps', 'desktop', 'prompts'),
join(validAutoBuildPath.replace(/[/\\]backend[/\\]*$/, ''), 'desktop', 'prompts'),
join(validAperantPath.replace(/[/\\]auto-claude[/\\]*$/, ''), 'apps', 'desktop', 'prompts'),
join(validAperantPath.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 autoBuildPath from old structure:', validAutoBuildPath, '->', correctedPath);
settings.autoBuildPath = correctedPath;
validAutoBuildPath = correctedPath;
console.log('[main] Migrating aperantPath from old structure:', validAperantPath, '->', correctedPath);
settings.aperantPath = correctedPath;
validAperantPath = 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 autoBuildPath to settings');
console.log('[main] Successfully saved migrated aperantPath to settings');
} catch (writeError) {
console.warn('[main] Failed to save migrated autoBuildPath:', writeError);
console.warn('[main] Failed to save migrated aperantPath:', writeError);
}
break;
}
}
if (!migrated) {
console.warn('[main] Configured autoBuildPath is invalid (missing planner.md), will use auto-detection:', validAutoBuildPath);
validAutoBuildPath = undefined; // Let auto-detection find the correct path
console.warn('[main] Configured aperantPath is invalid (missing planner.md), will use auto-detection:', validAperantPath);
validAperantPath = undefined; // Let auto-detection find the correct path
// Clear the stale setting so this warning doesn't repeat every startup
try {
delete settings.autoBuildPath;
delete settings.aperantPath;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
console.log('[main] Cleared stale autoBuildPath from settings');
console.log('[main] Cleared stale aperantPath from settings');
} catch {
// Non-critical - warning will just repeat next startup
}
@@ -515,12 +515,12 @@ app.whenReady().then(() => {
}
}
if (settings.pythonPath || validAutoBuildPath) {
if (settings.pythonPath || validAperantPath) {
console.warn('[main] Configuring AgentManager with settings:', {
pythonPath: settings.pythonPath,
autoBuildPath: validAutoBuildPath
aperantPath: validAperantPath
});
agentManager.configure(settings.pythonPath, validAutoBuildPath);
agentManager.configure(settings.pythonPath, validAperantPath);
}
} 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 auto-claude source
* Configure paths for Python and aperant source
*/
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
this.config.configure(pythonPath, autoBuildSourcePath);
configure(pythonPath?: string, aperantSourcePath?: string): void {
this.config.configure(pythonPath, aperantSourcePath);
}
/**
+4 -4
View File
@@ -18,9 +18,9 @@ insights-service.ts (186 lines)
## Module Responsibilities
### InsightsConfig (`config.ts`)
- Manages Python and auto-claude source path configuration
- Detects auto-claude installation automatically
- Loads environment variables from auto-claude .env file
- Manages Python and aperant source path configuration
- Detects aperant installation automatically
- Loads environment variables from aperant .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, autoBuildSourcePath);
service.configure(pythonPath, aperantSourcePath);
// 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, autoBuildSourcePath);
insightsService.configure(pythonPath, aperantSourcePath);
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 autoBuildSourcePath: string = '';
private aperantSourcePath: string = '';
configure(_pythonPath?: string, autoBuildSourcePath?: string): void {
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
configure(_pythonPath?: string, aperantSourcePath?: string): void {
if (aperantSourcePath) {
this.aperantSourcePath = aperantSourcePath;
}
}
/**
* Get the auto-claude source path (detects automatically if not configured)
* Get the aperant source path (detects automatically if not configured)
* Uses getEffectiveSourcePath() which handles userData override for user-updated backend
*/
getAutoBuildSourcePath(): string | null {
if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) {
return this.autoBuildSourcePath;
getAperantSourcePath(): string | null {
if (this.aperantSourcePath && existsSync(this.aperantSourcePath)) {
return this.aperantSourcePath;
}
// Use shared path resolver which handles:
// 1. User settings (autoBuildPath)
// 1. User settings (aperantPath)
// 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 auto-claude .env file
* Load environment variables from aperant .env file
*/
loadAutoBuildEnv(): Record<string, string> {
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) return {};
loadAperantEnv(): Record<string, string> {
const aperantSource = this.getAperantSourcePath();
if (!aperantSource) return {};
const envPath = path.join(autoBuildSource, '.env');
const envPath = path.join(aperantSource, '.env');
if (!existsSync(envPath)) return {};
try {
@@ -83,10 +83,10 @@ export class InsightsConfig {
/**
* Get complete environment for process execution
* Includes system env, auto-claude env, and active Claude profile
* Includes system env, aperant env, and active Claude profile
*/
async getProcessEnv(): Promise<Record<string, string>> {
const autoBuildEnv = this.loadAutoBuildEnv();
const aperantEnv = this.loadAperantEnv();
// 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,
...autoBuildEnv,
...aperantEnv,
...oauthModeClearVars,
...profileEnv,
...apiProfileEnv,
+1 -1
View File
@@ -1,6 +1,6 @@
import path from 'path';
const INSIGHTS_DIR = '.auto-claude/insights';
const INSIGHTS_DIR = '.aperant/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 Auto Claude UI, organized into domain-specific modules for better maintainability and code organization.
This directory contains the refactored IPC (Inter-Process Communication) handlers for Aperant 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 .auto-claude directory
- `PROJECT_INITIALIZE` - Initialize .aperant directory
- `PROJECT_CHECK_VERSION` - Check initialization status
- `project:has-local-source` - Check if project has local auto-claude source
- `project:has-local-source` - Check if project has local aperant 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, AUTO_BUILD_PATHS, getSpecsDir
- **Constants**: IPC_CHANNELS, APERANT_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, AUTO_BUILD_PATHS, getSpecsDir } from "../../shared/constants";
import { IPC_CHANNELS, APERANT_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.autoBuildPath);
const worktreePlanPath = path.join(worktreePath, specsBaseDir, exitTask.specId, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
const specsBaseDir = getSpecsDir(exitProject.aperantPath);
const worktreePlanPath = path.join(worktreePath, specsBaseDir, exitTask.specId, APERANT_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.autoBuildPath);
const specsBaseDir = getSpecsDir(specProject.aperantPath);
const specDir = path.join(specProject.path, specsBaseDir, specTask.specId);
const specFilePath = path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE);
const specFilePath = path.join(specDir, APERANT_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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
const worktreePlanPath = path.join(
worktreePath,
specsBaseDir,
task.specId,
AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN
APERANT_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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
const worktreeSpecDir = path.join(worktreePath, specsBaseDir, task.specId);
const worktreePlanPath = path.join(
worktreeSpecDir,
AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN
APERANT_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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
const worktreePlanPath = path.join(
worktreePath,
specsBaseDir,
task.specId,
AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN
APERANT_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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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 AutoClaude to use a stale cached token instead of
// Storing the token causes Aperant 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 Auto Claude 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 Aperant 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:**
- `getAutoBuildSourcePath()` - Get auto-build source path from settings
- `getAperantSourcePath()` - Get aperant source path from settings
- `parseEnvFile(content)` - Parse .env file content into key-value pairs
- `loadProjectEnvVars(projectPath, autoBuildPath)` - Load project-specific environment variables
- `loadProjectEnvVars(projectPath, aperantPath)` - 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, autoBuildPath)` - Load Graphiti state from most recent spec
- `buildMemoryStatus(projectPath, autoBuildPath, memoryState)` - Build memory status from environment
- `loadGraphitiStateFromSpecs(projectPath, aperantPath)` - Load Graphiti state from most recent spec
- `buildMemoryStatus(projectPath, aperantPath, 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', 'auto-claude');
const status = buildMemoryStatus('/path/to/project', 'aperant');
expect(status).toHaveProperty('enabled');
expect(status).toHaveProperty('available');
});
@@ -152,7 +152,7 @@ test('buildMemoryStatus returns correct status', () => {
## Related Documentation
- [Project Memory System](../../../../auto-claude/memory.py)
- [Graphiti Memory Integration](../../../../auto-claude/graphiti_memory.py)
- [Project Memory System](../../../../aperant/memory.py)
- [Graphiti Memory Integration](../../../../aperant/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, AUTO_BUILD_PATHS } from '../../../shared/constants';
import { IPC_CHANNELS, APERANT_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, AUTO_BUILD_PATHS.PROJECT_INDEX);
const indexPath = path.join(projectPath, APERANT_PATHS.PROJECT_INDEX);
if (!existsSync(indexPath)) {
return null;
}
@@ -137,7 +137,7 @@ export function registerProjectContextHandlers(
}
try {
const indexOutputPath = path.join(project.path, AUTO_BUILD_PATHS.PROJECT_INDEX);
const indexOutputPath = path.join(project.path, APERANT_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 {
autoBuildPath?: string;
aperantPath?: string;
globalOpenAIApiKey?: string;
}
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
/**
* Get the auto-build source path from settings
* Get the aperant source path from settings
*/
export function getAutoBuildSourcePath(): string | null {
export function getAperantSourcePath(): string | null {
if (existsSync(settingsPath)) {
try {
const content = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(content);
if (settings.autoBuildPath && existsSync(settings.autoBuildPath)) {
return settings.autoBuildPath;
if (settings.aperantPath && existsSync(settings.aperantPath)) {
return settings.aperantPath;
}
} 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, autoBuildPath?: string): EnvironmentVars {
if (!autoBuildPath) {
export function loadProjectEnvVars(projectPath: string, aperantPath?: string): EnvironmentVars {
if (!aperantPath) {
return {};
}
const projectEnvPath = path.join(projectPath, autoBuildPath, '.env');
const projectEnvPath = path.join(projectPath, aperantPath, '.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(), '.auto-claude', 'memories');
require('path').join(require('os').homedir(), '.aperant', '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.autoBuildModel !== undefined) {
existingVars['AUTO_BUILD_MODEL'] = config.autoBuildModel;
if (config.aperantModel !== undefined) {
existingVars['APERANT_MODEL'] = config.aperantModel;
}
if (config.linearApiKey !== undefined) {
existingVars['LINEAR_API_KEY'] = config.linearApiKey;
@@ -183,11 +183,11 @@ export function registerEnvHandlers(
}
// Generate content with sections
const content = `# Auto Claude Framework Environment Variables
# Managed by Auto Claude UI
const content = `# Aperant Framework Environment Variables
# Managed by Aperant UI
# Model override (OPTIONAL)
${existingVars['AUTO_BUILD_MODEL'] ? `AUTO_BUILD_MODEL=${existingVars['AUTO_BUILD_MODEL']}` : '# AUTO_BUILD_MODEL=claude-opus-4-6'}
${existingVars['APERANT_MODEL'] ? `APERANT_MODEL=${existingVars['APERANT_MODEL']}` : '# APERANT_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, Auto Claude will auto-detect main/master, or fall back to current branch
# If not set, Aperant 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=~/.auto-claude/memories'}
${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_DB_PATH']}` : '# GRAPHITI_DB_PATH=~/.aperant/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.autoBuildPath) {
if (!project.aperantPath) {
return { success: false, error: 'Project not initialized' };
}
const envPath = path.join(project.path, project.autoBuildPath, '.env');
const envPath = path.join(project.path, project.aperantPath, '.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['AUTO_BUILD_MODEL']) {
config.autoBuildModel = vars['AUTO_BUILD_MODEL'];
if (vars['APERANT_MODEL']) {
config.aperantModel = vars['APERANT_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.autoBuildPath) {
if (!project.aperantPath) {
return { success: false, error: 'Project not initialized' };
}
const envPath = path.join(project.path, project.autoBuildPath, '.env');
const envPath = path.join(project.path, project.aperantPath, '.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,
autoBuildPath: '.auto-claude',
aperantPath: '.aperant',
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 .auto-claude/github/config.json
* Auto-fix configuration stored in .aperant/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, '.auto-claude', 'github');
return path.join(project.path, '.aperant', '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, ".auto-claude", "github");
return path.join(project.path, ".aperant", "github");
}
/**
@@ -1081,7 +1081,7 @@ function createEmptyPRLogs(prNumber: number, repo: string, isFollowup: boolean):
/**
* Get PR logs file path
*
* Logs are stored at `.auto-claude/github/pr/logs_${prNumber}.json` within the project directory.
* Logs are stored at `.aperant/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: .auto-claude/github/pr/logs_${prNumber}.json
* - Location: .aperant/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 .auto-claude/github/pr/logs_${prNumber}.json
* - Creates/updates .aperant/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, ".auto-claude", "tmp_comment_body.txt");
const tmpFile = join(project.path, ".aperant", "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, ".auto-claude", "github");
const githubDir = path.join(project.path, ".aperant", "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 { AUTO_BUILD_PATHS, getSpecsDir } from '../../../shared/constants';
import { APERANT_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.autoBuildPath);
const specsBaseDir = getSpecsDir(project.aperantPath);
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.autoBuildPath);
const specNumber = lock.getNextSpecNumber(project.aperantPath);
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, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN),
path.join(specDir, APERANT_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, AUTO_BUILD_PATHS.REQUIREMENTS),
path.join(specDir, APERANT_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, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
const planPath = path.join(specDir, APERANT_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, '.auto-claude', 'github');
return path.join(project.path, '.aperant', '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.autoBuildPath) return null;
const envPath = path.join(project.path, project.autoBuildPath, '.env');
if (!project.aperantPath) return null;
const envPath = path.join(project.path, project.aperantPath, '.env');
if (!existsSync(envPath)) return null;
try {
@@ -68,7 +68,7 @@ function validatePathWithinProject(projectPath: string, resolvedPath: string): v
* Get the GitLab directory for a project
*/
function getGitLabDir(project: Project): string {
const gitlabDir = path.join(project.path, '.auto-claude', 'gitlab');
const gitlabDir = path.join(project.path, '.aperant', 'gitlab');
validatePathWithinProject(project.path, gitlabDir);
return gitlabDir;
}
@@ -58,7 +58,7 @@ function getReviewKey(projectId: string, mrIid: number): string {
* Get the GitLab directory for a project
*/
function getGitLabDir(project: Project): string {
return path.join(project.path, '.auto-claude', 'gitlab');
return path.join(project.path, '.aperant', 'gitlab');
}
async function waitForRebaseCompletion(
@@ -774,7 +774,7 @@ export function registerMRReviewHandlers(
debugLog('checkNewCommits handler called', { projectId, mrIid });
const result = await withProjectOrNull(projectId, async (project) => {
const gitlabDir = path.join(project.path, '.auto-claude', 'gitlab');
const gitlabDir = path.join(project.path, '.aperant', 'gitlab');
const reviewPath = path.join(gitlabDir, 'mr', `review_${mrIid}.json`);
if (!fs.existsSync(reviewPath)) {
@@ -388,7 +388,7 @@ export async function createSpecForIssue(
const safeProject = sanitizeText(config.project, 200);
const safeInstanceUrl = sanitizeInstanceUrl(config.instanceUrl);
const specsDir = path.join(project.path, project.autoBuildPath, 'specs');
const specsDir = path.join(project.path, project.aperantPath, 'specs');
// Ensure specs directory exists
await mkdir(specsDir, { recursive: true });

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