Skip to content
Draft
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
24 changes: 17 additions & 7 deletions packages/extension/skills/amico-vault/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,24 @@ The vault is no longer a single directory. A user's **Armonia** is the set of va

| Kind | Marker | Repo naming | Holds | Writable |
|------|--------|-------------|-------|----------|
| **personal** | `kind = "personal"` | `armonia-<name>` | Own research notes, hopper, solo specs/plans, session distillates, experiments-in-progress | single-writer (you) |
| **engagement** | `kind = "engagement"` | `armonia-<engagement>` | Engagement-scoped notes; lab state (`lab.toml`, device/model-of-lab notes, calibration, per-lab catalog) once hardware deploys | engagement staff |
| **project** | `kind = "project"` | `armonia-<project>` | Proprietary-package knowledge (your private package internals, hopper, insights) | per-person grant |
| **team** | `kind = "team"` | `armonia-<team>` | Your team knowledge tier: hardware/control context, methods + patterns, strategy/specs/plans, experiments + insights, papers, people/orgs, central pulse catalog (git-lfs) | PR-gated promotion |
| **public** | `kind = "public"` | `<org>/armonia` | Best-practice usage patterns for public packages, hazard notes, platform cards, recipes | world read-only |
| **personal** | `kind = "personal"` | `vault-<name>` (e.g. `vault-aaron`) | Own research notes, hopper, solo specs/plans, session distillates, experiments-in-progress | single-writer (you) |
| **engagement** | `kind = "engagement"` | `vault-<engagement>` | Engagement-scoped notes; lab state (`lab.toml`, device/model-of-lab notes, calibration, per-lab catalog) once hardware deploys | engagement staff |
| **project** | `kind = "project"` | `vault-<project>` | Proprietary-package knowledge (your private package internals, hopper, insights) | per-person grant |
| **team** | `kind = "team"` | `vault-<team>` (e.g. `vault-team` / `armonissima`) | Your team knowledge tier: hardware/control context, methods + patterns, strategy/specs/plans, experiments + insights, papers, people/orgs, central pulse catalog (git-lfs) | PR-gated promotion |
| **public** | `kind = "public"` | `harmoniqs/vault-public` | Best-practice usage patterns for public packages, hazard notes, platform cards, recipes | world read-only |

**Read precedence: personal → engagement → project(s) → team → public.** Queries search the **union** of all mounts; on a path collision the higher-precedence mount wins (first hit). `mounts.toml` (in `~/.amico/`) overrides order and writability; absent, kind-order applies. A dir with no marker, a duplicate id, or a manifest `path` that doesn't exist is dropped from the mount set with a warning in the hook summary — never guessed at, never fatal.

### First-run lifecycle (auto-provision)

A fresh Marketplace install gets a working vault ecosystem with zero commands:

1. **Personal vault** — `ensureVaultEcosystem()` creates `~/armonia/data/vaults/<os-username>/` (`kind="personal"`, local `git init`, no remote) if no personal mount resolves. Offline-tolerant, never throws; activation continues unpersonalized on failure.
2. **Public vault** — shallow-clones `harmoniqs/vault-public` to `~/armonia/data/vaults/vault-public/` (`kind="public"`, `writable=false`, 10s timeout, anonymous https). On offline / no-git / timeout it creates a placeholder dir with a `kind="public"` marker so the mount stack still resolves.
3. **`mounts.toml`** — written *only if absent* (personal rw first, public ro second). Presence means user-managed — never overwritten. The same three steps are mirrored in `tools/bootstrap-armonia.sh` so CLI-first or extension-first order is safe (second run is a no-op).

The canonical on-disk root is `~/armonia/` (`repos/` = versioned source, `data/` = managed state); `~/.amico/vaults` is a symlink into `~/armonia/data/vaults` for backward compat.

### Write routing (Claude is the resolver pre-Amicode)

Route every note-write by intent:
Expand All @@ -81,7 +91,7 @@ Route every note-write by intent:
|---|---|---|
| spec / plan for **shared** work | **team** (your team vault) | PR flow |
| lab state, calibration, device params, engagement notes | **engagement** vault | direct commit |
| proprietary-package knowledge, solver hopper items | **project** vault (e.g. `armonia-<project>`) | direct commit |
| proprietary-package knowledge, solver hopper items | **project** vault (e.g. `vault-<project>`) | direct commit |
| personal research, sessions, scratch, solo specs | **personal** vault | direct commit (auto-synced) |
| **ambiguous** | ask the user once → default personal | — |

