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
84 changes: 80 additions & 4 deletions packages/extension/DISTILLER.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,27 @@ run_id = "r20260703-095831Z-e5b7"
New `-v<N+1>` ONLY when fidelity strictly improves on the card's
`best_fidelity` or the formulation materially changed. Otherwise no new binary.

### `KNOWLEDGE.md` line (insert newest-first; update in place on card update; cap 50 lines)
### `KNOWLEDGE.md` — FROZEN (read-only; superseded by the typed memory store)

`KNOWLEDGE.md` is **no longer written**. It stays readable for back-compat — the
session bootstrap still splices its existing lines as "Your recent problems" —
but the distiller does not append to it. Problem cards are still written to
`problems/` (the card frontmatter is the source of truth, Hard rule 7); only the
flat index is frozen. Durable *facts* now live in the typed memory store
(`memory/`, see below). Historical line shape, for reading only:

```markdown
- [x-gate-transmon](problems/x-gate-transmon.md) — transmon gate X, solved 8×,
best F=0.99995, pulse: x-gate-transmon-v1
- [cat-state-transmon-cavity](problems/cat-state-transmon-cavity.md) — cavity-transmon
state_prep, ATTEMPTED (launch failed: no solvespec), no pulse yet
```

On each run, make `KNOWLEDGE.md` carry a single migration pointer to the new
store — **append the line below ONLY if that exact line is not already present**
(this prompt runs on every distill, so the presence check is what keeps it
idempotent → zero diff on re-run; never append a second copy):

```markdown
> Superseded — durable memory now lives in `memory/` (index: `memory/MEMORY.md`).
```

### Onboarding materialization (only per Hard rule 4)
Expand Down Expand Up @@ -264,7 +278,10 @@ script: <relative path to the representative script>
sys_params: { fock_cutoff: 20, chi: 0.0000328, alpha: 2 } # if readable
```

DEMOS.md line:
DEMOS.md line — **FROZEN** (read-only; superseded by the typed memory store).
Keep writing the demo *card* to `demos/<slug>.md` as before; the distiller no
longer appends to `DEMOS.md` (the bootstrap still reads its existing lines as
"Reference demos"). Historical line shape, for reading only:

```
- [stanford-bosonics-cat](demos/stanford-bosonics-cat.md) — cavity state_prep cat-state, N_fock=20, script scripts/optimize_cat_alpha2.jl
Expand All @@ -274,6 +291,65 @@ Match/idempotency: a `demo` job for a `demo_dir` already carded (same slug) with
no change is a no-op. Demo cards use the same 3-tuple identity as problem cards
but never merge with the user's own solves (source distinguishes them).

## Typed memory store (durable facts) — `<vault>/amicode/memory/` (spec-20260707-002846 C4)

Beyond problem/demo cards (which capture *solves*), record durable **facts**
about the user and the work in a typed memory store — the write side of the
"Memory index" the session bootstrap splices. This SUPERSEDES the flat
`KNOWLEDGE.md`/`DEMOS.md` indices (now frozen). Keep it lean and
non-duplicative: a fact worth remembering across future sessions, not the state
of the current one.

Four types — pick the best fit:

