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
13 changes: 13 additions & 0 deletions packages/extension/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,16 @@ answers like a well-written engineering doc, not a terminal log:
reader's next move, not by compressing the prose.
- **LaTeX for all math.** $\hat H$, $\Omega_{\max}$, $F = 0.9982$ — inline or
display — never ASCII approximations.

> **Live context — solver mode + routing, fleet, profile, recent problems, reference
> demos, mount stack, memory index — is injected into every session by the
> amicode_context plugin.** If you do not see a `## Stack state (live)`, `## Fleet
> (live)`, `## About this user`, or `## Memory index` block anywhere in this prompt,
> read the state directly before acting:
> - solver mode + routing: `~/.amico/amicode/solver-mode.json` and
> `~/.amico/connections.json` — especially before setting `tier` or `executor`
> in a solvespec.
> - fleet: `~/.amico/ops/fleet/fleet.json` and `~/.amico/ops/fleet-status.json`.
> - profile, problems, demos, mounts, memory: the personal Armonia mount (first
> `kind = "personal"` dir under `~/.amico/vaults/`) — its `amicode/PROFILE.md`,
> `amicode/KNOWLEDGE.md`, `amicode/DEMOS.md`, and `amicode/memory/MEMORY.md`.
36 changes: 36 additions & 0 deletions packages/extension/opencode-plugin/amicode_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// ============================================================================
// amicode_context — an opencode plugin that injects live stack-state context
// (solver mode, routing, active problem, live runs) into every system prompt
// via the `experimental.chat.system.transform` hook.
//
// RUNTIME: same constraints as amicode_tools.ts — executes inside opencode's
// embedded Bun runtime, registered by absolute path via OPENCODE_CONFIG_CONTENT
// `plugin: ["<abs>"]`. Exactly ONE export (the legacy-plugin scan constraint).
// All imports are sibling modules using node: builtins only.
//
// This is a SECOND plugin file alongside amicode_tools.ts; it is registered as
// a separate entry in the `plugin` array and operates independently from the
// tool pack. The split keeps the tested tool pack untouched and respects the
// single-export constraint.
// ============================================================================

import { buildStackStateBlock } from "./stack_state";

console.error("[amicode-context] loaded — stack-state injection plugin (experimental.chat.system.transform)");

