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
66
skills/beadboard-driver/SKILL.md
Normal file
66
skills/beadboard-driver/SKILL.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
---
|
||||||
|
name: beadboard-driver
|
||||||
|
description: Drive BeadBoard agent workflows with strict preflight, per-session unique agent identity, and evidence-backed closeout. Use when handling bead lifecycle work that combines bd status commands with bb agent coordination (register/list/show, send/inbox/read/ack, reserve/release/status), especially in multi-agent sessions where path resolution, collision avoidance, and verification discipline are required.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Beadboard Driver
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use this skill to run repeatable `bd` + `bb` workflows without drift. Resolve `bb` safely, generate a unique session identity, coordinate with reservations/mail, and produce closeout evidence before claiming completion.
|
||||||
|
|
||||||
|
## Core Workflow
|
||||||
|
|
||||||
|
1. Run preflight:
|
||||||
|
```bash
|
||||||
|
node skills/beadboard-driver/scripts/session-preflight.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Generate a unique per-session agent name:
|
||||||
|
```bash
|
||||||
|
node skills/beadboard-driver/scripts/generate-agent-name.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Register identity, then claim bead:
|
||||||
|
```bash
|
||||||
|
& "$env:BB_REPO\bb.ps1" agent register --name <agent-name> --role <role>
|
||||||
|
bd update <bead-id> --status in_progress --claim
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Coordinate during implementation:
|
||||||
|
```bash
|
||||||
|
& "$env:BB_REPO\bb.ps1" agent reserve --agent <agent-name> --scope "<path-glob>" --bead <bead-id>
|
||||||
|
& "$env:BB_REPO\bb.ps1" agent send --from <agent-name> --to <peer-agent> --bead <bead-id> --category HANDOFF --subject "<subject>" --body "<body>"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Build readiness summary before close:
|
||||||
|
```bash
|
||||||
|
node skills/beadboard-driver/scripts/readiness-report.mjs --checks '[{"name":"typecheck","ok":true}]' --artifacts '[{"path":"artifacts/final.png","required":true}]'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Resolution Policy
|
||||||
|
|
||||||
|
- Treat `BB_REPO` as authoritative when set.
|
||||||
|
- On invalid `BB_REPO`, stop and return remediation text. Do not silently bypass.
|
||||||
|
- If `BB_REPO` is unset, resolve from global `bb`, then cached path, then bounded discovery.
|
||||||
|
- Update the skill cache only after a verified path is found.
|
||||||
|
- Never mutate shell profile/env vars automatically.
|
||||||
|
|
||||||
|
## Identity Policy
|
||||||
|
|
||||||
|
- Create one unique agent identity per session.
|
||||||
|
- Use adjective-noun names and retry on collisions.
|
||||||
|
- Register identity before any mail/reservation command.
|
||||||
|
- Keep bead claim authority in `bd`; identity alone is not a claim.
|
||||||
|
|
||||||
|
## Verification Policy
|
||||||
|
|
||||||
|
- Do not claim completion without fresh command evidence.
|
||||||
|
- Require typecheck, test, and lint evidence for closeout tasks.
|
||||||
|
- Use readiness report output in bead notes.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Command and argument contracts: `references/command-matrix.md`
|
||||||
|
- Failure and recovery handling: `references/failure-modes.md`
|
||||||
|
- End-to-end session choreography: `references/session-lifecycle.md`
|
||||||
4
skills/beadboard-driver/agents/openai.yaml
Normal file
4
skills/beadboard-driver/agents/openai.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
interface:
|
||||||
|
display_name: "Beadboard Driver"
|
||||||
|
short_description: "Safe bd+bb agent workflow orchestration"
|
||||||
|
default_prompt: "Use Beadboard Driver to resolve bb path, register a unique session agent, coordinate via bb agent commands, and produce verification-backed closeout notes."
|
||||||
33
skills/beadboard-driver/references/command-matrix.md
Normal file
33
skills/beadboard-driver/references/command-matrix.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Command Matrix
|
||||||
|
|
||||||
|
## Preflight and Identity
|
||||||
|
|
||||||
|
- `node skills/beadboard-driver/scripts/resolve-bb.mjs`
|
||||||
|
- Output: `{ ok, source, resolved_path, reason, remediation }`
|
||||||
|
- `node skills/beadboard-driver/scripts/session-preflight.mjs`
|
||||||
|
- Output: `{ ok, tools.bd, bb }` or `{ ok:false, error_code }`
|
||||||
|
- `node skills/beadboard-driver/scripts/generate-agent-name.mjs`
|
||||||
|
- Output: `{ ok, agent_name, attempts, collisions }` or `{ ok:false, error_code }`
|
||||||
|
- `node skills/beadboard-driver/scripts/readiness-report.mjs --checks <json> --artifacts <json>`
|
||||||
|
- Output: `{ ok, checks, artifacts, dependency_sanity, summary }`
|
||||||
|
|
||||||
|
## Coordination Commands (`bb`)
|
||||||
|
|
||||||
|
- `bb agent register --name <agent> --role <role>`
|
||||||
|
- `bb agent list`
|
||||||
|
- `bb agent show --agent <agent>`
|
||||||
|
- `bb agent send --from <agent> --to <agent> --bead <id> --category <HANDOFF|BLOCKED|DECISION|INFO> --subject <text> --body <text>`
|
||||||
|
- `bb agent inbox --agent <agent> [--state unread|read|acked]`
|
||||||
|
- `bb agent read --agent <agent> --message <message-id>`
|
||||||
|
- `bb agent ack --agent <agent> --message <message-id>`
|
||||||
|
- `bb agent reserve --agent <agent> --scope <path> --bead <id> [--ttl <minutes>] [--takeover-stale]`
|
||||||
|
- `bb agent release --agent <agent> --scope <path>`
|
||||||
|
- `bb agent status [--bead <id>] [--agent <agent>]`
|
||||||
|
|
||||||
|
## Lifecycle Commands (`bd`)
|
||||||
|
|
||||||
|
- `bd ready`
|
||||||
|
- `bd show <bead-id>`
|
||||||
|
- `bd update <bead-id> --status in_progress --claim`
|
||||||
|
- `bd update <bead-id> --notes "<evidence>"`
|
||||||
|
- `bd close <bead-id> --reason "<summary>"`
|
||||||
40
skills/beadboard-driver/references/failure-modes.md
Normal file
40
skills/beadboard-driver/references/failure-modes.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Failure Modes
|
||||||
|
|
||||||
|
## `BD_NOT_FOUND`
|
||||||
|
|
||||||
|
- Cause: `bd` missing from PATH.
|
||||||
|
- Recovery: install beads CLI or add `bd` executable directory to PATH.
|
||||||
|
|
||||||
|
## `BB_NOT_FOUND`
|
||||||
|
|
||||||
|
- Cause: `BB_REPO` invalid or no `bb` command / cache / discovery hit.
|
||||||
|
- Recovery:
|
||||||
|
- Set `BB_REPO` to BeadBoard repo root.
|
||||||
|
- Verify `bb.ps1` exists under `BB_REPO`.
|
||||||
|
- Retry preflight.
|
||||||
|
|
||||||
|
## `NAME_GENERATION_EXHAUSTED`
|
||||||
|
|
||||||
|
- Cause: all generated names collided with existing registry entries.
|
||||||
|
- Recovery:
|
||||||
|
- increase retry count (`BB_NAME_MAX_RETRIES`),
|
||||||
|
- expand adjective/noun pools,
|
||||||
|
- retry generation.
|
||||||
|
|
||||||
|
## Reservation Conflicts
|
||||||
|
|
||||||
|
- `RESERVATION_CONFLICT`: active owner exists.
|
||||||
|
- `RESERVATION_STALE_FOUND`: stale reservation exists; use takeover only when safe.
|
||||||
|
- `RELEASE_FORBIDDEN`: non-owner attempted release.
|
||||||
|
|
||||||
|
## Mail Lifecycle Errors
|
||||||
|
|
||||||
|
- `UNKNOWN_SENDER` / `UNKNOWN_RECIPIENT`: register agents before send.
|
||||||
|
- `ACK_FORBIDDEN`: only recipient may ack.
|
||||||
|
- `MESSAGE_NOT_FOUND`: stale id or wrong message reference.
|
||||||
|
|
||||||
|
## Policy Guardrails
|
||||||
|
|
||||||
|
- Do not write `.beads/issues.jsonl` directly.
|
||||||
|
- Do not close beads without verification evidence.
|
||||||
|
- Do not bypass `BB_REPO` when it is set but invalid; fix it explicitly.
|
||||||
33
skills/beadboard-driver/references/session-lifecycle.md
Normal file
33
skills/beadboard-driver/references/session-lifecycle.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Session Lifecycle
|
||||||
|
|
||||||
|
## 1) Start Session
|
||||||
|
|
||||||
|
1. Run preflight.
|
||||||
|
2. Resolve bb path and confirm `bd` availability.
|
||||||
|
3. Generate unique session agent name.
|
||||||
|
4. Register agent identity.
|
||||||
|
|
||||||
|
## 2) Pick and Claim Work
|
||||||
|
|
||||||
|
1. `bd ready`
|
||||||
|
2. `bd show <id>`
|
||||||
|
3. `bd update <id> --status in_progress --claim`
|
||||||
|
|
||||||
|
## 3) Coordinate During Work
|
||||||
|
|
||||||
|
1. Reserve sensitive scopes before edits.
|
||||||
|
2. Send structured mail for blockers and handoffs.
|
||||||
|
3. Read and acknowledge required messages.
|
||||||
|
|
||||||
|
## 4) Verify and Close
|
||||||
|
|
||||||
|
1. Run required gates (typecheck/test/lint).
|
||||||
|
2. Build readiness report with checks + artifacts.
|
||||||
|
3. Post notes to bead.
|
||||||
|
4. Close bead with explicit reason.
|
||||||
|
|
||||||
|
## 5) Session End Hygiene
|
||||||
|
|
||||||
|
1. Release reservations.
|
||||||
|
2. Ensure no unresolved blocker mail is pending for your bead.
|
||||||
|
3. Hand off context if stopping before close.
|
||||||
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();
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'generate-agent-name.mjs');
|
||||||
|
|
||||||
|
test('generate-agent-name contract: returns structured success', async () => {
|
||||||
|
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BB_NAME_ADJECTIVES: 'green',
|
||||||
|
BB_NAME_NOUNS: 'castle',
|
||||||
|
BB_NAME_MAX_RETRIES: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = JSON.parse(stdout);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.agent_name, 'green-castle');
|
||||||
|
assert.equal(typeof result.attempts, 'number');
|
||||||
|
});
|
||||||
32
skills/beadboard-driver/tests/resolve-bb.contract.test.mjs
Normal file
32
skills/beadboard-driver/tests/resolve-bb.contract.test.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'resolve-bb.mjs');
|
||||||
|
|
||||||
|
test('resolve-bb contract: BB_REPO source', async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-skill-contract-resolve-'));
|
||||||
|
try {
|
||||||
|
const repo = path.join(root, 'beadboard');
|
||||||
|
await fs.mkdir(path.join(repo, 'tools'), { recursive: true });
|
||||||
|
await fs.writeFile(path.join(repo, 'bb.ps1'), 'echo ok', 'utf8');
|
||||||
|
|
||||||
|
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
||||||
|
env: { ...process.env, BB_REPO: repo, BB_SKILL_HOME: path.join(root, 'home'), PATH: '' },
|
||||||
|
});
|
||||||
|
const result = JSON.parse(stdout);
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.source, 'env');
|
||||||
|
} finally {
|
||||||
|
await fs.rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
23
skills/beadboard-driver/tests/run-tests.mjs
Normal file
23
skills/beadboard-driver/tests/run-tests.mjs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
path.join(__dirname, 'resolve-bb.contract.test.mjs'),
|
||||||
|
path.join(__dirname, 'generate-agent-name.contract.test.mjs'),
|
||||||
|
path.join(__dirname, 'session-preflight.contract.test.mjs'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const child = spawn(process.execPath, ['--test', ...tests], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code) => {
|
||||||
|
process.exit(code ?? 1);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'session-preflight.mjs');
|
||||||
|
|
||||||
|
test('session-preflight contract: surfaces BD_NOT_FOUND when missing', async () => {
|
||||||
|
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
||||||
|
env: { ...process.env, PATH: '' },
|
||||||
|
});
|
||||||
|
const result = JSON.parse(stdout);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.error_code, 'BD_NOT_FOUND');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('session-preflight contract: succeeds with bd + BB_REPO', async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-skill-contract-preflight-'));
|
||||||
|
try {
|
||||||
|
const repo = path.join(root, 'beadboard');
|
||||||
|
const toolsDir = path.join(root, 'tools');
|
||||||
|
await fs.mkdir(path.join(repo, 'tools'), { recursive: true });
|
||||||
|
await fs.mkdir(toolsDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(repo, 'bb.ps1'), 'echo ok', 'utf8');
|
||||||
|
await fs.writeFile(path.join(toolsDir, 'bd.cmd'), '@echo off\r\necho beads\r\n', 'utf8');
|
||||||
|
|
||||||
|
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
||||||
|
env: { ...process.env, PATH: toolsDir, BB_REPO: repo, BB_SKILL_HOME: path.join(root, 'home') },
|
||||||
|
});
|
||||||
|
const result = JSON.parse(stdout);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.bb.ok, true);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue