From b26e4da118dbdaeb99d9ce0873d23c72ff449ddd Mon Sep 17 00:00:00 2001 From: Operator & Codex Date: Sun, 24 May 2026 20:58:37 +0200 Subject: [PATCH] Add Colibri runtime version inventory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a structured runtime inventory schema, drift summary tests, and skills for Pi provider smoke tests plus Node/Pi/npm version synchronization across hosts and ISO build inputs. --- Build: pass | Tests: pass — 2485 passed (186 files) --- .agent/skills/pi-provider-smoke/SKILL.md | 112 +++++++++++ .agent/skills/runtime-version-sync/SKILL.md | 206 ++++++++++++++++++++ agent/library.yaml | 10 + doc/COLIBRI-PI-CONTROL-PLAN.md | 31 +++ src/colibri-runtime-inventory.test.ts | 104 ++++++++++ src/colibri-runtime-inventory.ts | 121 ++++++++++++ 6 files changed, 584 insertions(+) create mode 100644 .agent/skills/pi-provider-smoke/SKILL.md create mode 100644 .agent/skills/runtime-version-sync/SKILL.md create mode 100644 src/colibri-runtime-inventory.test.ts create mode 100644 src/colibri-runtime-inventory.ts diff --git a/.agent/skills/pi-provider-smoke/SKILL.md b/.agent/skills/pi-provider-smoke/SKILL.md new file mode 100644 index 0000000..4d715d3 --- /dev/null +++ b/.agent/skills/pi-provider-smoke/SKILL.md @@ -0,0 +1,112 @@ +--- +name: pi-provider-smoke +description: Smoke-test a Pi provider lane using structured JSON output. Use for DeepSeek, ZAI, or other Pi built-in providers before wiring them into Colibri. +--- + +# Pi Provider Smoke + +Validate that a provider works through `pi` and emits structured events that +Colibri can consume. + +## Rules + +- Never paste API keys into chat, logs, commits, or command output. +- Prefer Pi's credential store (`~/.pi/agent/auth.json`) or environment variables. +- Do not hardcode model IDs before `pi --provider --list-models` succeeds. +- Use `--mode json` for the final smoke test. +- One command at a time; inspect output before continuing. + +## 1. Identify Pi + +```bash +which pi +``` + +```bash +pi --version +``` + +```bash +node --version +``` + +Record host OS and install path. + +## 2. Confirm provider models + +For DeepSeek: + +```bash +pi --provider deepseek --list-models +``` + +For ZAI: + +```bash +pi --provider zai --list-models +``` + +If the command fails because credentials are missing, stop and ask the operator +to add the key locally. + +## 3. Credential layout + +Pi persistent credentials live at: + +```text +~/.pi/agent/auth.json +``` + +DeepSeek shape: + +```json +{ + "deepseek": { "type": "api_key", "key": "" } +} +``` + +ZAI uses the same shape with provider key `zai`. + +Important: + +- field is `key`, not `apiKey` +- type is exactly `api_key` +- `auth.json` wins over environment variables when both are set +- never commit `auth.json` + +## 4. JSON-mode smoke + +Use a tiny deterministic prompt: + +```bash +pi --provider deepseek -p --mode json "reply with the single word: ok" +``` + +Expected: + +- exit code 0 +- output is JSONL +- stream contains Pi lifecycle events such as `session`, `agent_start`, + `message_update`, `turn_end`, `agent_end` +- assistant text contains `ok` + +## 5. Colibri parser check + +If working inside `clawdie-ai`, save a short JSONL sample under repo `tmp/` and +parse it with the Colibri tests/helpers. Do not store secrets in the sample. + +```bash +npx vitest run src/colibri-pi-events.test.ts +``` + +## 6. Report + +Report: + +- OS and host +- Pi version +- Node version +- provider name +- exact model IDs listed, if relevant +- JSON-mode smoke pass/fail +- whether Colibri parser accepted a representative stream diff --git a/.agent/skills/runtime-version-sync/SKILL.md b/.agent/skills/runtime-version-sync/SKILL.md new file mode 100644 index 0000000..81f2c6f --- /dev/null +++ b/.agent/skills/runtime-version-sync/SKILL.md @@ -0,0 +1,206 @@ +--- +name: runtime-version-sync +description: Check and align Node, FreeBSD pkg, npm global, and Pi versions across Linux and FreeBSD hosts. Use for preventing runtime drift between OSA, debby, domedog, and ISO builds. +--- + +# Runtime Version Sync + +Keep Linux, FreeBSD, and ISO runtime inputs aligned without relying on moving +`latest` tags during builds. + +## Rules + +- Do not upgrade anything until the inventory is recorded. +- Fetch remotes before claiming remote repository state. +- For security-sensitive auth paths, inspect before mutating. +- Pin build inputs; do not let ISO builds depend on moving npm dist-tags. +- Prefer Node 24 across Linux and FreeBSD unless a target explicitly requires a + legacy lane. +- Use repo-local `tmp/` for generated inventories and manifests. + +## 1. Inventory local host + +```bash +uname -a +``` + +```bash +node --version +``` + +```bash +npm --version +``` + +```bash +which pi +``` + +```bash +pi --version +``` + +```bash +npm config get prefix +``` + +```bash +npm outdated -g --depth=0 +``` + +On FreeBSD, also check packages: + +```bash +pkg info -x 'node|npm|pi|codex|gemini' +``` + +On Linux with nvm: + +```bash +command -v nvm +``` + +```bash +nvm current +``` + +## 2. Check upstream versions + +Pi: + +```bash +npm view @earendil-works/pi-coding-agent version +``` + +```bash +npm view @earendil-works/pi-coding-agent dist-tags --json +``` + +Gemini CLI, if still intentionally shipped: + +```bash +npm view @google/gemini-cli version +``` + +FreeBSD package availability: + +```bash +pkg search '^node[0-9]+' +``` + +```bash +pkg search '^npm' +``` + +## 3. Cross-host manifest + +Each host should emit a small JSON manifest under repo-local or user-local +state. Example schema: + +```json +{ + "schema": "clawdie.runtime-version-inventory.v1", + "host": "osa", + "os": "FreeBSD", + "node": "v24.14.1", + "npm": "11.x", + "pi": "0.75.5", + "npm_prefix": "/home/clawdie/.npm-global", + "package_manager": "pkg", + "notes": [] +} +``` + +Colibri can aggregate these manifests later, the same way it aggregates +interagent run manifests. + +## 4. Upgrade policy + +### FreeBSD + +Preferred Node package: + +```text +node24 +``` + +Use FreeBSD package operations only after checking current state and taking any +needed ZFS/package rollback precautions. + +Install/upgrade package names: + +```bash +sudo pkg install -y node24 npm-node24 +``` + +Then verify: + +```bash +node --version +``` + +```bash +npm --version +``` + +### Linux + +If Node is managed by nvm, use nvm for Node 24: + +```bash +nvm install 24 +``` + +```bash +nvm alias default 24 +``` + +If Node is system-managed, do not assume the package manager. Inventory first +and choose the host's standard channel. + +## 5. npm global policy + +Pi should be updated through Pi when possible: + +```bash +pi update --self +``` + +Fallback: + +```bash +npm install -g @earendil-works/pi-coding-agent@ +``` + +ISO builds must use pinned npm globals from the ISO repo, not `latest`: + +```text +/home/clawdie/clawdie-iso/packages/npm-globals.txt +``` + +## 6. ISO follow-up + +When Pi or npm global versions change, update the ISO pin file in a separate +ISO commit and smoke the pack step: + +```bash +sh -n scripts/fetch-npm-globals.sh +``` + +```bash +OUT_DIR="$(pwd)/tmp/npm-globals-pin-smoke" ./scripts/fetch-npm-globals.sh +``` + +Do not run a full ISO build unless explicitly assigned. + +## 7. Report + +Report: + +- hosts checked +- Node versions and desired target +- Pi versions and desired target +- npm global pins changed +- package manager actions proposed or performed +- whether ISO pin file is aligned +- any blockers for Node 24 unification diff --git a/agent/library.yaml b/agent/library.yaml index 22398df..91b9684 100644 --- a/agent/library.yaml +++ b/agent/library.yaml @@ -167,6 +167,16 @@ skills: description: Update the pi coding-agent harness, including package rename migration and pi packages/extensions tags: [agent, ai-provider, update] + - id: pi-provider-smoke + source: local:.agent/skills/pi-provider-smoke/SKILL.md + description: Smoke-test a Pi provider lane using structured JSON output for Colibri validation + tags: [agent, ai-provider, colibri, smoke] + + - id: runtime-version-sync + source: local:.agent/skills/runtime-version-sync/SKILL.md + description: Check and align Node, FreeBSD pkg, npm global, and Pi versions across Linux, FreeBSD, and ISO builds + tags: [agent, infra, update, colibri, node, npm] + - id: agent-setup source: local:.agent/skills/agent-setup/SKILL.md description: Set up the Clawdie runtime on FreeBSD — host orchestrator plus all service jails diff --git a/doc/COLIBRI-PI-CONTROL-PLAN.md b/doc/COLIBRI-PI-CONTROL-PLAN.md index 6812b8a..cb35792 100644 --- a/doc/COLIBRI-PI-CONTROL-PLAN.md +++ b/doc/COLIBRI-PI-CONTROL-PLAN.md @@ -107,6 +107,37 @@ No legacy runner/status code should be removed until these are true: 7. The network throughput coordination test has produced structured manifests from at least two hosts. +## Runtime Drift And Version Sync + +Colibri should also become the coordination layer for boring but important +runtime hygiene: Node, npm globals, Pi provider lanes, and ISO build inputs. + +Current target: + +```text +Node: 24.x on Linux and FreeBSD +Pi: pinned through each host inventory, not assumed from PATH +ISO npm globals: pinned in the ISO repo, not fetched from moving latest tags +``` + +Principles: + +- each host emits a small version inventory manifest +- the coordinator compares manifests before upgrades +- FreeBSD package actions stay locally authorized and rollback-aware +- Linux Node management respects the host manager (`nvm`, system package, etc.) +- ISO build inputs are pinned separately so live USB rebuilds are reproducible + +The first skills for this are: + +- `pi-provider-smoke` — validates DeepSeek/ZAI/etc through Pi `--mode json` +- `runtime-version-sync` — inventories and aligns Node, Pi, npm globals, and ISO + pins across hosts + +This is a natural cross-agent communication use case: OSA, debby, and domedog +can each produce a manifest; Colibri aggregates the manifests and proposes the +minimal safe changes. + ## Branch Plan Implementation branch: diff --git a/src/colibri-runtime-inventory.test.ts b/src/colibri-runtime-inventory.test.ts new file mode 100644 index 0000000..4d2a318 --- /dev/null +++ b/src/colibri-runtime-inventory.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; + +import { + COLIBRI_RUNTIME_INVENTORY_SCHEMA, + buildRuntimeDriftReport, + parseColibriRuntimeInventory, + parseColibriRuntimeInventoryJson, + summarizeRuntimeDriftReport, +} from './colibri-runtime-inventory.js'; + +const PI_PACKAGE = '@earendil-works/pi-coding-agent'; + +const OSA_INVENTORY = { + schema: COLIBRI_RUNTIME_INVENTORY_SCHEMA, + host: 'osa', + os: 'FreeBSD', + node: 'v24.14.1', + npm: '11.6.2', + pi: '0.75.5', + npm_prefix: '/home/clawdie/.npm-global', + package_manager: 'pkg', + iso_npm_globals_pin: { + [PI_PACKAGE]: '0.75.5', + }, + notes: [], +}; + +describe('colibri runtime inventory', () => { + it('parses a runtime version inventory manifest', () => { + const result = parseColibriRuntimeInventory(OSA_INVENTORY); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.inventory.host).toBe('osa'); + expect(result.inventory.node).toBe('v24.14.1'); + expect(result.inventory.iso_npm_globals_pin[PI_PACKAGE]).toBe('0.75.5'); + }); + + it('applies defaults for optional map and notes fields', () => { + const result = parseColibriRuntimeInventory({ + schema: COLIBRI_RUNTIME_INVENTORY_SCHEMA, + host: 'debby', + os: 'Linux', + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.inventory.iso_npm_globals_pin).toEqual({}); + expect(result.inventory.notes).toEqual([]); + }); + + it('reports invalid JSON text', () => { + const result = parseColibriRuntimeInventoryJson('{bad'); + + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.errors[0]).toContain('invalid JSON'); + }); + + it('detects Node, Pi, and ISO pin drift across hosts', () => { + const report = buildRuntimeDriftReport( + [ + OSA_INVENTORY, + { + schema: COLIBRI_RUNTIME_INVENTORY_SCHEMA, + host: 'debby', + os: 'Linux', + node: 'v20.19.0', + pi: '0.75.5', + iso_npm_globals_pin: {}, + notes: [], + }, + { + schema: COLIBRI_RUNTIME_INVENTORY_SCHEMA, + host: 'oldfreebsd', + os: 'FreeBSD', + node: 'v24.10.0', + pi: '0.74.0', + iso_npm_globals_pin: { + [PI_PACKAGE]: '0.74.0', + }, + notes: [], + }, + ], + { targetNodeMajor: 24, targetPiVersion: '0.75.5' }, + ); + + expect(report.nodeDriftHosts).toEqual(['debby']); + expect(report.piDriftHosts).toEqual(['oldfreebsd']); + expect(report.isoPinDrift).toEqual(['oldfreebsd']); + }); + + it('renders a compact drift report summary', () => { + const report = buildRuntimeDriftReport([OSA_INVENTORY], { + targetPiVersion: '0.75.5', + }); + + const summary = summarizeRuntimeDriftReport(report); + expect(summary).toContain(''); + expect(summary).toContain('target_node_major=24'); + expect(summary).toContain('target_pi_version=0.75.5'); + expect(summary).toContain('node_drift_hosts=none'); + }); +}); diff --git a/src/colibri-runtime-inventory.ts b/src/colibri-runtime-inventory.ts new file mode 100644 index 0000000..65e1f3b --- /dev/null +++ b/src/colibri-runtime-inventory.ts @@ -0,0 +1,121 @@ +import { z } from 'zod'; + +export const COLIBRI_RUNTIME_INVENTORY_SCHEMA = + 'clawdie.runtime-version-inventory.v1' as const; + +export const ColibriRuntimeInventorySchema = z.object({ + schema: z.literal(COLIBRI_RUNTIME_INVENTORY_SCHEMA), + host: z.string().min(1), + os: z.string().min(1), + node: z.string().min(1).nullable().optional(), + npm: z.string().min(1).nullable().optional(), + pi: z.string().min(1).nullable().optional(), + npm_prefix: z.string().min(1).nullable().optional(), + package_manager: z.string().min(1).nullable().optional(), + iso_npm_globals_pin: z.record(z.string(), z.string()).default({}), + notes: z.array(z.string()).default([]), +}); + +export type ColibriRuntimeInventory = z.infer< + typeof ColibriRuntimeInventorySchema +>; + +export type ColibriRuntimeInventoryParseResult = + | { ok: true; inventory: ColibriRuntimeInventory } + | { ok: false; errors: string[] }; + +export interface ColibriRuntimeDriftReport { + targetNodeMajor: number; + targetPiVersion?: string; + hosts: string[]; + nodeDriftHosts: string[]; + piDriftHosts: string[]; + missingPiHosts: string[]; + isoPinDrift: string[]; +} + +export function parseColibriRuntimeInventory( + input: unknown, +): ColibriRuntimeInventoryParseResult { + const parsed = ColibriRuntimeInventorySchema.safeParse(input); + if (parsed.success) return { ok: true, inventory: parsed.data }; + + return { + ok: false, + errors: parsed.error.issues.map((issue) => { + const path = issue.path.length > 0 ? issue.path.join('.') : '(root)'; + return `${path}: ${issue.message}`; + }), + }; +} + +export function parseColibriRuntimeInventoryJson( + text: string, +): ColibriRuntimeInventoryParseResult { + let raw: unknown; + try { + raw = JSON.parse(text); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { ok: false, errors: [`invalid JSON: ${message}`] }; + } + return parseColibriRuntimeInventory(raw); +} + +function nodeMajor(version: string | null | undefined): number | null { + if (!version) return null; + const match = version.trim().match(/^v?(\d+)\./u); + if (!match?.[1]) return null; + return Number.parseInt(match[1], 10); +} + +export function buildRuntimeDriftReport( + inventories: ColibriRuntimeInventory[], + opts: { targetNodeMajor?: number; targetPiVersion?: string } = {}, +): ColibriRuntimeDriftReport { + const targetNodeMajor = opts.targetNodeMajor ?? 24; + const targetPiVersion = opts.targetPiVersion; + const piPackage = '@earendil-works/pi-coding-agent'; + + return { + targetNodeMajor, + targetPiVersion, + hosts: inventories.map((entry) => entry.host), + nodeDriftHosts: inventories + .filter((entry) => nodeMajor(entry.node) !== targetNodeMajor) + .map((entry) => entry.host), + piDriftHosts: targetPiVersion + ? inventories + .filter((entry) => entry.pi !== targetPiVersion) + .map((entry) => entry.host) + : [], + missingPiHosts: inventories + .filter((entry) => !entry.pi) + .map((entry) => entry.host), + isoPinDrift: targetPiVersion + ? inventories + .filter( + (entry) => + entry.iso_npm_globals_pin[piPackage] !== undefined && + entry.iso_npm_globals_pin[piPackage] !== targetPiVersion, + ) + .map((entry) => entry.host) + : [], + }; +} + +export function summarizeRuntimeDriftReport( + report: ColibriRuntimeDriftReport, +): string { + return [ + '', + `target_node_major=${report.targetNodeMajor}`, + `target_pi_version=${report.targetPiVersion ?? 'unspecified'}`, + `hosts=${report.hosts.join(',') || 'none'}`, + `node_drift_hosts=${report.nodeDriftHosts.join(',') || 'none'}`, + `pi_drift_hosts=${report.piDriftHosts.join(',') || 'none'}`, + `missing_pi_hosts=${report.missingPiHosts.join(',') || 'none'}`, + `iso_pin_drift=${report.isoPinDrift.join(',') || 'none'}`, + '', + ].join('\n'); +}