diff --git a/package.json b/package.json index 80b1b99..717d910 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agenthub", - "version": "0.8.1", + "version": "0.9.0", "description": "Local coordination layer for AI coding agents", "type": "module", "main": "./dist/index.js", diff --git a/src/cli/commands/agentSetup.ts b/src/cli/commands/agentSetup.ts new file mode 100644 index 0000000..f407f6f --- /dev/null +++ b/src/cli/commands/agentSetup.ts @@ -0,0 +1,65 @@ +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { homedir } from 'node:os'; +import { findProjectRoot } from '../../core/paths.js'; + +/** + * Auto-start: make a Claude Code agent enter the agenthub work-loop the moment a + * session starts — no manual "call agenthub_work" needed. + * + * Mechanism: a verified Claude Code `SessionStart` hook (.claude/settings.json) + * whose command runs `agenthub hook-context`, which prints the work-loop + * instruction. That text is injected into the new session as context. + * + * Honest limit (per the Claude Code docs): SessionStart context is a strong + * NUDGE the model reads, not a hard directive it must obey — so the guide + * reinforces it. It also can't block startup. Codex/Kimi aren't Claude Code; + * their guide (CODEX.md / KIMI.md) carries the same "call agenthub_work first" + * nudge. + */ + +/** Printed by the SessionStart hook → injected as session context. */ +export function hookContext(opts: { agent: string; role: string }): void { + const { agent, role } = opts; + process.stdout.write( + `AgentHub auto-start — you are "${agent}" (${role}). Before anything else, run:\n` + + ` agenthub work --agent ${agent} --role ${role}\n` + + `(or call the agenthub_work tool with { agent: "${agent}", role: "${role}" }), then LOOP: ` + + `work -> implement -> agenthub_task_review -> work. agenthub_work claims your tasks AND ` + + `surfaces your messages. Never run agenthub_task_done. Keep looping so you stay reachable.\n`, + ); +} + +export function agentSetup(cwd: string, opts: { agent: string; role: string; user?: boolean }): void { + const base = opts.user ? homedir() : (findProjectRoot(cwd) ?? cwd); + const dir = join(base, '.claude'); + const settingsPath = join(dir, 'settings.json'); + const command = `agenthub hook-context --agent ${opts.agent} --role ${opts.role}`; + const hookGroup = { matcher: 'startup|resume', hooks: [{ type: 'command', command }] }; + + let settings: { hooks?: { SessionStart?: unknown[] } } = {}; + if (existsSync(settingsPath)) { + try { + settings = JSON.parse(readFileSync(settingsPath, 'utf-8')) as typeof settings; + } catch { + settings = {}; + } + } + if (!settings.hooks) settings.hooks = {}; + if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = []; + + // Idempotent: drop any prior agenthub hook (identified by the command), re-add. + settings.hooks.SessionStart = (settings.hooks.SessionStart as Array<{ hooks?: Array<{ command?: string }> }>).filter( + (g) => !(g?.hooks ?? []).some((h) => typeof h?.command === 'string' && h.command.includes('agenthub hook-context')), + ); + (settings.hooks.SessionStart as unknown[]).push(hookGroup); + + mkdirSync(dir, { recursive: true }); + writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8'); + + const out = (s: string) => process.stdout.write(s + '\n'); + out(`AgentHub: auto-start hook written -> ${settingsPath}`); + out(` New Claude Code sessions ${opts.user ? '(all projects)' : 'in this project'} start as "${opts.agent}" (${opts.role}) and enter the work loop.`); + out(' Note: SessionStart context is a strong nudge, not a hard guarantee — the guide reinforces it.'); + out(' Codex/Kimi are not Claude Code: their guide already nudges agenthub_work on first turn.'); +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 28854d1..3482e20 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -6,6 +6,7 @@ import { taskCreate, taskList, taskShow, taskClaim, taskDone, taskReview, taskRe import { handoffCreate, handoffRead, handoffList } from './commands/handoff.js'; import { decisionCreate, decisionList } from './commands/decision.js'; import { messageSend, inboxList } from './commands/message.js'; +import { agentSetup, hookContext } from './commands/agentSetup.js'; import { delegate } from './commands/delegate.js'; import { serverStart } from './commands/server.js'; import { update } from './commands/update.js'; @@ -117,7 +118,7 @@ async function runRemote(serverUrl: string, fn: () => Promise): Promise', 'AgentHub server URL (env: AGENTHUB_SERVER)'); program @@ -493,6 +494,29 @@ export function createProgram(cwd: string): Command { } }); + // ─── auto-start ────────────────────────────────────────────────────────── + const agentCmd = new Command('agent').description('Per-agent machine setup'); + agentCmd + .command('setup') + .description('Auto-start: write a Claude Code SessionStart hook so sessions enter the work loop') + .requiredOption('--agent ', 'This machine\'s agent name') + .requiredOption('--role ', 'This agent\'s role (implementer / architect / tester)') + .option('--user', 'Install for all projects (~/.claude) instead of just this one') + .action((options: { agent: string; role: string; user?: boolean }) => { + agentSetup(cwd, { agent: options.agent, role: options.role, user: options.user }); + }); + program.addCommand(agentCmd); + + // Called by the SessionStart hook; prints the work-loop instruction as context. + program + .command('hook-context') + .description('Print the auto-start work-loop instruction (used by the SessionStart hook)') + .requiredOption('--agent ', 'Agent name') + .requiredOption('--role ', 'Agent role') + .action((options: { agent: string; role: string }) => { + hookContext({ agent: options.agent, role: options.role }); + }); + program .command('delegate') .description('Suggest or auto-delegate open tasks')