Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ MCP config, managed instructions, and native skill projections are separate:
automatically; drifted and unmanaged trees are preserved unless exact scoped
`--force` is supplied. Whole-inventory force also requires `--all`.
- Native skill changes set `restartRequired`; RUDI does not claim host hot reload.
- Private native names and allowed hosts come from `$RUDI_HOME/native-skills.json`.
Package IDs and receipt filenames remain stable. Receipt schema 3 separates
package ID from native name; schema 2 remains readable. Never infer ownership
from a matching name, move old targets automatically, or overwrite private
variants during a rename. Keep naming policy in the shared native lifecycle,
not individual command adapters.

Discover installed stacks with `rudi list stacks --json` or inspect
`~/.rudi/cache/tool-index.json`. Rebuild with `rudi index --json`. Do not use or
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,48 @@ native sessions to load the new projection; RUDI does not claim hot reload.
states. `rudi agent hosts --json` counts only receipt-backed, digest-matching
trees as synchronized; unrelated skill directories do not qualify.

Private native names and host restrictions can be configured in
`$RUDI_HOME/native-skills.json` (default `~/.rudi/native-skills.json`):

```json
{
"schemaVersion": 1,
"skills": {
"skill:image-generator": { "name": "image-generate" },
"skill:codex-project-task-archiver": { "hosts": ["codex"] }
}
}
```

Package IDs, canonical directories, lockfiles, and receipt filenames stay stable:
install/update/check/remove still use `skill:image-generator`; the native folder,
frontmatter name, and exact self-invocations use `image-generate`. Omitted `name`
keeps the package name. Omitted `hosts` permits all supported hosts; an empty
array excludes all hosts. Reconciliation returns `excluded` on disallowed hosts
without creating or removing a tree, including with `--force`. Explicit package
removal still cleans unchanged receipt-owned trees on previously allowed hosts. This is private local policy and
does not rename public registry packages.

Policy objects reject unknown fields, unsupported versions, invalid names,
unknown/duplicate hosts, duplicate target names, and symlinked paths. A name
reserved by another policy entry or owned by another package cannot be taken
over, including with force. Invalid policy prevents native reconciliation.

Receipt schema 3 records the stable `skillId` separately from the native
`skillName`; schema 2 remains readable. During a rename, an existing old target
blocks reconciliation. After that target has been deliberately preserved or
relocated, sync adopts an exact rendered new tree and updates the same receipt
atomically. A customized new tree remains `unmanaged` and the prior receipt is
retained; normal sync never recreates the retired name. A missing new tree may
be created. Use `--dry-run` to inspect these decisions first. Removal follows
the receipt's recorded target and digest, preserving unowned or changed trees.
Changing policy does not automatically move or delete existing directories.

Older CLI versions cannot read schema 3 receipts. Keep receipt backups when
rolling out this feature; reverting the executable alone is not a complete
rollback. Keep the new CLI and private policy together on every participating
workstation before running native reconciliation there.

### Running Headless Agent Hosts

`rudi agent` is the supported headless execution surface. Foreground launches
Expand Down
250 changes: 170 additions & 80 deletions dist/index.cjs

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions docs/swe-compliance/2026-09-29-native-skill-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Private native skill naming

## Phase 0: Baseline and manual lookup

- Scope: durable native names and host restrictions, with stable package IDs.
- Baseline: main at 60c78b09c6f2c6204c4b3349eeb6fa2fb523300b. Preserve the existing npm runtime binding work, README additions, and generated bundle changes. Baseline patch retained in the calling task's work directory.
- Manual: index, Master Engineering Doctrine, Agent Co-Pilot Operating Standard, testing doctrine, horizontal stewardship standard.
- Risk: high, because ownership receipts are persistent state used by removal.
- Horizontal scan: install, related install, update, sync, check, and removal already share src/native-skills/lifecycle.js. Resolve the shared naming contract here; do not add parallel command implementations.

## Phase 1: Scope lock