Expand All @@ -102,7 +112,7 @@ Never put the mechanism in a `team`/`public` note expecting a later scrub — au

Crystallization to the company vault is double-gated: **gate 1** = author tags `visibility: team`; **gate 2** = a human merges the dream-promote PR. On promotion:

- The **copy** lands in the **team** vault carrying `promoted_from: armonia-<name>` + `promoted_date: YYYY-MM-DD`.
- The **copy** lands in the **team** vault carrying `promoted_from: vault-<name>` + `promoted_date: YYYY-MM-DD`.
- The **original stays put** in its source vault and gains `promoted_to: "[[<central-note>]]"` (a frontmatter-only stamp written back *only after* the PR merges). It is never re-proposed.
- Move is wrong — copy preserves the source vault's local graph and the provenance backlink. `promoted_from`/`promoted_date`/`promoted_to` are the charter/12 provenance fields, carried over unchanged.

Expand Down
43 changes: 20 additions & 23 deletions packages/extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ import { runSetCloudKeyCommand } from "./cloud_key";
import { amicodeOpsDir } from "./substrate/vault_store";
import { stagePasqalConnector } from "./pasqal_assets";
import { needsProvision, pasqalVenvDir, provisionPasqalPython } from "./pasqal_python";
import { createLocalPersonalVault, sanitizeVaultName, suggestVaultName } from "./substrate/vault_setup";
import {
createLocalPersonalVault,
sanitizeVaultName,
suggestVaultName,
ensureVaultEcosystem,
} from "./substrate/vault_setup";
import {
pinnedJuliaMinor,
hasJuliaup,
Expand Down Expand Up @@ -442,6 +447,20 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
const parsed = parseLibraryRootSpecs(raw);
return parsed.length ? parsed : undefined;
};
// Vault ecosystem — first-run auto-provision (personal + public + mounts.toml).
// Idempotent and never throws; runs before the first project prep so the
// mount stack is correct on boot. The named `amicode.setupVault` command
// remains for manual re-entry.
try {
const eco = ensureVaultEcosystem();
if (eco.personal) opencodeChannel.appendLine(`[vault] auto-provisioned local personal vault: ${eco.personal.path} (git=${eco.personal.gitInit})`);
if (eco.publicCloned) opencodeChannel.appendLine(`[vault] public vault cloned: vault-public`);
else if (eco.publicPlaceholder) opencodeChannel.appendLine(`[vault] public vault placeholder (offline or no git)`);
if (eco.mountsWritten) opencodeChannel.appendLine(`[vault] mounts.toml written`);
} catch (e) {
opencodeChannel.appendLine(`[vault] ecosystem ensure failed: ${(e as Error).message}`);
}

const opencodeProject = prepareOpencodeProject({
agentsSrc: path.resolve(ctx.extensionPath, "AGENTS.md"),
// MODE-SELECTED vetted template: HP sessions get the Piccolissimo variant
Expand Down Expand Up @@ -973,28 +992,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
void vscode.window.showInformationMessage(`Amicode: personal vault "${created.name}" created and active.`);
};
ctx.subscriptions.push(vscode.commands.registerCommand("amicode.setupVault", () => void runVaultSetup(true)));
// Personal vault by default. The onboarding wizard (opencode-side) writes the
// profile but NOT a vault, and a genuine first-timer has none — so Amico would
// have nowhere to remember them (distiller disabled, session unpersonalized).
// Silently provision a LOCAL personal vault on first run when none resolves —
// no modal, like the Julia project. The `amicode.setupVault` command remains
// for naming / re-creating; the wizard finale offers attaching other vaults.
// Failure-tolerant: a creation error just leaves the session unpersonalized.
const ensureDefaultPersonalVault = async (): Promise<void> => {
if (personalMount(resolveMountStack())) return;
let created;
try {
created = createLocalPersonalVault(defaultVaultsRoot(), suggestVaultName());
} catch (e) {
opencodeChannel.appendLine(`[vault] default personal vault not created: ${(e as Error).message}`);
return;
}
opencodeChannel.appendLine(
`[vault] auto-provisioned local personal vault: ${created.path} (git=${created.gitInit})`,
);
await respawnForVault();
};
void ensureDefaultPersonalVault();

// Julia setup (#8): amicode manages the Julia toolchain via juliaup — install
// juliaup if absent, add the channel pinned to the Manifest's MINOR, and
Expand Down
216 changes: 216 additions & 0 deletions packages/extension/src/substrate/vault_setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,33 @@ function safeUsername(): string {
}
}

