Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@ async function startPgserveOnPort(port: number): Promise<number> {
child.unref();
pgserveChild = child;

const deadline = Date.now() + 15000;
const timeout = Number(process.env.GENIE_PGSERVE_TIMEOUT) || 30000;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
if (await isPostgresHealthy(port)) {
activePort = port;
Expand All @@ -399,7 +400,7 @@ async function startPgserveOnPort(port: number): Promise<number> {
} catch {
/* dead */
}
throw new Error(`pgserve failed to start on port ${port} (timeout after 15s)`);
throw new Error(`pgserve failed to start on port ${port} (timeout after ${timeout / 1000}s)`);
}

/** Register process exit handler to clean up lockfile (once). */
Expand Down
14 changes: 2 additions & 12 deletions src/lib/task-close-merged.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { execSync } from 'node:child_process';
import { resolveRepoPath } from './wish-state.js';

// ============================================================================
// Types
Expand Down Expand Up @@ -209,7 +210,7 @@ async function findTasksByWishSlug(
const { getConnection } = await import('./db.js');
const sql = await getConnection();

const repo = repoPath ?? getRepoPathSafe();
const repo = repoPath ?? resolveRepoPath();
const pattern = `%${slug}%`;

const rows = await sql`
Expand All @@ -228,14 +229,3 @@ async function findTasksByWishSlug(
repoPath: r.repo_path as string,
}));
}

function getRepoPathSafe(): string {
try {
return execSync('git rev-parse --show-toplevel', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
} catch {
return process.cwd();
}
}
9 changes: 8 additions & 1 deletion src/lib/task-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ function buildTaskVals(input: TaskInput) {
return {
...taskNullables(input),
type: input.typeId ?? 'software',
stage: input.stage ?? 'draft',
stage: input.stage, // resolved in createTask via type lookup
status: input.status ?? 'ready',
priority: input.priority ?? 'normal',
};
Expand All @@ -572,6 +572,13 @@ export async function createTask(input: TaskInput, repoPath?: string, projectId?
const projId = projectId ?? (await ensureProject(repo));
const vals = buildTaskVals(input);

// Resolve default stage from the task type when not explicitly provided
if (!vals.stage) {
const taskType = await getType(vals.type);
const stages = taskType?.stages as Array<{ name?: string }> | undefined;
vals.stage = stages?.[0]?.name ?? 'draft';
}

// If boardId provided and stage given, resolve stage name -> column_id
if (vals.boardId && !vals.columnId && vals.stage) {
vals.columnId = await resolveColumnId(sql, vals.boardId, vals.stage);
Expand Down
35 changes: 27 additions & 8 deletions src/term-commands/brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,33 @@
*/

import { execSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { dirname, join, resolve } from 'node:path';
import type { Command } from 'commander';

const BRAIN_PKG = '@automagik/genie-brain';
const BRAIN_REPO = 'github:automagik-dev/genie-brain';
const BRAIN_DIR = 'node_modules/@automagik/genie-brain';

/** Resolve genie's package root — works from both src/ (dev) and dist/ (compiled). */
function resolveGenieRoot(): string {
try {
const scriptDir = dirname(realpathSync(process.argv[1]));
const candidates = [
resolve(scriptDir, '..'), // dist/ or src/ → project root
resolve(scriptDir, '..', '..'), // src/term-commands/ → project root
];
for (const c of candidates) {
if (existsSync(join(c, 'package.json'))) return c;
}
} catch {
/* fallback below */
}
// Fallback: import.meta.dir (works in dev, unreliable in compiled)
return resolve(import.meta.dir, '..', '..');
}

const BRAIN_DIR = join(resolveGenieRoot(), 'node_modules', '@automagik', 'genie-brain');
const CACHE_PATH = join(homedir(), '.genie', 'brain-version-check.json');

/** Compare dot-separated version strings numerically (e.g., "260403.9" vs "260403.10"). */
Expand Down Expand Up @@ -192,7 +211,7 @@ async function showVersion(): Promise<void> {
if (check.updateAvailable && check.latestVersion) {
console.log(` Latest: ${check.latestVersion}`);
console.log('');
console.log(' Update available. Run: genie brain update');
console.log(' Update available. Run: genie brain upgrade');
} else {
console.log(' Status: up to date');
}
Expand Down Expand Up @@ -220,7 +239,7 @@ async function installBrain(): Promise<boolean> {

// Clone brain repo using gh CLI (handles private repos without exposing tokens in process list)
execSync(`rm -rf "${BRAIN_DIR}"`, { stdio: 'pipe' });
execSync('mkdir -p node_modules/@automagik', { stdio: 'pipe' });
execSync(`mkdir -p "${dirname(BRAIN_DIR)}"`, { stdio: 'pipe' });
execSync(`gh repo clone automagik-dev/genie-brain "${BRAIN_DIR}" -- --depth 1`, {
stdio: 'inherit',
});
Expand Down Expand Up @@ -309,7 +328,7 @@ async function executeBrainCommand(args: string[]): Promise<void> {
// Auto-check hint (cache-only, no network, sync)
const check = checkForUpdates();
if (check.updateAvailable && check.latestVersion) {
console.log(`\n Update available (${check.latestVersion}). Run: genie brain update`);
console.log(`\n Update available (${check.latestVersion}). Run: genie brain upgrade`);
}
} else {
console.error('Brain module loaded but execute() not found.');
Expand Down Expand Up @@ -357,8 +376,8 @@ export function registerBrainCommands(program: Command): void {
});

brain
.command('update')
.description('Update genie-brain to latest version')
.command('upgrade')
.description('Upgrade genie-brain to latest version')
.action(async () => {
await updateBrain();
});
Expand Down
51 changes: 44 additions & 7 deletions src/term-commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { existsSync, mkdirSync, symlinkSync, writeFileSync } from 'node:fs';
import { basename, join } from 'node:path';
import { basename, join, relative, resolve, sep } from 'node:path';
import { confirm } from '@inquirer/prompts';
import type { Command } from 'commander';
import { type WorkspaceConfig, findWorkspace, scanAgents } from '../lib/workspace.js';
Expand Down Expand Up @@ -38,8 +38,9 @@ async function ensureSetupCompleteForInit(): Promise<void> {
await setupCommand();
}

function scaffoldAgentInWorkspace(workspaceRoot: string, name: string): void {
const agentDir = join(workspaceRoot, 'agents', name);
function scaffoldAgentInWorkspace(workspaceRoot: string, name: string, agentsDir?: string): void {
const baseDir = agentsDir ?? join(workspaceRoot, 'agents');
const agentDir = join(baseDir, name);
if (existsSync(agentDir)) {
throw new Error(`Agent directory already exists: ${agentDir}`);
}
Expand Down Expand Up @@ -139,17 +140,52 @@ async function initWorkspace(): Promise<void> {
await syncWorkspaceAgents(cwd);
}

/**
* Resolve the agents parent directory based on --dir option and CWD.
*
* Priority:
* 1. Explicit `--dir` option (resolved absolutely — use with care).
* 2. CWD is inside the workspace and contains an `agents` path segment:
* return the path up to and including the first `agents` segment.
* This prevents nesting inside an existing agent subdirectory
* (e.g. CWD `<ws>/agents/foo` still scaffolds into `<ws>/agents`,
* not `<ws>/agents/foo/<new>`).
* 3. Fall back to `<wsRoot>/agents`.
*
* Uses `path.sep` for Windows compatibility and exact segment matching
* (not substring) so paths like `/tmp/agents-backup` or `.../agentship`
* never false-match the `agents` segment.
*/
function resolveAgentsDir(wsRoot: string, dirOption?: string): string {
if (dirOption) return resolve(dirOption);

const cwd = process.cwd();
const rel = relative(wsRoot, cwd);

// CWD outside the workspace (relative path escapes with '..') — fall back.
if (rel.startsWith('..')) return join(wsRoot, 'agents');

// Walk workspace-relative segments and find the first exact `agents` match.
const segments = rel === '' ? [] : rel.split(sep);
const idx = segments.indexOf('agents');
if (idx === -1) return join(wsRoot, 'agents');

return join(wsRoot, ...segments.slice(0, idx + 1));
}

/** genie init agent <name> — scaffold agent directory */
async function initAgent(name: string): Promise<void> {
async function initAgent(name: string, options: { dir?: string }): Promise<void> {
const cwd = process.cwd();
const ws = findWorkspace(cwd);
if (!ws) {
console.error('Error: Not in a genie workspace. Run `genie init` first.');
process.exit(1);
}

const agentsDir = resolveAgentsDir(ws.root, options.dir);

try {
scaffoldAgentInWorkspace(ws.root, name);
scaffoldAgentInWorkspace(ws.root, name, agentsDir);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
Expand All @@ -170,8 +206,9 @@ export function registerInitCommands(program: Command): void {
init
.command('agent <name>')
.description('Scaffold a new agent in the workspace')
.action(async (name: string) => {
.option('--dir <path>', 'Target directory for agent (default: CWD if inside agents/, else workspace agents/)')
.action(async (name: string, options: { dir?: string }) => {
await ensureSetupCompleteForInit();
await initAgent(name);
await initAgent(name, options);
});
}
3 changes: 3 additions & 0 deletions src/term-commands/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ async function printByColumn(tasks: taskServiceTypes.TaskRow[], boardName: strin

interface CreateOptions {
type?: string;
stage?: string;
priority?: string;
due?: string;
start?: string;
Expand Down Expand Up @@ -378,6 +379,7 @@ async function handleTaskCreate(title: string, options: CreateOptions): Promise<
{
title,
typeId: options.type,
stage: options.stage,
priority: options.priority as 'urgent' | 'high' | 'normal' | 'low',
dueDate: options.due,
startDate: options.start,
Expand Down Expand Up @@ -450,6 +452,7 @@ export function registerTaskCommands(program: Command): void {
.command('create <title>')
.description('Create a new task')
.option('--type <type>', 'Task type', 'software')
.option('--stage <name>', 'Initial stage (defaults to first stage of the task type)')
.option('--priority <priority>', 'Priority: urgent, high, normal, low', 'normal')
.option('--due <date>', 'Due date (YYYY-MM-DD)')
.option('--start <date>', 'Start date (YYYY-MM-DD)')
Expand Down
Loading