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
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
<img alt="Amicode" src="packages/extension/media/amico-face-light.svg" width="112">
</picture>

<picture>
<source media="(prefers-color-scheme: dark)" srcset="packages/extension/media/amicode-wordmark-dark.svg">
<img alt="AMICODE" src="packages/extension/media/amicode-wordmark-light.svg" width="224">
</picture>
# Amicode

### Open autonomous research, starting with quantum control.

Expand Down
31 changes: 20 additions & 11 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,14 @@
"command": "amicode.openRunDir",
"title": "Amicode: Open current run directory"
},
{
"command": "amicode.distillNow",
"title": "Amicode: Distill now (update my memory)"
},
{
"command": "amicode.distillNow",
"title": "Amicode: Distill now (update my memory)"
},
{
"command": "amicode.sync.now",
"title": "Amicode: Sync now (update workspace repos)"
},
{
"command": "amicode.catalog.remove",
"title": "Remove from Catalog"
Expand Down Expand Up @@ -303,13 +307,18 @@
"default": true,
"description": "Capture agent runs to Harmoniqs' internal run corpus (OpenTelemetry). Runs include your prompts and tool output, which may contain source code. On by default, but NOTHING is transmitted until ALL of: you answer the first-run consent prompt, an endpoint is configured (amicode.telemetry.endpoint), and your Amico cloud connection is set up (\"Amico: Connect Cloud\") — capture authenticates with your per-user cloud token. Turn off to disable capture entirely."
},
"amicode.telemetry.endpoint": {
"type": "string",
"default": "https://bld42qbgsn7gu6y44v4kd6a32e0hprmy.lambda-url.us-east-1.on.aws",
"description": "Base URL of the run-corpus ingest endpoint (no trailing slash; opencode appends /v1/traces and /v1/logs). Defaults to the PRODUCTION corpus. Empty = capture stays dormant even with consent given. Auth uses your per-user Amico cloud token (~/.amico/cloud.json, set up via \"Amico: Connect Cloud\") — never a separate ingest key. The token MUST be minted in the same AWS account as this endpoint: each account's credentials table is independent, so a token from the other account is rejected on every batch (401) while capture still looks enabled. This default is therefore account-coupled with DEFAULT_CLOUD_URL in cloud_key.ts — change both together or neither."
}
}
},
"amicode.telemetry.endpoint": {
"type": "string",
"default": "https://bld42qbgsn7gu6y44v4kd6a32e0hprmy.lambda-url.us-east-1.on.aws",
"description": "Base URL of the run-corpus ingest endpoint (no trailing slash; opencode appends /v1/traces and /v1/logs). Defaults to the PRODUCTION corpus. Empty = capture stays dormant even with consent given. Auth uses your per-user Amico cloud token (~/.amico/cloud.json, set up via \"Amico: Connect Cloud\") — never a separate ingest key. The token MUST be minted in the same AWS account as this endpoint: each account's credentials table is independent, so a token from the other account is rejected on every batch (401) while capture still looks enabled. This default is therefore account-coupled with DEFAULT_CLOUD_URL in cloud_key.ts — change both together or neither."
},
"amicode.sync.enabled": {
"type": "boolean",
"default": false,
"description": "Keep workspace git repos in sync (opt-in). When enabled, Amicode checks on startup if it missed a nightly sync (>20h) and, if so, fast-forwards clean repos and keeps WIP current with main. Dirty work is never touched. Also offered as 'Amicode: Sync now' in the Command Palette. Off by default — enable in Settings or via the first-run prompt."
}
}
},
"menus": {
"view/title": [
{
Expand Down
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
140 changes: 117 additions & 23 deletions packages/extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,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 All @@ -56,6 +61,14 @@ import {
import { probeCommand, formatHealthReport, type HealthResult } from "./healthcheck";
import { resolveMountStack, personalMount, defaultVaultsRoot } from "./substrate/mount_store";
import { initDistillerTransport, triggerRunDistill, triggerSweep, type DistillerSetup } from "./substrate/distiller";
import {
findWorkspaceRepos,
syncOneRepo,
isSyncDue,
SYNC_INTERVAL_MS,
LAST_SYNC_KEY,
SYNC_DISMISSED_KEY,
} from "./substrate/sync";
import {
registerBugReport,
unregisterBugReport,
Expand Down Expand Up @@ -431,6 +444,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 @@ -851,28 +878,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 Expand Up @@ -958,6 +963,95 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
void runJuliaSetup(false);
}

// ── Workspace sync (opt-in, extension-host) ──────────────────────────
// Single toggle amicode.sync.enabled (off by default). First-run nudge,
// missed-nightly check on activation (if >20h since last sync, run within
// 2 min), and manual Amicode: Sync now. Keeps any git repo in the open
// workspace current: clean WIP auto-rebases onto freshly fast-forwarded main,
// dirty skips rebase, conflicts abort cleanly. Output: channel log + notifications
// only on attention-needed (failed/conflicted). Julia env: check+nudge only (stub).
const syncChannel = vscode.window.createOutputChannel("Amicode — sync");
ctx.subscriptions.push(syncChannel);

const isSyncEnabled = (): boolean =>
vscode.workspace.getConfiguration("amicode").get<boolean>("sync.enabled", false);

const runWorkspaceSync = async (source: "startup" | "manual"): Promise<void> => {
const folders = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath);
const repos = findWorkspaceRepos(folders);
if (repos.length === 0) {
syncChannel.appendLine(`[sync:${source}] no git repos in workspace — nothing to do`);
return;
}
syncChannel.appendLine(`[sync:${source}] ${repos.length} repo(s): ${repos.map((r) => path.basename(r)).join(", ")}`);
let ok = 0,
skipped = 0,
failed = 0;
const failures: string[] = [];
for (const repoPath of repos) {
const res = syncOneRepo(repoPath);
syncChannel.appendLine(`[sync] ${res.repo} (${res.branch}) — ${res.status}: ${res.detail}`);
if (res.status === "ok") ok++;
else if (res.status === "skipped") skipped++;
else {
failed++;
failures.push(`${res.repo} (${res.branch}): ${res.detail}`);
}
}
syncChannel.appendLine(`[sync:${source}] done — ${ok} ok, ${skipped} skipped, ${failed} failed`);
await ctx.globalState.update(LAST_SYNC_KEY, new Date().toISOString());
// Subtle UX: silent success, noisy failure (grilled).
if (failed > 0) {
const detail = failures.slice(0, 3).join("; ") + (failures.length > 3 ? ` (+${failures.length - 3} more)` : "");
const pick = await vscode.window.showWarningMessage(
`Amicode sync: ${failed} repo(s) need attention — ${detail}`,
"Show log",
);
if (pick === "Show log") syncChannel.show();
}
};

const maybePromptSyncOptIn = async (): Promise<void> => {
if (isSyncEnabled()) return;
if (ctx.globalState.get<boolean>(SYNC_DISMISSED_KEY) === true) return;
// Only prompt if there's actually a workspace with git repos to sync.
const folders = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath);
if (findWorkspaceRepos(folders).length === 0) return;
const choice = await vscode.window.showInformationMessage(
"Keep your workspace in sync? Amicode can keep your git repos current overnight — fast-forwarding clean branches and keeping WIP up to date with main. Off by default, change anytime in Settings.",
"Enable sync",
"Not now",
"Don't ask again",
);
if (choice === "Enable sync") {
await vscode.workspace.getConfiguration("amicode").update("sync.enabled", true, vscode.ConfigurationTarget.Global);
void vscode.window.showInformationMessage("Amicode sync enabled — will run overnight and keep WIP current with main.");
// Run once now so the user sees it work.
void runWorkspaceSync("manual");
} else if (choice === "Don't ask again") {
await ctx.globalState.update(SYNC_DISMISSED_KEY, true);
}
};

// Register manual command.
ctx.subscriptions.push(vscode.commands.registerCommand("amicode.sync.now", () => void runWorkspaceSync("manual")));

// First-run nudge (fire-and-forget, like vault/julia prompts).
void maybePromptSyncOptIn();

// Missed-nightly check: if enabled and >20h since last sync, run within 2 min.
if (isSyncEnabled()) {
const last = ctx.globalState.get<string>(LAST_SYNC_KEY);
if (isSyncDue(last)) {
const delayMs = 30_000 + Math.floor(Math.random() * 90_000); // 30-120s jitter
syncChannel.appendLine(`[sync:startup] last sync ${last ?? "never"} — scheduling run in ${Math.round(delayMs / 1000)}s`);
const timer = setTimeout(() => void runWorkspaceSync("startup"), delayMs);
ctx.subscriptions.push({ dispose: () => clearTimeout(timer) });
} else {
syncChannel.appendLine(`[sync:startup] last sync ${last} — not due (interval ${Math.round(SYNC_INTERVAL_MS / 3600000)}h)`);
}
}

// Healthcheck (the real `amicode.healthcheck`): verify the managed Julia
// toolchain (Piccolo loads), the opencode server, and LLM creds. This is what
// the Julia-setup notification points users at to confirm setup worked.
Expand Down
Loading
Loading