export const PUBLIC_VAULT_DIR = "vault-public";
export const PUBLIC_VAULT_REPO = "https://github.com/harmoniqs/vault-public.git";
export const PUBLIC_VAULT_KIND = "public";

function defaultVaultsRoot(): string {
return path.join(os.homedir(), ".amico", "vaults");
}
function defaultMountsTomlPath(): string {
return path.join(os.homedir(), ".amico", "mounts.toml");
}
function armoniaDataRoot(): string {
return path.join(os.homedir(), "armonia", "data");
}

export interface CreatedVault {
path: string;
name: string;
gitInit: boolean;
}

export interface VaultEcosystemResult {
personal?: CreatedVault;
publicCloned: boolean;
publicPlaceholder: boolean;
mountsWritten: boolean;
}

/**
* Create a local personal vault. Refuses to clobber an existing directory.
* `git init` is best-effort (the vault is fully functional without git).
Expand Down Expand Up @@ -73,3 +94,198 @@ export function createLocalPersonalVault(
}
return { path: dir, name, gitInit };
}

/** Ensure ~/armonia/data/{env,problems,runs,vaults} and ~/.amico symlinks. Never throws. */
export function ensureArmoniaDataDirs(): void {
try {
const dataRoot = armoniaDataRoot();
for (const sub of ["env", "problems", "runs", "vaults"]) {
try {
fs.mkdirSync(path.join(dataRoot, sub), { recursive: true });
} catch {}
}
// Wire ~/.amico/<name> -> ~/armonia/data/<target> when safe.
const links: Array<[string, string]> = [
["julia", "env"],
["problems", "problems"],
["runs", "runs"],
["vaults", "vaults"],
];
const amico = path.join(os.homedir(), ".amico");
try {
fs.mkdirSync(amico, { recursive: true });
} catch {}
for (const [srcName, targetName] of links) {
const src = path.join(amico, srcName);
const dest = path.join(dataRoot, targetName);
try {
if (fs.lstatSync(src).isSymbolicLink()) continue;
} catch {}
try {
if (fs.existsSync(src)) {
// Real dir with content → migration case, don't clobber.
const entries = fs.readdirSync(src);
if (entries.length > 0) continue;
fs.rmdirSync(src);
}
fs.symlinkSync(dest, src);
} catch {}
}
} catch {}
}

/** Ensure the public vault (vault-public, kind=public, ro). Never throws. */
export function ensurePublicVault(opts: {
vaultsRoot?: string;
repo?: string;
timeoutMs?: number;
} = {}): { cloned: boolean; placeholder: boolean } {
const vaultsRoot = opts.vaultsRoot ?? defaultVaultsRoot();
const repo = opts.repo ?? PUBLIC_VAULT_REPO;
const timeoutMs = opts.timeoutMs ?? 10_000;
const dir = path.join(vaultsRoot, PUBLIC_VAULT_DIR);
const marker = path.join(dir, ".amico-vault.toml");

// Already present (any kind) → no-op.
try {
if (fs.existsSync(marker)) return { cloned: false, placeholder: false };
} catch {}
if (fs.existsSync(dir)) {
// Dir exists without marker → seed marker as public.
try {
fs.writeFileSync(marker, `kind = "${PUBLIC_VAULT_KIND}"\nname = "${PUBLIC_VAULT_DIR}"\n`);
return { cloned: false, placeholder: true };
} catch {
return { cloned: false, placeholder: false };
}
}

// Try shallow clone.
try {
execFileSync("git", ["clone", "--depth", "1", "--single-branch", repo, dir], {
stdio: "ignore",
timeout: timeoutMs,
});
// Ensure marker is public (repo should already carry it).
try {
const text = fs.readFileSync(marker, "utf8");
if (!text.includes(`kind = "${PUBLIC_VAULT_KIND}"`)) {
fs.writeFileSync(marker, `kind = "${PUBLIC_VAULT_KIND}"\nname = "${PUBLIC_VAULT_DIR}"\n`);
}
} catch {
try {
fs.writeFileSync(marker, `kind = "${PUBLIC_VAULT_KIND}"\nname = "${PUBLIC_VAULT_DIR}"\n`);
} catch {}
}
return { cloned: true, placeholder: false };
} catch {}

// Offline / no-git / timeout → placeholder.
try {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(marker, `kind = "${PUBLIC_VAULT_KIND}"\nname = "${PUBLIC_VAULT_DIR}"\n`);
try {
fs.writeFileSync(path.join(dir, "README.md"), "# vault-public (offline placeholder)\n\nCloned on next online activate.\n");
} catch {}
return { cloned: false, placeholder: true };
} catch {
return { cloned: false, placeholder: false };
}
}

