feat(autostart): agenthub agent setup — SessionStart hook enters the work loop

Closes the dormancy gap: an agent no longer needs to be told "call agenthub_work".
`agenthub agent setup --agent <name> --role <role>` writes a verified Claude Code
SessionStart hook (.claude/settings.json) whose command runs `agenthub hook-context`,
which prints the work-loop instruction injected into every new session — so the
agent claims tasks AND surfaces messages from the first turn, no manual trigger.

- agentSetup.ts: agentSetup() (idempotent hook write, project or --user scope) +
  hookContext() (the injected instruction).
- Honest limit (per Claude Code docs): SessionStart context is a strong nudge the
  model reads, not a hard directive; the guide reinforces it. Can't block startup.
- Codex/Kimi aren't Claude Code — their guide already nudges agenthub_work first.

Run once per agent machine. Bump 0.8.1 -> 0.9.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-29 04:57:30 +02:00
parent ed6c5b01d0
commit d00f2812f9
3 changed files with 91 additions and 2 deletions

View File

@ -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",

View File

@ -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.');
}

View File

@ -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<void>): Promise<vo
export function createProgram(cwd: string): Command {
const program = new Command('agenthub')
.description('Local coordination layer for AI coding agents')
.version('0.8.1')
.version('0.9.0')
.option('--server <url>', '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 <name>', 'This machine\'s agent name')
.requiredOption('--role <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 <name>', 'Agent name')
.requiredOption('--role <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')