- **user** — the user's role, goals, preferences, environment (who they are, how
they like to work). File: `memory/user_<topic>.md`.
- **feedback** — a correction the user gave OR an approach they confirmed ("do
X", "never Y", "yes, that was right"). Lead with the rule, then a **Why:** line
(the reason/incident) and a **How to apply:** line. File: `memory/feedback_<topic>.md`.
- **project** — an ongoing initiative, decision, deadline, or incident not
derivable from the artifacts or git history. File: `memory/project_<topic>.md`.
- **reference** — a pointer to where information lives outside the vault (a repo,
dashboard, or channel) and what it is for. File: `memory/reference_<topic>.md`.

Each card's frontmatter (then the body):

```markdown
---
name: <short-kebab-slug>
description: <one-line summary — used to judge relevance in future sessions>
type: user | feedback | project | reference
---

<body — for feedback/project, structure as: rule/fact, then **Why:** + **How to apply:**>
```

Maintain `memory/MEMORY.md` as the one-line index (this is the exact file the
session bootstrap reads and splices). One entry per card, newest-first, ≤~150
chars each, cap ~50 lines:

```markdown
- [user-role](user_role.md) — Aaron is CEO of Harmoniqs; frame for a senior IC
- [feedback-latex](feedback_latex.md) — use LaTeX math in chat, not just docs
```

Rules for the typed store (same discipline as problem cards):

- **Match before create.** Read `memory/MEMORY.md` and the frontmatter of every
file in `memory/` first. If a card already covers the fact, UPDATE it in place
(never write a second card, never a duplicate index line). Remove a card only
when the fact is explicitly retracted.
- **Idempotent.** Re-running any job must produce zero diff once a fact is
recorded — the MEMORY.md line is updated in place, appended only when new.
- **No secrets** (Hard rule 5) and **no fabrication** — record only facts the
job's artifacts/transcript actually establish.
- **Identity gate.** Only an `onboarding` job may write **user**-type identity
facts that mirror `PROFILE.md` (parity with Hard rule 4). `run`/`sweep`/
`batch`/`demo` jobs may record **feedback**/**project**/**reference** facts
they legitimately observe, but never touch `PROFILE.md` or user-identity cards.
- Writes stay under `<vault>/amicode/` → the pathspec-scoped commit (Hard rule 1)
and the existing grant already cover them; **no new permission is needed**.

## Finishing a job

1. Write the files. 2. Pathspec-scoped commit (Hard rule 1). 3. Final message:
Expand Down
5 changes: 5 additions & 0 deletions packages/extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
opencodeChannel.appendLine(`[boot] opencode project dir: ${opencodeProject.projectDir}`);
opencodeChannel.appendLine(`[boot] AGENTS.md: ${opencodeProject.agentsPath}`);
opencodeChannel.appendLine(`[boot] template: ${opencodeProject.templatePath}`);
opencodeChannel.appendLine(
`[boot] armonia mounts: ${opencodeProject.mounts.length} (${opencodeProject.mounts.map((m) => m.name).join(", ")})`,
);

// 4. Spawn opencode — the VENDORED binary by default (spec §4; S35, kills
// Assumption 4). Config override is a dev-only escape hatch. On a missing
Expand Down Expand Up @@ -221,6 +224,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
opencodeProject.skillPaths,
opencodeProject.skillsStageDir,
opencodeProject.vaultDir,
// Armonia mount stack (spec-20260707-002846 C1): per-mount read grants.
opencodeProject.mounts,
// Model pin (fallback-only, resolveModelPin): without it, default
// resolution gambles on provider ordering — with Google creds it
// picked a preview model that hung every headless/agent turn.
Expand Down
78 changes: 67 additions & 11 deletions packages/extension/src/opencode_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ import {
type SkillIndexEntry,
} from "./scores/package_skills";
import {
resolvePersonalVault,
defaultVaultsRoot,
readProfileMd,
readKnowledgeLines,
readDemoLines,
readMemoryIndexLines,
hasOnboardingCompleted,
onboardingDir,
} from "./substrate/vault_store";
import { buildAboutUserSection, buildRecentProblemsSection, buildReferenceDemosSection } from "./substrate/user_splice";
import { resolveMountStack, personalMount, type Mount, type MountStack } from "./substrate/mount_store";
import {
buildAboutUserSection,
buildRecentProblemsSection,
buildReferenceDemosSection,
buildMountStackSection,
buildMemoryIndexSection,
} from "./substrate/user_splice";

// ============================================================================
// Prepare a per-session opencode project directory.
Expand Down Expand Up @@ -291,6 +297,7 @@ export function buildOpencodeConfigContent(
skillPaths: string[] = [],
skillsStageDir: string = "",
vaultDir: string = "",
mounts: Mount[] = [],
modelPin?: string,
): string {
const templatesDir = path.dirname(templatePath);
Expand Down Expand Up @@ -333,6 +340,12 @@ export function buildOpencodeConfigContent(
[`${problemsRoot()}/**`]: "allow", // amicode_* problem workspaces the agent reads back
[`${scoresRoot}/**`]: "allow", // score templates + memory hooks ([Why?]) the agent reads
...skillGrants, // per-indexed-skill dirs (spec §3, least-privilege)
// Armonia mount stack (spec-20260707-002846 C1): a READ grant per mount
// so the agent can read cards/notes on demand across the WHOLE stack.
// The permission surface has no read/write split, so even a read-only
// mount gets a grant here (read posture); write discipline stays
// distiller-side (its own config), same contract as the vault grant below.
...Object.fromEntries(mounts.map((m) => [`${m.path}/**`, "allow"])),
// User-memory substrate (spec-20260705-002847 §6): the interview reads
// problem/environment cards on demand. Read-only BY CONTRACT — vault
// writes are distiller-only (its own config); the permission surface
Expand Down Expand Up @@ -362,9 +375,12 @@ export interface OpencodeConfigOptions {
platformSkills?: string[];
/** Roots for the central platform-skill library (spec §3). Default: DEFAULT_LIBRARY_ROOTS. */
skillLibraryRoots?: string[];
/** Personal vault dir for the user-memory substrate (spec-20260705-002847).
* undefined → auto-resolve (kind=personal marker scan under ~/.amico/vaults);
* "" → personalization disabled; a path → used as-is. */
/** Personal vault dir for the user-memory substrate (spec-20260705-002847),
* three-state (spec-20260707-002846 C1):
* undefined → auto-resolve the full Armonia mount stack under
* ~/.amico/vaults; vaultDir = the personal mount ("" if none);
* "" → personalization disabled (empty stack, no grants, no splice);
* a path → a single forced personal mount at that path (dev escape hatch). */
vaultDir?: string;
}

Expand All @@ -379,8 +395,13 @@ export interface OpencodeProject {
* buildOpencodeConfigContent as `skills.paths`. "" if none staged. */
skillsStageDir: string;
/** Resolved personal vault ("" when personalization is off) — thread into
* buildOpencodeConfigContent for the read grant. */
* buildOpencodeConfigContent for the read grant. Equals `personalMount(mounts)`
* path (unchanged behavior for the distiller + onboarding consumers). */
vaultDir: string;
/** The resolved Armonia mount stack (spec-20260707-002846 C1) — thread into
* buildOpencodeConfigContent for the per-mount read grants. [] when
* personalization is disabled ("" vaultDir). */
mounts: Mount[];
}

export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodeProject {
Expand All @@ -402,10 +423,27 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro
// transport for the Bun-side plugin. FALLBACK: any failure leaves the substituted
// AGENTS.md exactly as before — the hardcoded section IS the fallback content;
// score trouble must never brick the boot.
// User-memory substrate (spec-20260705-002847): resolve the personal vault
// ONCE, up front — the routing predicate (§3) and the splice (§6) both need
// it. undefined → auto-resolve (kind=personal marker scan); "" → off.
const vaultDir = opts.vaultDir !== undefined ? opts.vaultDir : resolvePersonalVault(defaultVaultsRoot(), "");
// Armonia mount stack (spec-20260707-002846 C1) + user-memory substrate
// (spec-20260705-002847): resolve ONCE, up front — the routing predicate (§3),
// the per-mount read grants, and the splice (§6, C3/C4) all need it. The
// three-state opts.vaultDir contract is preserved EXACTLY:
// undefined → auto-resolve the FULL stack (personal mount → vaultDir);
// "" → personalization OFF (empty stack, no grants, no splice);
// a path → a single forced personal mount at that path (dev escape hatch).
let stack: MountStack;
if (opts.vaultDir === undefined) {
stack = resolveMountStack();
} else if (opts.vaultDir === "") {
stack = { mounts: [], warnings: [] };
} else {
stack = {
mounts: [{ name: path.basename(opts.vaultDir), kind: "personal", path: opts.vaultDir, writable: true }],
warnings: [],
};
}
// vaultDir === the personal mount path (unchanged behavior for the distiller +
// onboarding predicate consumers); "" when there is no personal mount.
const vaultDir = personalMount(stack)?.path ?? "";

let finalContent = filled;
try {
Expand Down Expand Up @@ -517,6 +555,23 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro
}
}

// Mount-stack + memory-index splice (spec-20260707-002846 C3/C4 read side):
// its OWN try/catch — mount-parity trouble must never brick the boot. The
// mount-stack section renders whenever the stack has mounts (mounts can exist
// without a personal vault — e.g. a team-only stack); the typed-memory index
// is read from the personal mount, so it is gated on vaultDir. Empty stack
// ("" vaultDir) → both builders return "" → nothing is spliced.
try {
const mountSection = buildMountStackSection(stack);
if (mountSection) finalContent = finalContent + "\n\n" + mountSection;
if (vaultDir) {
const memorySection = buildMemoryIndexSection(readMemoryIndexLines(vaultDir));
if (memorySection) finalContent = finalContent + "\n\n" + memorySection;
}
} catch (e) {
console.warn(`amicode: mount-stack/memory-index splice failed (session continues): ${e}`);
}

fs.writeFileSync(agentsPath, finalContent, "utf8");

// The agent reads the template from its bundled absolute path (the session
Expand All @@ -528,5 +583,6 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro
skillPaths: skillEntries.map((e) => e.path),
skillsStageDir,
vaultDir,
mounts: stack.mounts,
};
}
Loading
Loading