/** Ensure ~/.amico/mounts.toml exists with personal + public precedence. Absent-only. */
export function ensureMountsToml(opts: {
mountsTomlPath?: string;
vaultsRoot?: string;
} = {}): boolean {
const mountsTomlPath = opts.mountsTomlPath ?? defaultMountsTomlPath();
const vaultsRoot = opts.vaultsRoot ?? defaultVaultsRoot();
try {
if (fs.existsSync(mountsTomlPath)) return false;
} catch {}
// Resolve personal name from actual dirs after ensure steps.
let personalId: string | undefined;
try {
const entries = fs.readdirSync(vaultsRoot);
for (const base of entries) {
const marker = path.join(vaultsRoot, base, ".amico-vault.toml");
try {
const text = fs.readFileSync(marker, "utf8");
if (text.includes('kind = "personal"')) {
const m = text.match(/name\s*=\s*"([^"]+)"/);
personalId = m ? m[1] : base;
break;
}
} catch {}
}
} catch {}

const lines: string[] = [];
if (personalId) {
lines.push("[[mount]]");
lines.push(`id = "${personalId}"`);
lines.push(`kind = "personal"`);
lines.push(`writable = true`);
lines.push("");
}
// Public entry — always emitted if vault-public dir exists (even placeholder).
const publicDir = path.join(vaultsRoot, PUBLIC_VAULT_DIR);
let hasPublic = false;
try {
hasPublic = fs.existsSync(path.join(publicDir, ".amico-vault.toml"));
} catch {}
if (hasPublic) {
lines.push("[[mount]]");
lines.push(`id = "${PUBLIC_VAULT_DIR}"`);
lines.push(`kind = "${PUBLIC_VAULT_KIND}"`);
lines.push(`writable = false`);
lines.push("");
}
if (lines.length === 0) return false;
try {
fs.mkdirSync(path.dirname(mountsTomlPath), { recursive: true });
fs.writeFileSync(mountsTomlPath, lines.join("\n"));
return true;
} catch {
return false;
}
}

/** Full first-run ecosystem: data dirs + personal + public + mounts.toml. Never throws. */
export function ensureVaultEcosystem(opts: {
vaultsRoot?: string;
mountsTomlPath?: string;
publicRepo?: string;
hint?: string;
} = {}): VaultEcosystemResult {
const vaultsRoot = opts.vaultsRoot ?? defaultVaultsRoot();
const mountsTomlPath = opts.mountsTomlPath ?? defaultMountsTomlPath();
const result: VaultEcosystemResult = { publicCloned: false, publicPlaceholder: false, mountsWritten: false };

ensureArmoniaDataDirs();

// Personal — silent auto-provision if none resolves.
let hasPersonal = false;
try {
const entries = fs.readdirSync(vaultsRoot);
for (const base of entries) {
try {
const text = fs.readFileSync(path.join(vaultsRoot, base, ".amico-vault.toml"), "utf8");
if (text.includes('kind = "personal"')) { hasPersonal = true; break; }
} catch {}
}
} catch {}
if (!hasPersonal) {
try {
const created = createLocalPersonalVault(vaultsRoot, suggestVaultName(opts.hint));
result.personal = created;
} catch {}
}

const pub = ensurePublicVault({ vaultsRoot, repo: opts.publicRepo });
result.publicCloned = pub.cloned;
result.publicPlaceholder = pub.placeholder;

result.mountsWritten = ensureMountsToml({ mountsTomlPath, vaultsRoot });
return result;
}
9 changes: 6 additions & 3 deletions packages/extension/src/trees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,12 @@ export function registerTrees(ctx: vscode.ExtensionContext): {
armonia: PlaceholderTree;
} {
const catalog = new SessionCatalogTree(ctx);
// amicode#204: the single Armonia panel. Its roots are the mounted Vaults;
// until ArmoniaService lands, a product empty state names what collects here.
const armonia = new PlaceholderTree("Your vaults collect here — run Amicode: Set up a vault");
// amicode#204: single Armonia panel. Until ArmoniaService lands, a product
// empty state names the canonical location. First-run auto-provisions
// personal + vault-public (offline → placeholder) and writes mounts.toml.
const armonia = new PlaceholderTree(
"Your vaults live in ~/armonia/data/vaults — personal + vault-public after first run",
);

ctx.subscriptions.push(
vscode.window.registerTreeDataProvider("amicode.catalog", catalog),
Expand Down
Loading
Loading