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