beadboard/scripts/bb-init.mjs
zenchantlive e010e0b10b feat(protocol): deliver Activity Lease model (Zero Background Workers)
Following a critical collaboration to resolve Windows terminal pop-ups, we've delivered a more robust 'Passive Activity' architecture:
- Terminology Pivot: Renamed 'Heartbeat' to 'Activity Lease' (Parking Permit model).
- Side-Effect Extension: tools/bb.ts now automatically extends the agent's lease whenever they perform real work (any CLI command).
- Passive Handshake: bb-init.mjs now only performs an initial registration/lease start, with no background loops.
- 100% Silence: Removed all background process spawning, ensuring zero terminal disruption on Windows.
- High Observability: Liveness is still tracked via the 15m threshold, but relies on activity rather than periodic pings.

OPERATIVE: silver-castle
SESSION: 2026-02-14-1330
2026-02-14 11:18:40 -08:00

126 lines
No EOL
3.3 KiB
JavaScript

#!/usr/bin/env node
/**
* bb-init.mjs - Agent Session Bootstrapper (Lease-Based)
*
* Part of Operative Protocol v1 (bb-u6f.6.3)
*
* Responsibility:
* 1. Resolve bb.ps1 path.
* 2. Identify agent (adopt or register).
* 3. Start the initial activity lease.
*
* Note: No background processes are spawned. Liveness is maintained
* via passive side-effects of CLI commands (Activity-based model).
*/
import { parseArgs } from 'node:util';
import fs from 'node:fs/promises';
import path from 'node:path';
import { execSync } from 'node:child_process';
function log(obj) {
process.stdout.write(`${JSON.stringify(obj, null, 2)}
`);
}
function error(code, message) {
log({ ok: false, error: { code, message } });
process.exit(1);
}
async function getUncommittedChanges(projectRoot) {
try {
const out = execSync('git status --porcelain', { cwd: projectRoot, encoding: 'utf8' });
return out.split('\n')
.filter(Boolean)
.map(line => line.slice(3).trim())
.filter(p => !p.startsWith('.beadboard') && !p.startsWith('.beads'));
} catch {
return [];
}
}
async function resolveBbPath() {
const envRepo = process.env.BB_REPO;
if (envRepo) {
const p = path.join(envRepo, 'bb.ps1');
try {
await fs.access(p);
return p;
} catch {}
}
const local = path.join(process.cwd(), 'bb.ps1');
try {
await fs.access(local);
return local;
} catch {}
const tsEntry = path.join(process.cwd(), 'tools', 'bb.ts');
try {
await fs.access(tsEntry);
return `npx tsx ${tsEntry}`;
} catch {}
return null;
}
async function main() {
const { values } = parseArgs({
options: {
'non-interactive': { type: 'boolean' },
adopt: { type: 'string' },
register: { type: 'string' },
role: { type: 'string' },
json: { type: 'boolean' }
}
});
const isNonInteractive = values['non-interactive'];
const projectRoot = process.cwd();
const bbPath = await resolveBbPath();
if (!bbPath) {
error('BB_NOT_FOUND', 'Could not resolve bb.ps1 or tools/bb.ts');
}
let agentId = values.adopt || values.register;
let mode = values.adopt ? 'adopt' : (values.register ? 'register' : 'auto');
if (mode === 'auto' && isNonInteractive) {
error('AMBIGUOUS_SESSION', 'In non-interactive mode, --adopt or --register is required.');
}
if (mode === 'adopt') {
const changes = await getUncommittedChanges(projectRoot);
if (changes.length === 0 && isNonInteractive) {
error('ADOPTION_REJECTED', 'No evidence (uncommitted changes) to support identity adoption.');
}
}
try {
const bbExec = bbPath.includes('npx tsx') ? bbPath : `powershell.exe -NoProfile -Command "& '${bbPath}'"`;
if (mode === 'register') {
const role = values.role || 'agent';
execSync(`${bbExec} agent register --name ${agentId} --role ${role} --json`, { stdio: 'ignore' });
} else {
// Start/Extend the lease to show we are now active
execSync(`${bbExec} agent activity-lease --agent ${agentId} --json`, { stdio: 'ignore' });
}
log({
ok: true,
agent_id: agentId,
mode,
lease: { status: 'active', note: 'Activity lease started. Liveness maintained via real work.' },
timestamp: new Date().toISOString()
});
} catch (err) {
error('INIT_FAILED', err.message);
}
}
main();