export const AmicodeContext = async () => ({
"experimental.chat.system.transform": (
_input: { sessionID?: string; model?: string },
output: { system: string[] },
): void => {
try {
const block = buildStackStateBlock();
if (block) {
output.system.push(block);
}
} catch (e) {
console.error(`[amicode-context] buildStackStateBlock failed: ${e instanceof Error ? e.message : String(e)}`);
// Never throw — a failing hook must not break the prompt build.
}
},
});
608 changes: 608 additions & 0 deletions packages/extension/opencode-plugin/stack_state.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
// Telemetry gate → experimental.openTelemetry (span generation), coupled
// to the exporter env this same spawnEnv resolves.
telemetryOpen(),
// Context plugin: injects live stack state (solver mode, routing,
// active problem, live runs) per system-prompt build.
[path.resolve(ctx.extensionPath, "opencode-plugin", "amicode_context.ts")],
),
}),
channel: opencodeChannel,
Expand Down Expand Up @@ -765,6 +768,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
skillRoots: cfgArr("skillRoots"),
skillLibraryRoots: cfgLibraryRoots(),
vaultDir: vscode.workspace.getConfiguration("amicode").get<string>("vaultDir", "") || undefined,
projectDir: path.join((ctx.storageUri ?? ctx.globalStorageUri).fsPath, "opencode-project"),
});
ChatPanel.setBugReportAvailable(bugReportSkillStaged(project2.skillPaths)); // #250 AC5
await serverManager?.stop();
Expand All @@ -789,6 +793,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
// Same pin rule as boot: only an explicit amicode.defaultModel pins.
vscode.workspace.getConfiguration("amicode").get<string>("defaultModel", "").trim() || resolveModelPin(),
telemetryOpen(), // gate → experimental.openTelemetry (span generation)
// Context plugin: injects live stack state per system-prompt build.
[path.resolve(ctx.extensionPath, "opencode-plugin", "amicode_context.ts")],
),
}),
channel: opencodeChannel,
Expand Down
69 changes: 21 additions & 48 deletions packages/extension/src/opencode_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,10 @@ import { studioPathsOrLegacy } from "@amicode/schema";
import { buildRoutingSection, readRoutingContext } from "./routing";
import {
readProfileMd,
readKnowledgeLines,
readDemoLines,
readMemoryIndexLines,
hasOnboardingCompleted,
onboardingDir,
} from "./substrate/vault_store";
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 @@ -410,6 +400,11 @@ export function buildOpencodeConfigContent(
* When false we OMIT the key entirely (rather than force it false) so a user's
* own global `experimental.openTelemetry` is never clobbered by the deep-merge. */
telemetryOpen: boolean = false,
/** Additional plugin paths to register alongside pluginPath. Each entry is an
* absolute path to a .ts plugin file. Used to register the amicode_context
* plugin (experimental.chat.system.transform hook) without touching the
* single-export amicode_tools pack. */
extraPluginPaths: string[] = [],
): string {
const templatesDir = path.dirname(templatePath);
// Least-privilege read grants for the skill index (spec §3): each indexed
Expand All @@ -434,7 +429,7 @@ export function buildOpencodeConfigContent(
default_agent: "plan",
...(modelPin ? { model: modelPin } : {}),
instructions: [agentsPath],
plugin: [pluginPath],
plugin: [pluginPath, ...extraPluginPaths],
...(skills ? { skills } : {}),
// Enable AI-SDK span generation ONLY behind the telemetry gate — deep-merges
// into cfg.experimental alongside any user keys (see telemetryOpen above).
Expand Down Expand Up @@ -669,7 +664,11 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro
console.warn(`amicode: skill index failed (session continues without it): ${e}`);
}

fs.writeFileSync(agentsPath, finalContent + solverModeSection() + routingSection(), "utf8");
// Solver mode, routing, fleet, and the user-memory sections are injected
// per-prompt by the amicode_context plugin (see the recovery pointer the
// template carries at its tail). Early safe write in case a later step
// throws — the second write below is the authoritative one.
fs.writeFileSync(agentsPath, finalContent, "utf8");

// spec C: write the authoring.json seam amico-run reads (allowlist resolved
// from the same entitlements the score filter used + the bundled asset paths).
Expand All @@ -690,42 +689,16 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro
fs.rmSync(path.join(projectDir, "skills"), { recursive: true, force: true });
const skillsStageDir = stageOpencodeSkills(path.join(projectDir, "skills"), skillEntries);

// User-memory splice (spec-20260705-002847 §6): About-this-user + Your-recent-
// problems, appended to the compiled content — own try/catch, personalization
// trouble must never brick the boot. `vaultDir` was resolved up front (above).
// When we routed to the overture (no profile yet) these read empty and add
// nothing — correct: there is no memory to splice on the very first session.
if (vaultDir) {
try {
const about = buildAboutUserSection(readProfileMd(vaultDir));
const recent = buildRecentProblemsSection(readKnowledgeLines(vaultDir));
const demos = buildReferenceDemosSection(readDemoLines(vaultDir)); // L1 §3
for (const section of [about, recent, demos]) {
if (section) finalContent = finalContent + "\n\n" + section;
}
} catch (e) {
console.warn(`amicode: user-memory splice failed (session continues unpersonalized): ${e}`);
}
}

// 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 + solverModeSection() + routingSection(), "utf8");
// Solver mode, routing, fleet, and the user-memory sections (profile,
// recent problems, reference demos, mount stack, memory index) are injected
// per-prompt by the amicode_context plugin (experimental.chat.system.transform
// hook), read LIVE from disk — a distiller write or a solver switch reaches
// the next message without a re-prep or restart (the boot-time file splices
// these replaced went stale between sessions; spec-20260705-002847 §6 and
// spec-20260707-002846 C3/C4 read side moved to the live hook). The
// recovery pointer the template carries at its tail tells the agent where
// to read the state directly if the hook ever fails silently.
fs.writeFileSync(agentsPath, finalContent, "utf8");

// The agent reads the template from its bundled absolute path (the session
// cwd is the workspace, not this temp dir — so no copy is made here).
Expand Down
94 changes: 0 additions & 94 deletions packages/extension/src/substrate/user_splice.ts

This file was deleted.

48 changes: 7 additions & 41 deletions packages/extension/src/substrate/vault_store.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
/** Vault resolution + user-memory readers (spec-20260705-002847 §2, §3 routing).
*
* The personal vault is the first mount under the vaults root whose
* `.amico-vault.toml` marker declares `kind = "personal"`. Everything here is
* read-only and failure-tolerant: a missing vault, file, or stream simply
/** Vault resolution + onboarding-stream readers (spec-20260705-002847 §2, §3
* routing). The user-memory section BUILDERS + index readers (KNOWLEDGE /
* DEMOS / memory index) moved to the amicode_context plugin's live
* stack_state.ts (injected per-prompt); what remains here is what prep-time
* code still needs — the personal-vault resolver, PROFILE.md presence (the
* onboarding routing predicate), and the onboarding-stream marker. Everything
* is read-only and failure-tolerant: a missing vault, file, or stream simply
* yields the empty value and the session proceeds unpersonalized. */
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";

export const KNOWLEDGE_LINE_CAP = 50;
export const MEMORY_INDEX_LINE_CAP = 50;

export function defaultVaultsRoot(): string {
return path.join(os.homedir(), ".amico", "vaults");
}
Expand Down Expand Up @@ -55,39 +54,6 @@ export function readProfileMd(vaultDir: string): string {
}
}

