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
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 };
|
||||
Loading…
Add table
Add a link
Reference in a new issue