- In scope: private ~/.rudi/native-skills.json, strict policy validation, rendered names and invocations, receipt compatibility, preservation and reporting of conflicts, command integration tests, documentation, build and verification on both Macs.
- Non-goals: public package renames, registry edits, changes to private skill bodies, unrelated runtime binding work, automatic deletion of old or customized folders.
- Interface: schemaVersion 1 with skills keyed by package ID; each entry can specify name and hosts. Missing policy preserves defaults. Receipt filenames remain keyed by stable package ID; schema 3 separates skillId from skillName and reads schema 2 safely.
- Trust boundaries: JSON configuration, symlinks, names, host names, native trees, receipts. Invalid or ambiguous ownership fails closed. Exact rendered trees may be adopted; differing custom trees remain unmanaged.
- Rollback: preserve prior receipts on failure; stage tree writes with existing guarded promotion and rollback. Existing old targets block renaming until deliberately reconciled.
- Commit slices: (1) policy and lifecycle behavior with tests; (2) docs and verification evidence; (3) generated build. Commits, publication, and installed CLI activation are not authorized by the execution skill alone.
- Review: fresh-context Standards/Spec/Proof review after tests; high-risk migration proof before live receipt changes.

## Phase 2: Red tests

- One behavior at a time: native alias, invalid policy, excluded host, old receipt migration, collision/drift preservation, metadata invocation, discovery/removal and command paths.
- Record red/green commands below as performed.

## Phase 3: Implementation

- Allowed paths: src/native-skills/, affected unit tests and necessary command adapters, README.md, AGENTS.md, this checklist, generated dist output.
- No new dependency; structured lifecycle results retain package ID, native name, state/action, error, and restart requirement.

## Phase 4: Green tests and refactor

- Rerun unchanged red command, then affected lifecycle and command tests. Preserve existing safety tests.

## Phase 5: Full verification

- Implementation verification completed: targeted and full suites, build, focused SWE debt scan, repository debt runner, package dry-run, isolated real-state rehearsal, independent review, and admin Mac source reconciliation. Clean release verification is recorded below; publication and live activation remain delivery steps.

## Phase 6: Docs, contracts, and closure

- Pending: evidence, exact delivery boundary, closeout receipt, final verdict. Goal remains active until the full approved outcome is verified or a remaining gate is explicitly reported.

## Implementation execution evidence

- Red/green: `pnpm test src/__tests__/unit/native-skill-policy.test.js` first failed on the old folder name, then passed after policy wiring. Subsequent unchanged tests failed then passed for excluded hosts, schema-2 adoption, mapped discovery/removal, bundled metadata invocation, cross-package ownership collision, and missing skill namespace.
- Regression: 95 focused lifecycle/command tests passed before additional boundary coverage. A separate child-process integration test exercises actual native adapters for direct install, related install, update, sync, check and removal with an isolated HOME; package fetching is injected, native lifecycle is real.
- Runtime provenance: Homebrew's default Node failed to launch (missing simdjson dylib). Node 20.10 runs focused tests but lacks import.meta.dirname needed by two existing router tests. Node 22.23.2 then exposed an existing native-addon ABI mismatch. Final verification uses checksum-verified Node 20.19.0 with `RUDI_CLI_TEST_NODE` set explicitly because the repository runner prefers bundled Node 20 over PATH.
- Focused debt: `swe_debt_scan` using `pr-review` profile reports 0 findings for the two implementation files and two new test files. The default daemon-only entrypoints reported false orphan warnings; the repository's CLI profile resolves them without code changes.
- Isolated installed-state rehearsal on primary Mac: 10 native trees copied, all retained byte/mode-identically, all retired folders remain absent after two syncs. Three exact Claude trees were adopted; seven customized variants remained unmanaged with prior receipts preserved. No live native files or receipts changed.
- Build and `npm pack --dry-run` passed; tracked bundle includes preserved pre-existing runtime binding work. No commits, publication, or installed executable activation performed.

## Verified implementation boundary