/** List-item lines from an amicode index file, capped. */
function readIndexLines(vaultDir: string, file: string, cap: number): string[] {
let text: string;
try {
text = fs.readFileSync(path.join(vaultDir, "amicode", file), "utf8");
} catch {
return [];
}
return text
.split("\n")
.filter((l) => l.startsWith("- "))
.slice(0, cap);
}

/** KNOWLEDGE.md list-item lines, capped (§2.3). */
export function readKnowledgeLines(vaultDir: string, cap: number = KNOWLEDGE_LINE_CAP): string[] {
return readIndexLines(vaultDir, "KNOWLEDGE.md", cap);
}

/** DEMOS.md list-item lines (L1 §3) — separate index so reference demos never
* age against KNOWLEDGE.md's problem cap. Capped tighter (splice budget ≤~2KB). */
export function readDemoLines(vaultDir: string, cap = 30): string[] {
return readIndexLines(vaultDir, "DEMOS.md", cap);
}

/** Typed-memory index list lines (spec-20260707-002846 C4). The distiller writes
* durable facts as typed cards under `<vault>/amicode/memory/` and maintains a
* one-line index at `memory/MEMORY.md`; only that index is spliced (the cards
* load on demand). Subdir-capable reuse of the readIndexLines pattern. */
export function readMemoryIndexLines(vaultDir: string, cap: number = MEMORY_INDEX_LINE_CAP): string[] {
return readIndexLines(vaultDir, path.join("memory", "MEMORY.md"), cap);
}

/** Second disjunct of the routing predicate (§3): completed marker in the
* onboarding stream. Malformed lines are skipped. */
export function hasOnboardingCompleted(onboardingStreamDir: string): boolean {
Expand Down
2 changes: 2 additions & 0 deletions packages/extension/test/packaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const REQUIRED = [
// amicode_* plugin (Bun-transpiled .ts, loaded by absolute path) — every sibling
// is load-bearing: a dropped file silently reverts the session to vanilla opencode.
"extension/opencode-plugin/amicode_tools.ts",
"extension/opencode-plugin/amicode_context.ts", // live stack-state injection plugin (system.transform hook)
"extension/opencode-plugin/stack_state.ts", // its sibling readers/builders (imported by amicode_context)
"extension/opencode-plugin/entities.ts",
"extension/opencode-plugin/problems.ts",
"extension/opencode-plugin/hashes.ts",
Expand Down
Loading
Loading