Add Colibri runtime version inventory

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)
This commit is contained in:
Operator & Codex 2026-05-24 20:58:37 +02:00
parent 4cf190deb8
commit b26e4da118
6 changed files with 584 additions and 0 deletions

View file

@ -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 <name> --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": "<YOUR_DEEPSEEK_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

View file

@ -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@<pinned-version>
```
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

View file

@ -167,6 +167,16 @@ skills:
description: Update the pi coding-agent harness, including package rename migration and pi packages/extensions description: Update the pi coding-agent harness, including package rename migration and pi packages/extensions
tags: [agent, ai-provider, update] 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 - id: agent-setup
source: local:.agent/skills/agent-setup/SKILL.md source: local:.agent/skills/agent-setup/SKILL.md
description: Set up the Clawdie runtime on FreeBSD — host orchestrator plus all service jails description: Set up the Clawdie runtime on FreeBSD — host orchestrator plus all service jails

View file

@ -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 7. The network throughput coordination test has produced structured manifests
from at least two hosts. 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 ## Branch Plan
Implementation branch: Implementation branch:

View file

@ -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('<colibri-runtime-drift>');
expect(summary).toContain('target_node_major=24');
expect(summary).toContain('target_pi_version=0.75.5');
expect(summary).toContain('node_drift_hosts=none');
});
});

View file

@ -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 [
'<colibri-runtime-drift>',
`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'}`,
'</colibri-runtime-drift>',
].join('\n');
}