- Final full suite: **827 passed, 0 failed** on both primary arm64 and admin x64 Macs, using checksum-verified Node 20.19.0 in isolated diagnostic directories. This provides import.meta.dirname while retaining the native addon ABI; installed runtimes/dependencies were unchanged.
- Final focused debt: 0 errors, 0 warnings, 0 informational findings using the CLI `pr-review` profile. Repository changed-file debt runner passed on both Macs.
- Build and package dry-run passed on both Macs. Source and build checksum parity verified; no installed CLI, native naming policy, native tree, or receipt was activated/modified during this implementation phase.
- Independent review: initially revise for duplicate schema-3 receipts claiming one removal target. A regression first observed unsafe removal, then passed after ownership checks were added to removal and host summary. Focused confirmation: Standards pass, Spec pass, Proof pass, overall pass. Explicit host-exclusion removal behavior is documented and covered.
- Admin source reconciliation: exact seven-file patch applied only after baseline SHA-256 checks, preserving its existing dirty work. Both repos retain main at the baseline revision; nothing staged, committed, pushed, published, or released.
- Real-state rehearsals: 20 native trees across both Macs retained their exact bytes/modes through dry-run, apply, and repeated sync in temporary copies. Seven exact trees adopted; 13 customized trees preserved as unmanaged. No old-name folder was recreated. Canonical package IDs remained unchanged.
- Horizontal disposition: standardize contract, resolved in this change through one shared lifecycle. No duplicate command-specific implementation or registry/package-ID migration.
- Planned slices remain uncommitted: implementation/tests; docs; generated bundle. Existing runtime-binding work is preserved and must remain separately attributable during any later commit/release preparation.
- Final verdict at this boundary: **ready for authorized rollout**, not activated. Remaining: authorize and carry out installed CLI delivery plus the exact private policy on both Macs, retain receipt backups, run exact non-forced syncs and verify live checks. A public release/commit/push is a separate authorization gate. Reverting only the executable cannot read new schema-3 receipts; rollback must preserve paired receipt state.
- Goal remains active until authorized delivery and live verification are complete.

## Authorized release preparation

- User explicitly approved commit, PR/merge, release, and installation on both Macs after the implementation review.
- Release version: 1.10.27 (npm latest observed as 1.10.26 before preparation).
- Isolated release branch starts from accepted origin/main; only the task-owned source/test/docs patch is transferred. Generated output is rebuilt here. Unrelated runtime-binding edits remain in the original checkouts and are excluded from this release.
- Managed worktree creation was unavailable for the projectless chat (not a Git repository); a named Git worktree under RUDI/worktrees/cli is used as the documented fallback.
- Fresh release tests, build, production audit, package inventory, PR CI, npm trusted publication, peer source reconciliation, installed checksums and live policy verification are required before completion.

- Release audit red: production dependency audit rejected fast-uri 3.1.6 for GHSA-qw65-cvwx-89v3 and GHSA-58mr-gqgx-xq4g. The existing override and lock resolution advance narrowly to 3.1.8; the dependency-floor contract test follows the patched floor. Production audit then passed with zero vulnerabilities. No other dependency resolution changed.
- Clean release baseline excluded two tests belonging to unrelated runtime work: the initial isolated suite passed 825/825, compared with 827 in the original mixed checkout. The isolated suite is rerun after dependency remediation.

- Final isolated release proof: 825/825 tests passed after remediation, production audit has zero vulnerabilities, focused CLI debt scan has zero findings, six-file package inventory matches the publishing contract, and nine direct compiled-CLI invocations pass the policy/alias/preservation/removal checks.

