UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

972 lines (878 loc) • 30.8 kB
#!/usr/bin/env node import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync, } from 'node:fs'; import { spawn, spawnSync } from 'node:child_process'; import { homedir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; export const LAUNCHER_REVISION = 1; export const PLUGIN_REPO_URL = 'https://github.com/Eliyce/paqad-claude-plugin.git'; export const PLUGIN_ID = 'paqad@eliyce'; export const SETUP_LINE_LAPTOP = 'Setting up paqad for this repo (one time).'; export const SETUP_LINE_CLOUD = 'Setting up paqad for this cloud session.'; export const FAILED_NOTICE = 'paqad could not be set up for this repo, so its checks and evidence are off for this session. Tell the user this once, in one sentence, then continue normally.'; export const STILL_SETTING_UP = 'paqad is still being set up for this repo, so this edit is blocked until it is ready; try it again in a moment.'; export const REFRESH_FLAG = '--paqad-team-refresh'; export const LOCK_STALE_MS = 600 * 1000; export const LOCK_WAIT_MS = 590 * 1000; export const LOCK_POLL_MS = 500; export const CLONE_TIMEOUT_MS = 540 * 1000; export const EDIT_WAIT_MS = 5 * 1000; export const REFRESH_AGE_MS = 6 * 60 * 60 * 1000; export const PLUGIN_CLAIM_WAIT_MS = 2 * 1000; export const PLUGIN_CLAIM_POLL_MS = 100; export const MIN_NODE_MAJOR = 22; export const REQUIRED_COPY_SCRIPTS = [ 'plugin-session-start.mjs', 'agent-entry-session-start.mjs', 'agent-entry-gate.mjs', 'decision-pause-gate.mjs', 'capability-gate.mjs', ]; const ENV_TRUTHY = ['1', 'true', 'yes', 'on']; const FALSY = ['false', '0', 'no', 'off']; const BOOLEAN_WORDS = ENV_TRUTHY.concat(FALSY); export const LEGACY_TEAM_CONFIG_READ_UNTIL = '2027-02-01'; const SCRIPT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*\.mjs$/; const SESSION_ID = /^[A-Za-z0-9_-]{1,128}$/; const SCRIPT_EVENTS = { 'agent-entry-gate.mjs': 'PreToolUse', 'stage-writer.mjs': 'PreToolUse', 'decision-pause-gate.mjs': 'PreToolUse', 'agent-entry-prompt-gate.mjs': 'UserPromptSubmit', 'ticket-intake-prompt.mjs': 'UserPromptSubmit', 'agent-entry-session-start.mjs': 'SessionStart', 'silent-update.mjs': 'SessionStart', 'stage-marker-parse.mjs': 'Stop', 'verification-completion.mjs': 'Stop', 'stage-agent-start.mjs': 'SubagentStart', 'stage-agent-completion.mjs': 'SubagentStop', 'skill-activation.mjs': 'PostToolUse', 'agent-delegation.mjs': 'PostToolUse', }; function lower(value) { return typeof value === 'string' ? value.trim().toLowerCase() : ''; } function slash(path) { return String(path).split('\\').join('/'); } export function readKeyFromFile(path, key) { let raw; try { raw = readFileSync(path, 'utf8'); } catch { return undefined; } let value; const re = new RegExp('^\\s*(?:export\\s+)?' + key + '\\s*=(.*)$'); const lines = raw.split(/\r?\n/); for (let i = 0; i < lines.length; i += 1) { const m = lines[i].match(re); if (m) value = m[1]; } if (value === undefined) return undefined; let v = value.trim(); if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) { v = v.slice(1, -1); } else { const hash = v.search(/\s#/); if (hash !== -1) v = v.slice(0, hash).trim(); } return v; } function isMigratedStubFile(path) { try { return readFileSync(path, 'utf8').split(/\r?\n/, 1)[0].trim() === '# paqad-migrated-stub'; } catch { return false; } } function readKeyFromConfigsDir(projectRoot, key) { const dir = join(projectRoot, '.paqad', 'configs'); let names; try { names = readdirSync(dir); } catch { return undefined; } let value; const sorted = names.filter((n) => /^\.config\..+/.test(n) && n !== '.config.example').sort(); for (let i = 0; i < sorted.length; i += 1) { const path = join(dir, sorted[i]); if (isMigratedStubFile(path)) continue; const fromFile = readKeyFromFile(path, key); if (fromFile !== undefined) value = fromFile; } return value; } function indentOf(line) { const match = /^[ \t]*/.exec(line); return match ? match[0].length : 0; } function isContent(line) { const trimmed = line.trim(); return trimmed !== '' && !trimmed.startsWith('#'); } function keyValue(line, key) { const body = line.replace(/^[ \t]+/, ''); if (!body.startsWith(`${key}:`)) return null; const rest = body.slice(key.length + 1); if (rest !== '' && !/^[ \t]/.test(rest)) return null; return rest; } function cleanScalar(rest) { let value = ` ${rest}`; const hash = value.search(/[ \t]#/); if (hash !== -1) value = value.slice(0, hash); value = value.trim(); if ( value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) ) { value = value.slice(1, -1); } return value === '' ? undefined : value; } export function readYamlScalar(text, path) { if (typeof text !== 'string' || !Array.isArray(path) || path.length < 1 || path.length > 2) { return undefined; } const lines = text.split(/\r?\n/); const [top, child] = path; for (let i = 0; i < lines.length; i += 1) { const line = lines[i]; if (!isContent(line) || indentOf(line) !== 0) continue; const rest = keyValue(line, top); if (rest === null) continue; if (child === undefined) return cleanScalar(rest); let childIndent = -1; for (let j = i + 1; j < lines.length; j += 1) { const inner = lines[j]; if (!isContent(inner)) continue; const indent = indentOf(inner); if (indent === 0) return undefined; if (childIndent === -1) childIndent = indent; if (indent !== childIndent) continue; const value = keyValue(inner, child); if (value !== null) return cleanScalar(value); } return undefined; } return undefined; } const TEAM_YAML_HOMES = { paqad_enable: ['enabled'], team_bootstrap_enabled: ['team_bootstrap', 'enabled'], }; function readTeamYamlKey(projectRoot, key) { const path = TEAM_YAML_HOMES[key]; if (path === undefined) return undefined; let text; try { text = readFileSync(join(projectRoot, '.paqad', 'configs', 'paqad.yaml'), 'utf8'); } catch { return undefined; } const raw = readYamlScalar(text, path); return raw !== undefined && BOOLEAN_WORDS.indexOf(lower(raw)) !== -1 ? raw : undefined; } export function readLayeredKey(projectRoot, key, envName, env) { let value = readTeamYamlKey(projectRoot, key); if (value === undefined && new Date().toISOString().slice(0, 10) <= LEGACY_TEAM_CONFIG_READ_UNTIL) { value = readKeyFromConfigsDir(projectRoot, key); } const local = readKeyFromFile(join(projectRoot, '.paqad', '.config'), key); if (local !== undefined) value = local; const fromEnv = env[envName]; if (typeof fromEnv === 'string' && fromEnv.trim() !== '') value = fromEnv.trim(); return value; } export function keyIsOff(projectRoot, key, envName, env) { const raw = readLayeredKey(projectRoot, key, envName, env); return raw !== undefined && FALSY.indexOf(lower(raw)) !== -1; } export function offSwitch(projectRoot, env) { if (ENV_TRUTHY.indexOf(lower(env.PAQAD_DISABLED)) !== -1) return 'PAQAD_DISABLED'; if (keyIsOff(projectRoot, 'paqad_enable', 'PAQAD_ENABLE', env)) return 'paqad_enable'; return null; } export function canonicalRoot(path) { let full; try { full = realpathSync.native(path); } catch { full = resolve(path); } const trimmed = full.replace(/[\\/]+$/, ''); return process.platform === 'win32' ? trimmed.toLowerCase() : trimmed; } export function claudeDir(env, home) { return env.CLAUDE_CONFIG_DIR || join(home, '.claude'); } function readJson(path) { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return undefined; } } function pluginEntries(parsed) { const plugins = parsed.plugins && typeof parsed.plugins === 'object' ? parsed.plugins : parsed; const value = plugins[PLUGIN_ID]; return Array.isArray(value) ? value : [value]; } export function installedPluginRoots(env, home) { const parsed = readJson(join(claudeDir(env, home), 'plugins', 'installed_plugins.json')); if (!parsed || typeof parsed !== 'object') return null; const roots = pluginEntries(parsed) .map((entry) => (entry && typeof entry === 'object' ? entry.installPath : undefined)) .filter((path) => typeof path === 'string' && path.trim() !== '') .map((path) => canonicalRoot(path)); return roots.length > 0 ? roots : null; } function entryLoadsHere(entry, projectRoot) { if (!entry || typeof entry !== 'object') return false; const path = entry.installPath; if (typeof path !== 'string' || path.trim() === '' || !existsSync(path)) return false; const scope = typeof entry.scope === 'string' ? entry.scope : 'user'; if (scope !== 'project' && scope !== 'local') return true; return ( typeof entry.projectPath === 'string' && canonicalRoot(entry.projectPath) === canonicalRoot(projectRoot) ); } export function pluginSettingsLayers(projectRoot, env, home) { return [ join(claudeDir(env, home), 'settings.json'), join(projectRoot, '.claude', 'settings.json'), join(projectRoot, '.claude', 'settings.local.json'), ]; } export function pluginEnabledInSettings(projectRoot, env, home) { let enabled; const layers = pluginSettingsLayers(projectRoot, env, home); for (let i = 0; i < layers.length; i += 1) { const settings = readJson(layers[i]); const map = settings && typeof settings === 'object' ? settings.enabledPlugins : undefined; if (map && typeof map === 'object' && typeof map[PLUGIN_ID] === 'boolean') { enabled = map[PLUGIN_ID]; } } return enabled !== false; } export function pluginLoadState(projectRoot, env, home) { const parsed = readJson(join(claudeDir(env, home), 'plugins', 'installed_plugins.json')); if (!parsed || typeof parsed !== 'object' || parsed.version !== 2) return 'unknown'; if (!parsed.plugins || typeof parsed.plugins !== 'object') return 'unknown'; const loads = pluginEntries(parsed).some((entry) => entryLoadsHere(entry, projectRoot)); if (!loads) return 'absent'; return pluginEnabledInSettings(projectRoot, env, home) ? 'live' : 'absent'; } export function teamPluginRoot(home) { return join(home, '.paqad-ai', 'team-plugin'); } export function teamPluginData(home) { return join(home, '.paqad-ai', 'team-plugin-data'); } function isUnder(path, root) { const p = slash(path); const r = slash(root); return p === r || p.startsWith(r + '/'); } function packageNameAt(dir) { const parsed = readJson(join(dir, 'package.json')); return parsed && typeof parsed.name === 'string' ? parsed.name : null; } export function npmRuntimeTarget(env, home) { const link = env.PAQAD_FRAMEWORK_HOME || join(home, '.paqad-ai', 'current'); let target; try { target = realpathSync.native(link); } catch { return null; } const canon = canonicalRoot(target); if (isUnder(canon, canonicalRoot(teamPluginRoot(home)))) return null; if (isUnder(canon, canonicalRoot(teamPluginData(home)))) return null; if (slash(canon).indexOf('/plugins/') !== -1) return null; const underNodeModules = slash(canon).indexOf('/node_modules/') !== -1; if (!underNodeModules && packageNameAt(dirname(target)) !== 'paqad-ai') return null; return existsSync(join(target, 'hooks')) ? target : null; } export function inferEvent(payload, script, args) { if (payload && typeof payload.hook_event_name === 'string' && payload.hook_event_name !== '') { return payload.hook_event_name; } if (script === 'capability-gate.mjs') { return args[0] === 'completion' ? 'Stop' : 'PreToolUse'; } return SCRIPT_EVENTS[script] || ''; } function parsePayload(stdin) { try { const parsed = JSON.parse(stdin.toString('utf8')); return parsed && typeof parsed === 'object' ? parsed : undefined; } catch { return undefined; } } export function safeSessionId(payload) { const raw = payload && typeof payload.session_id === 'string' ? payload.session_id.trim() : ''; return SESSION_ID.test(raw) ? raw : null; } function sessionDir(projectRoot, name) { return join(projectRoot, '.paqad', 'session', name); } export function routeRecordPath(projectRoot, sid) { return join(sessionDir(projectRoot, 'team-route.d'), sid); } export function noticePath(projectRoot, sid) { return join(sessionDir(projectRoot, 'team-bootstrap-notice.d'), sid); } export function pluginClaimPath(projectRoot, sid) { return join(sessionDir(projectRoot, 'plugin-active.d'), sid); } export function pluginClaimsSession(projectRoot, sid, home) { if (!sid) return false; const parsed = readJson(pluginClaimPath(projectRoot, sid)); const root = parsed && typeof parsed.root === 'string' ? parsed.root.trim() : ''; if (root === '' || !existsSync(root)) return false; return !isUnder(canonicalRoot(root), canonicalRoot(teamPluginRoot(home))); } function claimedByPlugin(ctx) { if (pluginClaimsSession(ctx.projectRoot, ctx.sid, ctx.home)) return true; if (ctx.event !== 'SessionStart' || !ctx.sid) return false; if (pluginLoadState(ctx.projectRoot, ctx.env, ctx.home) !== 'live') return false; for (let waited = 0; waited < PLUGIN_CLAIM_WAIT_MS; waited += PLUGIN_CLAIM_POLL_MS) { ctx.deps.sleep(PLUGIN_CLAIM_POLL_MS); if (pluginClaimsSession(ctx.projectRoot, ctx.sid, ctx.home)) return true; } return false; } function readRoute(projectRoot, sid) { if (!sid) return null; const parsed = readJson(routeRecordPath(projectRoot, sid)); return parsed && typeof parsed.route === 'string' ? parsed : null; } function writeRoute(projectRoot, sid, record, ctx) { if (!sid) return; try { mkdirSync(sessionDir(projectRoot, 'team-route.d'), { recursive: true }); writeFileSync(routeRecordPath(projectRoot, sid), JSON.stringify(record)); } catch (error) { logLine(projectRoot, 'route record not written: ' + errorCode(error), ctx); } } function errorCode(error) { return error && typeof error === 'object' && error.code ? String(error.code) : 'error'; } export function logLine(projectRoot, line, ctx) { try { const dir = join(projectRoot, '.paqad', 'logs'); mkdirSync(dir, { recursive: true }); appendFileSync( join(dir, 'hooks.log'), new Date(ctx.deps.now()).toISOString() + ' team-launcher: ' + line + '\n', ); } catch { return; } } function claimNotice(projectRoot, sid) { if (!sid) return true; try { mkdirSync(sessionDir(projectRoot, 'team-bootstrap-notice.d'), { recursive: true }); writeFileSync(noticePath(projectRoot, sid), '', { flag: 'wx' }); return true; } catch { return false; } } function lockDir(home) { return join(home, '.paqad-ai', 'locks', 'team-plugin.lock'); } function defaultPidAlive(pid) { try { process.kill(pid, 0); return true; } catch (error) { return errorCode(error) === 'EPERM'; } } function lockOwner(home) { return readJson(join(lockDir(home), 'owner.json')); } export function lockIsStale(home, deps, judgedOwner) { const dir = lockDir(home); const owner = judgedOwner === undefined ? lockOwner(home) : judgedOwner; let started; if (owner && typeof owner.started_at === 'number') { started = owner.started_at; } else { try { started = statSync(dir).mtimeMs; } catch { return false; } } if (deps.now() - started > LOCK_STALE_MS) return true; return Boolean(owner && typeof owner.pid === 'number' && !deps.pidAlive(owner.pid)); } function lockHeld(home) { return existsSync(lockDir(home)); } function sameOwner(a, b) { return Boolean(a && b && a.pid === b.pid && a.started_at === b.started_at); } function mkdirLock(home, deps) { const dir = lockDir(home); try { mkdirSync(dir); } catch { return false; } const mine = { pid: deps.pid, started_at: deps.now() }; try { writeFileSync(join(dir, 'owner.json'), JSON.stringify(mine)); } catch { rmSync(dir, { recursive: true, force: true }); return false; } return sameOwner(readJson(join(dir, 'owner.json')), mine); } function takeOverStaleLock(home, deps, judged) { const dir = lockDir(home); const grave = dir + '.stale-' + deps.pid; rmSync(grave, { recursive: true, force: true }); try { renameSync(dir, grave); } catch { return false; } const moved = readJson(join(grave, 'owner.json')); if (judged && !sameOwner(moved, judged)) { try { renameSync(grave, dir); } catch { rmSync(grave, { recursive: true, force: true }); } return false; } rmSync(grave, { recursive: true, force: true }); return true; } export function acquireLock(home, deps) { try { mkdirSync(dirname(lockDir(home)), { recursive: true }); } catch { return false; } if (mkdirLock(home, deps)) return true; const judged = lockOwner(home); if (!lockIsStale(home, deps, judged)) return false; if (!takeOverStaleLock(home, deps, judged)) return false; return mkdirLock(home, deps); } const RENAME_RETRY_CODES = ['EPERM', 'EACCES', 'EBUSY']; const RENAME_ATTEMPTS = 5; export function renameWithRetry(from, to, deps) { for (let attempt = 1; ; attempt += 1) { try { (deps.rename || renameSync)(from, to); return; } catch (error) { const retry = deps.platform === 'win32' && RENAME_RETRY_CODES.indexOf(errorCode(error)) !== -1; if (!retry || attempt >= RENAME_ATTEMPTS) throw error; deps.sleep(20 * Math.pow(2, attempt - 1)); } } } export function releaseLock(home) { rmSync(lockDir(home), { recursive: true, force: true }); } function copyIsPresent(home) { return existsSync(join(teamPluginRoot(home), 'runtime', 'hooks')); } export function waitForCopy(home, deps, limitMs) { const deadline = deps.now() + limitMs; for (;;) { if (copyIsPresent(home)) return true; if (!lockHeld(home) || lockIsStale(home, deps)) return false; if (deps.now() >= deadline) return false; deps.sleep(LOCK_POLL_MS); } } function tempPrefix() { return 'team-plugin.tmp-'; } export function sweepTemps(home, deps) { const base = join(home, '.paqad-ai'); let names; try { names = readdirSync(base); } catch { return; } for (let i = 0; i < names.length; i += 1) { const name = names[i]; if (!name.startsWith(tempPrefix()) && !name.startsWith('team-plugin.old-')) continue; const pid = Number(name.slice(name.lastIndexOf('-') + 1)); if (pid === deps.pid || (Number.isInteger(pid) && pid > 0 && deps.pidAlive(pid))) continue; rmSync(join(base, name), { recursive: true, force: true }); } } function copyIsComplete(dir) { const hooks = join(dir, 'runtime', 'hooks'); return REQUIRED_COPY_SCRIPTS.every((name) => existsSync(join(hooks, name))); } export function cloneInto(home, deps, ctx) { const tmp = join(home, '.paqad-ai', tempPrefix() + deps.pid); rmSync(tmp, { recursive: true, force: true }); const result = deps.runGit(['clone', '--depth', '1', '--quiet', PLUGIN_REPO_URL, tmp], { timeout: CLONE_TIMEOUT_MS, }); if (!result || result.status !== 0 || result.error || !copyIsComplete(tmp)) { rmSync(tmp, { recursive: true, force: true }); const why = result && result.error ? errorCode(result.error) : 'exit ' + (result && result.status); logLine(ctx.projectRoot, 'clone failed (' + why + ')', ctx); return null; } return tmp; } function installCopy(home, deps, ctx) { const tmp = cloneInto(home, deps, ctx); if (!tmp) return false; try { renameWithRetry(tmp, teamPluginRoot(home), deps); return true; } catch (error) { rmSync(tmp, { recursive: true, force: true }); logLine(ctx.projectRoot, 'install rename failed (' + errorCode(error) + ')', ctx); return false; } } export function swapCopy(home, tmp, deps) { const live = teamPluginRoot(home); const old = join(home, '.paqad-ai', 'team-plugin.old-' + deps.pid); rmSync(old, { recursive: true, force: true }); renameWithRetry(live, old, deps); try { renameWithRetry(tmp, live, deps); } catch (error) { renameWithRetry(old, live, deps); throw error; } rmSync(old, { recursive: true, force: true }); } export function runRefresh(home, deps, ctx) { if (!acquireLock(home, deps)) return 'busy'; try { sweepTemps(home, deps); const tmp = cloneInto(home, deps, ctx); if (!tmp) { logLine(ctx.projectRoot, 'refresh failed; the current copy keeps working', ctx); return 'failed'; } try { swapCopy(home, tmp, deps); } catch (error) { rmSync(tmp, { recursive: true, force: true }); logLine(ctx.projectRoot, 'refresh swap failed (' + errorCode(error) + ')', ctx); return 'failed'; } return 'refreshed'; } finally { releaseLock(home); } } export function copyAgeMs(home, deps) { const root = teamPluginRoot(home); let stamp; try { stamp = statSync(join(root, '.git', 'FETCH_HEAD')).mtimeMs; } catch { try { stamp = statSync(root).mtimeMs; } catch { return 0; } } return deps.now() - stamp; } function maybeStartRefresh(home, deps, ctx) { if (copyAgeMs(home, deps) <= REFRESH_AGE_MS) return false; try { deps.spawnDetached(process.execPath, [deps.launcherPath, REFRESH_FLAG, ctx.projectRoot], { detached: true, stdio: 'ignore', windowsHide: true, env: ctx.env, }); return true; } catch (error) { logLine(ctx.projectRoot, 'refresh not started (' + errorCode(error) + ')', ctx); return false; } } function appendEnvFileLine(env, line) { const file = env.CLAUDE_ENV_FILE; if (typeof file !== 'string' || file === '') return; let existing; try { existing = readFileSync(file, 'utf8'); } catch { existing = ''; } if (existing.split(/\r?\n/).indexOf(line) !== -1) return; const lead = existing === '' || existing.endsWith('\n') ? '' : '\n'; appendFileSync(file, lead + line + '\n'); } function isRemote(env) { return lower(env.CLAUDE_CODE_REMOTE) === 'true'; } function copyEnv(env, home) { const out = Object.assign({}, env, { CLAUDE_PLUGIN_ROOT: teamPluginRoot(home), CLAUDE_PLUGIN_DATA: teamPluginData(home), }); if (isRemote(env)) out.PAQAD_RAG_ENABLED = 'false'; return out; } function runChild(hookPath, args, ctx, env) { if (!existsSync(hookPath)) return { started: false }; const result = ctx.deps.spawnSync(process.execPath, [hookPath].concat(args), { input: ctx.stdin, stdio: ['pipe', 'inherit', 'inherit'], env, windowsHide: true, }); if (!result || result.error || typeof result.status !== 'number') return { started: false }; return { started: true, status: result.status }; } function exitFor(child, ctx) { const blocking = ctx.event === 'PreToolUse'; if (!child.started) return blocking ? 2 : 0; if (child.status === 0 || child.status === 2) return child.status; return blocking ? 2 : child.status; } function missingScript(ctx, route) { if (ctx.event !== 'PreToolUse') return 0; const reason = route === 'npm' ? ' because your paqad-ai install does not have it, so update your own paqad-ai (for example `npm install -g paqad-ai@latest`) and try again.\n' : ' because the paqad setup committed in this repo is out of date, so the lead must run `paqad-ai update` and commit the result.\n'; ctx.deps.stderr('paqad cannot run ' + ctx.script + reason); return 2; } function runNpm(target, ctx) { const hookPath = join(target, 'hooks', ctx.script); if (!ctx.validScript || !existsSync(hookPath)) return missingScript(ctx, 'npm'); return exitFor(runChild(hookPath, ctx.args, ctx, ctx.env), ctx); } function runCopy(ctx) { const home = ctx.home; if (ctx.script === 'silent-update.mjs') return 0; const hooks = join(teamPluginRoot(home), 'runtime', 'hooks'); const hookPath = join(hooks, ctx.script); if (!ctx.validScript || !existsSync(hookPath)) return missingScript(ctx, 'copy'); const env = copyEnv(ctx.env, home); if (ctx.event === 'SessionStart' && isRemote(ctx.env)) { try { appendEnvFileLine(ctx.env, 'PAQAD_RAG_ENABLED=false'); } catch (error) { logLine(ctx.projectRoot, 'env file not updated (' + errorCode(error) + ')', ctx); } } if (ctx.script === 'agent-entry-session-start.mjs') { runChild(join(hooks, 'plugin-session-start.mjs'), [], ctx, env); } return exitFor(runChild(hookPath, ctx.args, ctx, env), ctx); } function printNotice(ctx) { if (ctx.event !== 'SessionStart' && ctx.event !== 'UserPromptSubmit') return; if (!claimNotice(ctx.projectRoot, ctx.sid)) return; ctx.deps.stdout(FAILED_NOTICE + '\n'); } export function setupMessage(env) { const line = isRemote(env) ? SETUP_LINE_CLOUD : SETUP_LINE_LAPTOP; return '{"systemMessage": "' + line + '"}\n'; } function nodeTooOld(deps) { return deps.nodeMajor < MIN_NODE_MAJOR; } function installAtSessionStart(ctx) { const home = ctx.home; const deps = ctx.deps; if (nodeTooOld(deps)) return { route: 'none', reason: 'node-too-old' }; if (acquireLock(home, deps)) { let installed; try { if (copyIsPresent(home)) return { route: 'copy', reason: 'installed-by-peer' }; sweepTemps(home, deps); installed = installCopy(home, deps, ctx); } finally { releaseLock(home); } if (!installed) return { route: 'none', reason: 'install-failed' }; deps.stdout(setupMessage(ctx.env)); return { route: 'copy', reason: 'installed' }; } if (waitForCopy(home, deps, LOCK_WAIT_MS)) return { route: 'copy', reason: 'installed-by-peer' }; return { route: 'none', reason: 'install-wait-ended' }; } export function decideRoute(ctx) { const off = offSwitch(ctx.projectRoot, ctx.env); if (off) return { route: 'off', reason: off }; if (claimedByPlugin(ctx)) return { route: 'plugin', reason: 'plugin-claimed' }; const target = npmRuntimeTarget(ctx.env, ctx.home); if (target) return { route: 'npm', reason: 'npm-installed', target }; if (keyIsOff(ctx.projectRoot, 'team_bootstrap_enabled', 'PAQAD_TEAM_BOOTSTRAP_ENABLED', ctx.env)) { return { route: 'none', reason: 'team-bootstrap-disabled' }; } if (copyIsPresent(ctx.home)) { if (nodeTooOld(ctx.deps)) return { route: 'none', reason: 'node-too-old' }; return { route: 'copy', reason: 'copy-present' }; } return { route: 'none', reason: 'no-copy' }; } function sessionStart(ctx) { let decided = decideRoute(ctx); if (decided.route === 'copy') { sweepTemps(ctx.home, ctx.deps); maybeStartRefresh(ctx.home, ctx.deps, ctx); } else if (decided.route === 'none' && decided.reason === 'no-copy') { decided = installAtSessionStart(ctx); } writeRoute(ctx.projectRoot, ctx.sid, decided, ctx); if (ctx.script === 'agent-entry-session-start.mjs' || !ctx.sid) { logLine( ctx.projectRoot, 'revision=' + LAUNCHER_REVISION + ' route=' + decided.route + ' reason=' + decided.reason, ctx, ); } return follow(decided, ctx); } function laterEvent(ctx) { const recorded = readRoute(ctx.projectRoot, ctx.sid); const stale = recorded && recorded.route === 'plugin' && !pluginClaimsSession(ctx.projectRoot, ctx.sid, ctx.home); if (recorded && !stale) { if (ENV_TRUTHY.indexOf(lower(ctx.env.PAQAD_DISABLED)) !== -1) return 0; return follow(recorded, ctx); } const decided = decideRoute(ctx); if (decided.route === 'none' && decided.reason === 'no-copy' && lockHeld(ctx.home)) { if (lockIsStale(ctx.home, ctx.deps)) return follow(decided, ctx); if (ctx.event !== 'PreToolUse') return 0; if (waitForCopy(ctx.home, ctx.deps, EDIT_WAIT_MS)) { return follow({ route: 'copy', reason: 'installed-while-waiting' }, ctx); } ctx.deps.stderr(STILL_SETTING_UP + '\n'); return 2; } return follow(decided, ctx); } function follow(decided, ctx) { if (decided.route === 'npm' && typeof decided.target === 'string') { return runNpm(decided.target, ctx); } if (decided.route === 'copy') return runCopy(ctx); if (decided.route === 'none' && decided.reason !== 'team-bootstrap-disabled') printNotice(ctx); return 0; } function defaultDeps() { return { home: homedir, now: Date.now, pid: process.pid, platform: process.platform, nodeMajor: Number(String(process.versions.node).split('.')[0]), sleep: (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms), pidAlive: defaultPidAlive, spawnSync, spawnDetached: (cmd, args, opts) => spawn(cmd, args, opts).unref(), runGit: (args, opts) => spawnSync('git', args, { stdio: 'ignore', timeout: opts.timeout, windowsHide: true, env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: '0' }), }), readStdin: () => { try { return readFileSync(0); } catch { return Buffer.alloc(0); } }, stdout: (text) => writeSync(1, text), stderr: (text) => writeSync(2, text), launcherPath: fileURLToPath(import.meta.url), }; } function projectRootFrom(env, deps) { if (deps.projectRoot) return deps.projectRoot; if (typeof env.CLAUDE_PROJECT_DIR === 'string' && env.CLAUDE_PROJECT_DIR !== '') { return env.CLAUDE_PROJECT_DIR; } return dirname(dirname(deps.launcherPath)); } export function runLauncher(argv, env, overrides) { const deps = Object.assign(defaultDeps(), overrides || {}); const home = deps.home(); if (argv[0] === REFRESH_FLAG) { const projectRoot = argv[1] || projectRootFrom(env, deps); runRefresh(home, deps, { projectRoot, env, deps }); return 0; } const script = typeof argv[0] === 'string' ? argv[0] : ''; const args = argv.slice(1); const stdin = deps.readStdin(); const payload = parsePayload(stdin); const ctx = { deps, env, home, stdin, script, args, validScript: SCRIPT_NAME.test(script), event: inferEvent(payload, script, args), sid: safeSessionId(payload), projectRoot: projectRootFrom(env, deps), }; if (ENV_TRUTHY.indexOf(lower(env.PAQAD_DISABLED)) !== -1) return 0; try { return ctx.event === 'SessionStart' ? sessionStart(ctx) : laterEvent(ctx); } catch (error) { logLine(ctx.projectRoot, 'launcher error (' + errorCode(error) + ')', ctx); return ctx.event === 'PreToolUse' ? 2 : 0; } } function isDirectEntry(url, argv1) { if (!argv1) return false; try { return url === pathToFileURL(realpathSync(argv1)).href; } catch { return false; } } if (isDirectEntry(import.meta.url, process.argv[1])) { process.exitCode = runLauncher(process.argv.slice(2), process.env); }