feat(skills): formalize agent coordination via beadboard-driver
We moved from ad-hoc task claims to a strictly defined 'Skill' system. Triumphs: - Implemented the 'beadboard-driver' skill, which encodes our project-specific coordination protocols (claim, reservation, handoff). - This ensures that any AI operative (or human supervisor) can participate in the project lifecycle using a unified CLI-driven state machine. - Decoupled high-level mission logic from low-level file mutations, allowing for easier agent skill composition in the future. Raw Honest Moment: Initially, we were just 'winging it' with manual status updates. Formalizing this into a skill was a necessary step to ensure our collaboration is repeatable and resilient to agent context swaps.
This commit is contained in:
parent
c7c3a25457
commit
1ae7efb31b
14 changed files with 848 additions and 0 deletions
142
skills/beadboard-driver/scripts/generate-agent-name.mjs
Normal file
142
skills/beadboard-driver/scripts/generate-agent-name.mjs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
|
||||
function normalizeList(raw, fallback) {
|
||||
const value = (raw || '').trim();
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
return value
|
||||
.split(',')
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function sanitizeName(value) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
function buildRandomSource() {
|
||||
const sequenceRaw = (process.env.BB_NAME_SEED_SEQUENCE || '').trim();
|
||||
if (!sequenceRaw) {
|
||||
return () => Math.random();
|
||||
}
|
||||
const sequence = sequenceRaw
|
||||
.split(',')
|
||||
.map((value) => Number.parseFloat(value.trim()))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
let index = 0;
|
||||
return () => {
|
||||
if (sequence.length === 0) {
|
||||
return Math.random();
|
||||
}
|
||||
const value = sequence[index % sequence.length];
|
||||
index += 1;
|
||||
return Math.min(Math.max(value, 0), 0.999999);
|
||||
};
|
||||
}
|
||||
|
||||
function pickIndex(length, randomFn) {
|
||||
if (length <= 1) {
|
||||
return 0;
|
||||
}
|
||||
return Math.floor(randomFn() * length);
|
||||
}
|
||||
|
||||
async function nameExists(registryDir, agentName) {
|
||||
const filePath = path.join(registryDir, `${agentName}.json`);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function registryRoot() {
|
||||
if (process.env.BB_AGENT_REGISTRY_DIR) {
|
||||
return process.env.BB_AGENT_REGISTRY_DIR;
|
||||
}
|
||||
return path.join(process.env.USERPROFILE || os.homedir(), '.beadboard', 'agent', 'agents');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const adjectives = normalizeList(process.env.BB_NAME_ADJECTIVES, [
|
||||
'green',
|
||||
'silver',
|
||||
'swift',
|
||||
'steady',
|
||||
]);
|
||||
const nouns = normalizeList(process.env.BB_NAME_NOUNS, ['castle', 'harbor', 'falcon', 'orchard']);
|
||||
const maxRetriesRaw = Number.parseInt(process.env.BB_NAME_MAX_RETRIES || '12', 10);
|
||||
const maxRetries = Number.isInteger(maxRetriesRaw) && maxRetriesRaw > 0 ? maxRetriesRaw : 12;
|
||||
const random = buildRandomSource();
|
||||
const registryDir = registryRoot();
|
||||
|
||||
let collisions = 0;
|
||||
let attempts = 0;
|
||||
for (let index = 0; index < maxRetries; index += 1) {
|
||||
attempts += 1;
|
||||
const adjective = adjectives[pickIndex(adjectives.length, random)];
|
||||
const noun = nouns[pickIndex(nouns.length, random)];
|
||||
const candidate = sanitizeName(`${adjective}-${noun}`);
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
const exists = await nameExists(registryDir, candidate);
|
||||
if (!exists) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
agent_name: candidate,
|
||||
attempts,
|
||||
collisions,
|
||||
registry_dir: registryDir,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
collisions += 1;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'NAME_GENERATION_EXHAUSTED',
|
||||
reason: 'Unable to generate a unique agent name in allotted retries.',
|
||||
attempts,
|
||||
collisions,
|
||||
registry_dir: registryDir,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} catch (error) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'NAME_GENERATION_INTERNAL_ERROR',
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
||||
185
skills/beadboard-driver/scripts/lib/driver-lib.mjs
Normal file
185
skills/beadboard-driver/scripts/lib/driver-lib.mjs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
|
||||
function homeRoot() {
|
||||
return process.env.BB_SKILL_HOME || os.homedir();
|
||||
}
|
||||
|
||||
function cacheFilePath() {
|
||||
return path.join(homeRoot(), '.beadboard', 'skill-config.json');
|
||||
}
|
||||
|
||||
async function pathExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function readCache() {
|
||||
const filePath = cacheFilePath();
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeCache(payload) {
|
||||
const filePath = cacheFilePath();
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
`${JSON.stringify({ ...payload, updated_at: new Date().toISOString() }, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
function splitPathVariable(value) {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(path.delimiter).map((entry) => entry.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
async function findCommandInPath(commandName) {
|
||||
const pathEntries = splitPathVariable(process.env.PATH || '');
|
||||
const candidateNames =
|
||||
process.platform === 'win32'
|
||||
? [`${commandName}.cmd`, `${commandName}.exe`, `${commandName}.ps1`, `${commandName}.bat`, commandName]
|
||||
: [commandName];
|
||||
|
||||
for (const entry of pathEntries) {
|
||||
for (const candidate of candidateNames) {
|
||||
const fullPath = path.join(entry, candidate);
|
||||
if (await pathExists(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function validateRepoPath(repoPath) {
|
||||
if (!repoPath || !(await pathExists(repoPath))) {
|
||||
return { ok: false, reason: 'BB_REPO does not exist.' };
|
||||
}
|
||||
|
||||
const bbPath = path.join(repoPath, 'bb.ps1');
|
||||
if (!(await pathExists(bbPath))) {
|
||||
return { ok: false, reason: 'BB_REPO is set, but bb.ps1 was not found at BB_REPO\\bb.ps1.' };
|
||||
}
|
||||
|
||||
return { ok: true, bbPath };
|
||||
}
|
||||
|
||||
async function discoverBbPath() {
|
||||
const configuredRoots = splitPathVariable(process.env.BB_SEARCH_ROOTS || '');
|
||||
const roots = configuredRoots.length > 0 ? configuredRoots : [process.cwd(), path.join(homeRoot(), 'codex'), homeRoot()];
|
||||
const maxDepth = 4;
|
||||
|
||||
for (const root of roots) {
|
||||
if (!(await pathExists(root))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const queue = [{ dir: root, depth: 0 }];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
const candidate = path.join(current.dir, 'bb.ps1');
|
||||
if (await pathExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
if (current.depth >= maxDepth) {
|
||||
continue;
|
||||
}
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fs.readdir(current.dir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
queue.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveBbPath() {
|
||||
const cache = await readCache();
|
||||
const envRepo = (process.env.BB_REPO || '').trim();
|
||||
|
||||
if (envRepo) {
|
||||
const validated = await validateRepoPath(envRepo);
|
||||
if (!validated.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
source: 'env',
|
||||
resolved_path: null,
|
||||
reason: validated.reason,
|
||||
remediation: 'Set BB_REPO to your BeadBoard repo root, e.g. `$env:BB_REPO="C:\\path\\to\\beadboard"`.',
|
||||
};
|
||||
}
|
||||
|
||||
let reason = 'Resolved from BB_REPO.';
|
||||
if (cache.bb_path && cache.bb_path !== validated.bbPath) {
|
||||
reason = 'Resolved from BB_REPO; cache mismatch detected and cache updated.';
|
||||
}
|
||||
await writeCache({ bb_path: validated.bbPath, source: 'env' });
|
||||
return { ok: true, source: 'env', resolved_path: validated.bbPath, reason, remediation: null };
|
||||
}
|
||||
|
||||
const globalBb = await findCommandInPath('bb');
|
||||
if (globalBb) {
|
||||
await writeCache({ bb_path: globalBb, source: 'global' });
|
||||
return {
|
||||
ok: true,
|
||||
source: 'global',
|
||||
resolved_path: globalBb,
|
||||
reason: 'Resolved from PATH.',
|
||||
remediation: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (cache.bb_path && (await pathExists(cache.bb_path))) {
|
||||
return {
|
||||
ok: true,
|
||||
source: 'cache',
|
||||
resolved_path: cache.bb_path,
|
||||
reason: 'Resolved from cached bb path.',
|
||||
remediation: null,
|
||||
};
|
||||
}
|
||||
|
||||
const discovered = await discoverBbPath();
|
||||
if (discovered) {
|
||||
await writeCache({ bb_path: discovered, source: 'discovery' });
|
||||
return {
|
||||
ok: true,
|
||||
source: 'discovery',
|
||||
resolved_path: discovered,
|
||||
reason: 'Resolved by filesystem discovery and cached.',
|
||||
remediation: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
source: 'none',
|
||||
resolved_path: null,
|
||||
reason: 'Unable to find bb command or bb.ps1.',
|
||||
remediation:
|
||||
'Set BB_REPO to your BeadBoard repo root, or install a global bb command, then retry.',
|
||||
};
|
||||
}
|
||||
|
||||
export { cacheFilePath, findCommandInPath, resolveBbPath };
|
||||
112
skills/beadboard-driver/scripts/readiness-report.mjs
Normal file
112
skills/beadboard-driver/scripts/readiness-report.mjs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const output = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const token = argv[index];
|
||||
if (!token.startsWith('--')) {
|
||||
continue;
|
||||
}
|
||||
const key = token.slice(2);
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
output[key] = 'true';
|
||||
continue;
|
||||
}
|
||||
output[key] = value;
|
||||
index += 1;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function parseJsonArray(raw, fallback) {
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function withArtifactExistence(artifacts) {
|
||||
const output = [];
|
||||
for (const artifact of artifacts) {
|
||||
const item = {
|
||||
path: artifact.path,
|
||||
required: Boolean(artifact.required),
|
||||
exists: false,
|
||||
};
|
||||
if (typeof artifact.path === 'string' && artifact.path.trim()) {
|
||||
try {
|
||||
await fs.access(artifact.path);
|
||||
item.exists = true;
|
||||
} catch {
|
||||
item.exists = false;
|
||||
}
|
||||
}
|
||||
output.push(item);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const checks = parseJsonArray(args.checks, []);
|
||||
const artifacts = parseJsonArray(args.artifacts, []);
|
||||
const dependencySanity = args['dependency-note'] || '';
|
||||
|
||||
const normalizedChecks = checks.map((check) => ({
|
||||
name: check.name || 'unnamed-check',
|
||||
ok: Boolean(check.ok),
|
||||
details: check.details || '',
|
||||
}));
|
||||
const normalizedArtifacts = await withArtifactExistence(artifacts);
|
||||
|
||||
const allChecksPass = normalizedChecks.every((check) => check.ok);
|
||||
const requiredArtifactsPresent = normalizedArtifacts.every((artifact) => !artifact.required || artifact.exists);
|
||||
const ready = allChecksPass && requiredArtifactsPresent;
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
generated_at: new Date().toISOString(),
|
||||
checks: normalizedChecks,
|
||||
artifacts: normalizedArtifacts,
|
||||
dependency_sanity: dependencySanity,
|
||||
summary: {
|
||||
checks_passed: allChecksPass,
|
||||
required_artifacts_present: requiredArtifactsPresent,
|
||||
ready,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} catch (error) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
summary: {
|
||||
checks_passed: false,
|
||||
required_artifacts_present: false,
|
||||
ready: false,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
||||
26
skills/beadboard-driver/scripts/resolve-bb.mjs
Normal file
26
skills/beadboard-driver/scripts/resolve-bb.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { resolveBbPath } from './lib/driver-lib.mjs';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const resolved = await resolveBbPath();
|
||||
process.stdout.write(`${JSON.stringify(resolved, null, 2)}\n`);
|
||||
} catch (error) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
source: 'internal',
|
||||
resolved_path: null,
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
remediation: 'Inspect resolve-bb.js runtime environment and retry.',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
||||
83
skills/beadboard-driver/scripts/session-preflight.mjs
Normal file
83
skills/beadboard-driver/scripts/session-preflight.mjs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { findCommandInPath, resolveBbPath } from './lib/driver-lib.mjs';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const bdPath = await findCommandInPath('bd');
|
||||
if (!bdPath) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'BD_NOT_FOUND',
|
||||
reason: 'Could not find bd in PATH.',
|
||||
remediation: 'Install beads CLI or add bd executable to PATH.',
|
||||
tools: {
|
||||
bd: { available: false, path: null },
|
||||
},
|
||||
bb: null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const bb = await resolveBbPath();
|
||||
if (!bb.ok) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'BB_NOT_FOUND',
|
||||
reason: bb.reason,
|
||||
remediation: bb.remediation,
|
||||
tools: {
|
||||
bd: { available: true, path: bdPath },
|
||||
},
|
||||
bb,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
tools: {
|
||||
bd: { available: true, path: bdPath },
|
||||
},
|
||||
bb,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} catch (error) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error_code: 'PREFLIGHT_INTERNAL_ERROR',
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
remediation: 'Inspect session-preflight.js and retry.',
|
||||
tools: {
|
||||
bd: { available: false, path: null },
|
||||
},
|
||||
bb: null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
||||
Loading…
Add table
Add a link
Reference in a new issue