- Fresh independent release review: Standards pass, Spec pass, Proof pass, no actionable findings. Reviewer reran 21 tests, reproduced the generated artifacts byte-for-byte, and explicitly scanned all five changed JavaScript files with zero findings. Reviewed bundle SHA-256: 237bd3a7efa0ec90c73ecfdfa7033d97238d233bf24eb72bce3cdedc68ef8b3a. PR CI and live rollout remain delivery proof.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@learnrudi/cli",
"version": "1.10.26",
"version": "1.10.27",
"packageManager": "pnpm@10.22.0",
"description": "RUDI CLI - Install and manage local MCP stacks, runtimes, daemon lifecycle, and agent router integrations",
"type": "module",
Expand Down Expand Up @@ -40,7 +40,7 @@
},
"pnpm": {
"overrides": {
"fast-uri": "3.1.6"
"fast-uri": "3.1.8"
}
},
"engines": {
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions src/__tests__/unit/native-skill-command-policy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

test('install, related install, update, sync, check and removal honor one private naming policy', t => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'rudi-native-command-policy-'));
t.after(() => fs.rmSync(home, { recursive: true, force: true }));
const rudiHome = path.join(home, '.rudi');
const source = path.join(rudiHome, 'skills/image-generator');
fs.mkdirSync(source, { recursive: true });
fs.writeFileSync(path.join(source, 'SKILL.md'), '---\nname: image-generator\ndescription: Generate images\n---\n\nVersion one.\n');
fs.writeFileSync(path.join(rudiHome, 'native-skills.json'), JSON.stringify({
schemaVersion: 1, skills: { 'skill:image-generator': { name: 'image-generate', hosts: ['codex', 'claude'] } },
}));
const script = String.raw`
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { cmdInstall, syncRelatedSkillWrappers } from './src/commands/install.js';
import { runUpdate } from './src/commands/update.js';
import { syncSelectedSkillsToNativeHosts } from './src/commands/skills.js';
import { getSkillCheck } from './src/commands/check.js';
import { cleanupRemovedSkill } from './src/commands/remove.js';
const source = path.join(process.env.RUDI_HOME, 'skills/image-generator');
const skill = { id: 'skill:image-generator', kind: 'skill', name: 'image-generator', version: '1.0.0', source: 'rudi', path: source, entryPath: path.join(source, 'SKILL.md'), dependencies: [], requires: {} };
const agents = [{ id: 'codex' }, { id: 'claude-code' }];
const listInstalled = async () => [skill];
await cmdInstall([skill.id], {}, {
fetchIndex: async () => ({}), resolvePackage: async () => skill,
installPackage: async () => ({ success: true, id: skill.id, path: source, installed: [skill.id] }),
installedAgents: agents, exit: code => assert.fail('unexpected exit ' + code),
});
for (const root of [process.env.CODEX_HOME, process.env.CLAUDE_HOME]) {
assert.equal(fs.existsSync(path.join(root, 'skills/image-generate/SKILL.md')), true);
assert.equal(fs.existsSync(path.join(root, 'skills/image-generator')), false);
}
await syncRelatedSkillWrappers([skill], [{ success: true, id: skill.id, path: source }], agents);
const sync = await syncSelectedSkillsToNativeHosts({ targets: ['codex', 'claude', 'gemini'], skillIds: [skill.id], dryRun: true }, { listInstalled });
assert.equal(sync.results.codex.results[0].action, 'would_current');
assert.equal(sync.results.gemini.results[0].action, 'excluded');
const update = await runUpdate([skill.id], {}, {
listInstalled, fetchIndex: async () => ({}), log() {}, error: message => assert.fail(message),
updatePackage: async () => {
fs.appendFileSync(skill.entryPath, 'Version two.\n');
return { success: true, id: skill.id, path: source };
},
});
assert.equal(update.failed, 0);
assert.deepEqual(update.skillProjection.targets, ['codex', 'claude']);
assert.match(fs.readFileSync(path.join(process.env.CODEX_HOME, 'skills/image-generate/SKILL.md'), 'utf8'), /Version two/);
const checked = await getSkillCheck('image-generator', { listInstalled });
assert.equal(checked.projections.codex.state, 'current');
assert.equal(checked.projections.claude.state, 'current');
assert.equal(checked.projections.gemini.state, 'excluded');
fs.appendFileSync(path.join(process.env.CLAUDE_HOME, 'skills/image-generate/SKILL.md'), 'Private customization.\n');
const removed = await cleanupRemovedSkill(skill);
assert.equal(removed.results.codex.action, 'removed');
assert.equal(removed.results.claude.action, 'drifted');
assert.equal(fs.existsSync(path.join(process.env.CODEX_HOME, 'skills/image-generate')), false);
assert.equal(fs.existsSync(path.join(process.env.CLAUDE_HOME, 'skills/image-generate')), true);
`;
assert.doesNotThrow(() => execFileSync(process.execPath, ['--input-type=module', '-e', script], {
cwd: fileURLToPath(new URL('../../..', import.meta.url)),
env: { ...process.env, HOME: home, RUDI_HOME: rudiHome, CODEX_HOME: path.join(home, '.codex'), CLAUDE_HOME: path.join(home, '.claude'), GEMINI_HOME: path.join(home, '.gemini'), ANTIGRAVITY_HOME: path.join(home, '.antigravity') },
encoding: 'utf8', timeout: 30000,
}));
});
Loading
Loading