Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/amico-run/src/ledger_dispatch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/amico-run/src/lens_registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
13 changes: 7 additions & 6 deletions packages/amico-run/src/mounts.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
75 changes: 56 additions & 19 deletions packages/amico-run/src/profile_verb.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
//
// amico profile resolve (--name <preset> | --profile <file.toml>)
// [--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
Expand All@@ -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
Expand All@@ -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";
Expand DownExpand Up@@ -91,23 +92,52 @@ 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];
if (explicit) return explicit;
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 `<dir>/entitlements.toml`'s `codes` —
Expand All@@ -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) {
Expand DownExpand Up@@ -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 ───────────────────────────────────────────────
Expand DownExpand Up@@ -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) {
Expand DownExpand Up@@ -449,7 +486,7 @@ export function profileVerb(argv: string[]): VerbResult {
verb: "profile",
error: `unknown subcommand ${sub ? `"${sub}"` : "(none)"}`,
usage:
"amico profile resolve (--name <preset> | --profile <file.toml>) [--mode spool-up|dispatch] [--profiles-dir D] [--skills-dir D] [--gates-dir D] [--entitlements a,b]",
"amico profile resolve (--name <preset> | --profile <file.toml>) [--mode spool-up|dispatch] [--profiles-dir D] [--skills-dir D (repeatable)] [--gates-dir D] [--entitlements a,b]",
},
code: 64,
};
Expand Down
2 changes: 1 addition & 1 deletion packages/amico-run/src/verbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
};
Expand Down
5 changes: 3 additions & 2 deletions packages/amico-run/test/mounts.test.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
34 changes: 20 additions & 14 deletions packages/amico-run/test/profile_verb.test.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand DownExpand Up@@ -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<string, unknown> } {
const r = profileResolve([
Expand DownExpand Up@@ -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",
]);
Expand Down
6 changes: 3 additions & 3 deletions packages/extension/src/substrate/mount_store.ts
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
Loading