From 39ad5ff3d7e57c3569ea17e8ead9c367af60f047 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Tue, 4 Aug 2026 00:59:37 -0400 Subject: [PATCH] chore(fleet): re-home the profile/skill tree to the armonissima vault; multi-root skill validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The internal content half of the amico-plugin dissolution (the public half is PR #255): profiles/, gates/, and the internal skill library now live in the armonissima team vault (pushed as armonissima@3d78491); mount presence is the eligibility proof, same pattern as the retired plugin checkout. profile_verb: the shipped fleet presets compose BOTH tiers (researcher names amico-vault + hypothesis-review — public), and a single --skills-dir cannot name two roots. --skills-dir is now REPEATABLE; skill existence/surface is a union check across roots (first-hit wins, matching the library resolver); defaults = the vault skills dir + the extension's in-repo public library when resolvable from the installed bundle layout (bin/dist -> ../../skills). This lets the 12 long-skipped shipped-preset tests RUN for the first time (the old default root never existed on main) — they pass. Canonicity sweep: the mounts resolver + mount_store were ported from the Claude session-start hook; with the hook retired, the TS ports are the source of truth and the fixtures are the spec. profile_verb's lint-port comments re-point at the vault tree; lens_registry's deviation note resolves to "the design"; verbs/ledger_dispatch comments updated. Verified: amico-run suite 952 passed / 0 skipped (was 940 + 12 skips); extension suite 868 passed; typecheck clean. --- packages/amico-run/src/ledger_dispatch.ts | 2 +- packages/amico-run/src/lens_registry.ts | 3 +- packages/amico-run/src/mounts.ts | 13 ++-- packages/amico-run/src/profile_verb.ts | 75 ++++++++++++++----- packages/amico-run/src/verbs.ts | 2 +- packages/amico-run/test/mounts.test.ts | 5 +- packages/amico-run/test/profile_verb.test.ts | 34 +++++---- .../extension/src/substrate/mount_store.ts | 6 +- 8 files changed, 93 insertions(+), 47 deletions(-) diff --git a/packages/amico-run/src/ledger_dispatch.ts b/packages/amico-run/src/ledger_dispatch.ts index cc408754..6b9339c0 100644 --- a/packages/amico-run/src/ledger_dispatch.ts +++ b/packages/amico-run/src/ledger_dispatch.ts @@ -72,7 +72,7 @@ export function laneOf(task_type: TaskType | string): Lane { export const LADDER = ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-5", "anthropic/claude-opus-4-8"] as const; /** The frontier tier. G-E asked which model backs the frontier rung: fable-class has * landed, so the frontier set is the 5-series top tier and `opus-4-8` is now a - * BELOW-frontier rung (kept in step with amico-plugin's profiles/SCHEMA.md §5). */ + * BELOW-frontier rung (kept in step with the armonissima vault's profiles/SCHEMA.md §5). */ export const FRONTIER_MODELS = [ "anthropic/claude-opus-5", "anthropic/claude-fable-5", diff --git a/packages/amico-run/src/lens_registry.ts b/packages/amico-run/src/lens_registry.ts index 043c02d9..d0f64d73 100644 --- a/packages/amico-run/src/lens_registry.ts +++ b/packages/amico-run/src/lens_registry.ts @@ -8,7 +8,8 @@ // DEVIATION FROM THE SPEC, recorded deliberately (advisory A-13): spec §3.5 places this // registry in amico-plugin and defines `lens_registry_version` as that repo's git sha. // Nothing in amico-run can read another repo's sha at runtime, so the registry lives here -// and the version is a local constant until the plugin-side home exists. +// and the version is a local constant. (amico-plugin is now retired — the content home +// is the armonissima vault — and this deviation is simply the design.) import { TASK_TYPES, type TaskType } from "./ledger.js"; /** Bumped BY HAND whenever the lens set or its applicability changes. Stamped into every diff --git a/packages/amico-run/src/mounts.ts b/packages/amico-run/src/mounts.ts index 0dbbe3f2..a3cf5a0f 100644 --- a/packages/amico-run/src/mounts.ts +++ b/packages/amico-run/src/mounts.ts @@ -12,12 +12,13 @@ // the extension can depend on amico-run. Keep the two byte-for-byte behaviorally // identical until then — the ONLY intended delta is this file's env seam (below). // -// PARITY ORACLE: the amico-plugin session-start hook -// (~/harmoniqs/amico-plugin-vault-cli/hooks/session-start, branch -// feat/amico-vault-mounts-toml, PR #27). Same ranks, same skip/rescue rules, same -// unlisted-append behavior. The canonical kind order follows the APPROVED -// vault-CLI spec (spec-20260703-053956), NOT the Ombra draft table — the Ombra -// draft swapped team/restricted; here restricted=3 < team=4 (spec correction). +// CANONICAL: this module was ported from the amico-plugin session-start hook +// (branch feat/amico-vault-mounts-toml, PR #27) — that hook is now RETIRED with +// the plugin repo, and THIS port is the source of truth. Same ranks, same +// skip/rescue rules, same unlisted-append behavior. The canonical kind order +// follows the APPROVED vault-CLI spec (spec-20260703-053956), NOT the Ombra +// draft table — the Ombra draft swapped team/restricted; here restricted=3 < +// team=4 (spec correction). // // kind rank writable(default) // personal 0 rw diff --git a/packages/amico-run/src/profile_verb.ts b/packages/amico-run/src/profile_verb.ts index 584a6fd1..6d39feba 100644 --- a/packages/amico-run/src/profile_verb.ts +++ b/packages/amico-run/src/profile_verb.ts @@ -3,7 +3,7 @@ // // amico profile resolve (--name | --profile ) // [--mode spool-up|dispatch] -// [--profiles-dir D] [--skills-dir D] [--gates-dir D] +// [--profiles-dir D] [--skills-dir D (repeatable)] [--gates-dir D] // [--entitlements a,b | --entitlements-dir D] // → validate the profile, entitlement-filter its `skills`, apply the spool-up // composition rule, and print the resolved loadout as JSON (plus the lossy @@ -13,10 +13,11 @@ // chat, so the error payload names that fallback explicitly. // // THE SCHEMA THIS VALIDATES lives (with the presets and the gate registry) in the -// amico-plugin repo: `profiles/*.toml` + `profiles/SCHEMA.md`, with -// `tests/lint_profiles.sh` as its executable spec. The rules below are a deliberate -// port of that lint — same vocabularies, same error/warning split — so a profile that -// passes CI resolves here and vice versa. When the lint changes, change this too. +// armonissima team vault: `profiles/*.toml` + `profiles/SCHEMA.md` + `gates/*.toml` +// (re-homed from the retired amico-plugin repo). The rules below are a deliberate +// port of that tree's lint — same vocabularies, same error/warning split — so a +// profile that passes the vault's CI resolves here and vice versa. When the lint +// changes, change this too. // // SPOOL-UP COMPOSITION RULE (§2.2/§3.1, the one rule easiest to get wrong): spool-up // ALWAYS instantiates the `resident` shell. A referenced preset contributes only @@ -26,7 +27,7 @@ // enforces that and reports the ignored value rather than silently dropping it. import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { parse as parseToml } from "smol-toml"; import { TASK_TYPES } from "./ledger.js"; import { FRONTIER_MODELS, LADDER } from "./ledger_dispatch.js"; @@ -91,16 +92,23 @@ function flagValue(argv: string[], name: string): string | undefined { return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; } +/** Repeatable-flag collector (unlike flagValue): every occurrence, in order. */ +function flagValues(argv: string[], name: string): string[] { + const out: string[] = []; + for (let i = 0; i < argv.length; i++) if (argv[i] === name && i + 1 < argv.length) out.push(argv[i + 1]); + return out; +} + const str = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined); const strList = (v: unknown): string[] | undefined => Array.isArray(v) && v.every((x) => typeof x === "string") ? (v as string[]) : undefined; // ── roots ──────────────────────────────────────────────────────────────────────── -/** The profiles/gates/skills tree: the amico-plugin checkout, which is also where the - * extension looks for library skills (DEFAULT_LIBRARY_ROOTS). Overridable per-flag - * and per-env so CI and tests point at a fixture tree. */ +/** The profiles/gates/skills tree: the armonissima vault mount, which is also where + * the extension looks for internal library skills (DEFAULT_LIBRARY_ROOTS). Overridable + * per-flag and per-env so CI and tests point at a fixture tree. */ function profilesDir(argv: string[]): string { - return flagValue(argv, "--profiles-dir") ?? process.env.AMICO_PROFILES_DIR ?? join(homedir(), "harmoniqs", "amico-plugin", "profiles"); + return flagValue(argv, "--profiles-dir") ?? process.env.AMICO_PROFILES_DIR ?? join(homedir(), ".amico", "vaults", "armonissima", "profiles"); } function siblingDir(argv: string[], flag: string, env: string, name: string): string { const explicit = flagValue(argv, flag) ?? process.env[env]; @@ -108,6 +116,28 @@ function siblingDir(argv: string[], flag: string, env: string, name: string): st return join(profilesDir(argv), "..", name); } +/** The skills roots a profile's skills are validated against. Post-amico-plugin the + * library is SPLIT ACROSS TWO ROOTS: the armonissima vault's skills/ (internal tier) + * and the public library shipped INSIDE the amicode extension (packages/extension/ + * skills/), resolvable from the installed CLI bundle (bin/dist/amico.js → ../../skills). + * A profile legitimately composes both tiers, so existence must be a union check. + * `--skills-dir` is REPEATABLE and any explicit flag(s) replace the defaults outright + * (tests always pass them); the in-repo default self-disables wherever the bundle + * layout is absent (dev checkouts of amico-run alone, CI). */ +function skillsDirs(argv: string[]): string[] { + const explicit = flagValues(argv, "--skills-dir"); + if (explicit.length > 0) return explicit; + const env = process.env.AMICO_SKILLS_DIR; + if (env && env.trim() !== "") return [env]; + const dirs = [join(profilesDir(argv), "..", "skills")]; + const script = process.argv[1]; + if (script) { + const inRepo = resolve(dirname(script), "..", "..", "skills"); + if (existsSync(inRepo)) dirs.push(inRepo); + } + return dirs; +} + // ── entitlements ───────────────────────────────────────────────────────────────── /** The held entitlement codes. Precedence: `--entitlements a,b` (an empty string means * "none"), then `$AMICO_ENTITLEMENTS`, then `/entitlements.toml`'s `codes` — @@ -132,11 +162,18 @@ function readEntitlements(argv: string[], warnings: string[]): { codes: string[] } // ── skills ─────────────────────────────────────────────────────────────────────── -/** A skill's `surface:` tag, read from its SKILL.md frontmatter. undefined = the file - * exists but carries no tag; null = no such skill. */ -function skillSurface(skillsDir: string, name: string): string | undefined | null { - const file = join(skillsDir, name, "SKILL.md"); - if (!existsSync(file)) return null; +/** A skill's `surface:` tag, read from its SKILL.md frontmatter — searched across the + * skills roots in order, first hit wins. undefined = the file exists but carries no + * tag; null = no such skill in ANY root. */ +function skillSurface(skillsDirs: string[], name: string): string | undefined | null { + for (const dir of skillsDirs) { + const file = join(dir, name, "SKILL.md"); + if (!existsSync(file)) continue; + return skillSurfaceIn(file); + } + return null; +} +function skillSurfaceIn(file: string): string | undefined { const lines = readFileSync(file, "utf8").split("\n"); let fences = 0; for (const line of lines) { @@ -205,7 +242,7 @@ export function profileResolve(argv: string[]): VerbResult { } const stem = path.replace(/^.*[\\/]/, "").replace(/\.toml$/, ""); - const skillsDir = siblingDir(argv, "--skills-dir", "AMICO_SKILLS_DIR", "skills"); + const skillsRoots = skillsDirs(argv); const gatesDir = siblingDir(argv, "--gates-dir", "AMICO_GATES_DIR", "gates"); // ── required fields + identity ─────────────────────────────────────────────── @@ -326,10 +363,10 @@ export function profileResolve(argv: string[]): VerbResult { const skills: string[] = []; const skillsFiltered: Array<{ name: string; reason: string }> = []; for (const sk of declaredSkills ?? []) { - const sksurface = skillSurface(skillsDir, sk); + const sksurface = skillSurface(skillsRoots, sk); if (sksurface === null) { // §3.1's "missing skill" failure path: loud, pre-injection. - errors.push(`skill "${sk}" does not exist (no ${join(skillsDir, sk, "SKILL.md")})`); + errors.push(`skill "${sk}" does not exist (no SKILL.md under any skills root: ${skillsRoots.join(", ")})`); continue; } if (sksurface === undefined) { @@ -449,7 +486,7 @@ export function profileVerb(argv: string[]): VerbResult { verb: "profile", error: `unknown subcommand ${sub ? `"${sub}"` : "(none)"}`, usage: - "amico profile resolve (--name | --profile ) [--mode spool-up|dispatch] [--profiles-dir D] [--skills-dir D] [--gates-dir D] [--entitlements a,b]", + "amico profile resolve (--name | --profile ) [--mode spool-up|dispatch] [--profiles-dir D] [--skills-dir D (repeatable)] [--gates-dir D] [--entitlements a,b]", }, code: 64, }; diff --git a/packages/amico-run/src/verbs.ts b/packages/amico-run/src/verbs.ts index 0a1055f0..1f33720d 100644 --- a/packages/amico-run/src/verbs.ts +++ b/packages/amico-run/src/verbs.ts @@ -121,7 +121,7 @@ const ledger: Verb = { const profile: Verb = { name: "profile", summary: "resolve a capability profile: validate + entitlement-filter skills + apply the spool-up composition rule", - generalizes: "the fleet substrate's profile tree (amico-plugin profiles/ + gates/) at session spool-up", + generalizes: "the fleet substrate's profile tree (armonissima vault profiles/ + gates/) at session spool-up", slice: "fleet substrate (§9 step 2)", run: profileVerb, }; diff --git a/packages/amico-run/test/mounts.test.ts b/packages/amico-run/test/mounts.test.ts index 4c2e4dca..dcc64958 100644 --- a/packages/amico-run/test/mounts.test.ts +++ b/packages/amico-run/test/mounts.test.ts @@ -1,8 +1,9 @@ // `mounts.ts` — the Armonia mount-stack resolver (Slice B, plan Task 6; // spec-20260703-053956 vault-CLI canonical order). Pure logic, so it is unit-tested // directly against src (no bundle): fixture tmp-dir vault trees exercise discovery, -// precedence, the manifest override/rescue, and the env seam. The parity oracle is -// the amico-plugin session-start hook (branch feat/amico-vault-mounts-toml, PR #27). +// precedence, the manifest override/rescue, and the env seam. (The resolver was +// originally ported from the retired amico-plugin session-start hook; the port is +// canonical now — these fixtures ARE the spec.) import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; diff --git a/packages/amico-run/test/profile_verb.test.ts b/packages/amico-run/test/profile_verb.test.ts index 75cc15aa..1823ee0e 100644 --- a/packages/amico-run/test/profile_verb.test.ts +++ b/packages/amico-run/test/profile_verb.test.ts @@ -1,10 +1,11 @@ // `amico profile resolve` (fleet spec §2/§3.1) — the capability-profile resolver the // extension shells at spool-up, before it injects an agent def. // -// The rules here are a port of amico-plugin's tests/lint_profiles.sh (the executable -// spec, documented in profiles/SCHEMA.md): a profile that passes CI must resolve here, -// and vice versa. The last block resolves the REAL shipped presets when the -// amico-plugin checkout is present, so the two halves cannot drift silently. +// The rules here are a port of the profile tree's lint (the executable spec, +// documented in profiles/SCHEMA.md — now homed in the armonissima vault): a profile +// that passes CI must resolve here, and vice versa. The last block resolves the +// REAL shipped presets when the vault mount is present, so the two halves cannot +// drift silently. // Run: pnpm --filter @amicode/amico-run test profile_verb import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; @@ -90,7 +91,7 @@ function profile(name: string, f: Fields = {}): string { } /** Always pass the dirs AND the entitlement set explicitly: the defaults read the real - * amico-plugin checkout and ~/.amico/amicode/entitlements.toml, which would make these + * armonissima vault mount and ~/.amico/amicode/entitlements.toml, which would make these * tests depend on the developer's machine. */ function resolve(args: string[], entitlements = "issimo"): { code: number; json: Record } { const r = profileResolve([ @@ -502,22 +503,27 @@ describe("the verifiability rule — ungated failure is silent failure", () => { }); }); -// ── cross-repo: the REAL shipped presets ────────────────────────────────────────── -// Skipped when the amico-plugin checkout is absent (CI has no sibling repos), exactly -// like the lint's own runner-existence check. -const PLUGIN = join(homedir(), "harmoniqs", "amico-plugin"); -const HAVE_PLUGIN = existsSync(join(PLUGIN, "profiles")) && existsSync(join(PLUGIN, "gates")); +// ── cross-store: the REAL shipped presets ───────────────────────────────────────── +// Skipped when the armonissima vault mount is absent (CI has no team vault), exactly +// like the lint's own runner-existence check. The skills are validated against BOTH +// roots a profile legitimately composes: the vault's internal tier + the in-repo +// public library (packages/extension/skills — always present in this monorepo). +const VAULT = join(homedir(), ".amico", "vaults", "armonissima"); +const IN_REPO_SKILLS = join(__dirname, "..", "..", "extension", "skills"); +const HAVE_VAULT = existsSync(join(VAULT, "profiles")) && existsSync(join(VAULT, "gates")) && existsSync(IN_REPO_SKILLS); -describe.skipIf(!HAVE_PLUGIN)("the shipped amico-plugin presets resolve", () => { +describe.skipIf(!HAVE_VAULT)("the shipped armonissima presets resolve", () => { const real = (args: string[]) => profileResolve([ ...args, "--profiles-dir", - join(PLUGIN, "profiles"), + join(VAULT, "profiles"), "--skills-dir", - join(PLUGIN, "skills"), + join(VAULT, "skills"), + "--skills-dir", + IN_REPO_SKILLS, "--gates-dir", - join(PLUGIN, "gates"), + join(VAULT, "gates"), "--entitlements", "issimo", ]); diff --git a/packages/extension/src/substrate/mount_store.ts b/packages/extension/src/substrate/mount_store.ts index 760d093a..953c628f 100644 --- a/packages/extension/src/substrate/mount_store.ts +++ b/packages/extension/src/substrate/mount_store.ts @@ -1,9 +1,9 @@ /** Armonia mount-stack discovery + precedence (spec-20260707-002846 Component 1 * — "bootstrap parity", read side). * - * A TypeScript port of the amico-plugin session-start hook's mount discovery - * (the PARITY ORACLE: ~/harmoniqs/amico-plugin-vault-cli/hooks/session-start, - * branch feat/amico-vault-mounts-toml / PR #27, lines 53–232). Same ranks, same + * A TypeScript port of the (now-retired) amico-plugin session-start hook's mount + * discovery (branch feat/amico-vault-mounts-toml / PR #27, lines 53–232). The hook + * died with the Claude Code channel; THIS port is canonical. Same ranks, same * skip/rescue semantics, same unlisted-append behavior. * * Canonical kind ranks follow the APPROVED vault-CLI spec