Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
524d520
chore: open integration branch for #880 Research Environments
jeonghun-jj-lee Sep 7, 2026
96b1abf
feat: research environment schemas + CLI foundation (#881)
jeonghun-jj-lee Sep 7, 2026
240b124
feat: environment resolution module + detection update (#882)
jeonghun-jj-lee Sep 7, 2026
5782978
feat: system prompt context injection for research environments (#883)
jeonghun-jj-lee Sep 7, 2026
a1b18b6
feat: skill resolution includes environment skills (#888)
jeonghun-jj-lee Sep 7, 2026
58a98f4
feat: amico env promote — file promotion from project to environment …
jeonghun-jj-lee Sep 7, 2026
440de76
feat: sidebar environment pill data plumbing + color palette (#884)
jeonghun-jj-lee Sep 7, 2026
d564328
feat: composer environment subtitle data plumbing (#886)
jeonghun-jj-lee Sep 7, 2026
308e93b
feat: commands + context menu bridge messages for environments (#887)
jeonghun-jj-lee Sep 7, 2026
bf446e7
feat: autoresearch environment integration — digest, briefs, staging …
jeonghun-jj-lee Sep 7, 2026
fd18556
feat: nested environment tree data plumbing (#885)
jeonghun-jj-lee Sep 7, 2026
f8116cf
refactor: drop monorepo topology — multi-repo only (#880)
jeonghun-jj-lee Sep 7, 2026
4633b4c
refactor: enforce multi-repo topology — nesting guard + scaffold clea…
jeonghun-jj-lee Sep 7, 2026
8a01427
feat(amico-run): envBind verb — string-append bind, idempotent, force…
jeonghun-jj-lee Sep 7, 2026
ed2a8c6
feat(extension): wire bind-to-environment and promote-to-environment …
jeonghun-jj-lee Sep 7, 2026
a0c64ee
feat(extension): createNewEnvironment + 3 VS Code command handlers (n…
jeonghun-jj-lee Sep 7, 2026
7fb5718
feat(skills): create-research-environment — 6-stage interview, post-c…
jeonghun-jj-lee Sep 7, 2026
7c964a3
feat(skills): create-research-project Stage 8 — environment binding w…
jeonghun-jj-lee Sep 7, 2026
320f1a1
feat(skills): migrate-research-project Phase 6 — environment binding …
jeonghun-jj-lee Sep 7, 2026
b7a44f3
feat(session): add command parameter to amicode_session for reliable …
jeonghun-jj-lee Sep 8, 2026
a41b1e3
feat(sidebar): render environment pill on bound research project root…
jeonghun-jj-lee Sep 8, 2026
77e0cbc
fix: typecheck — cast through unknown for ProjectToml in envBind forc…
jeonghun-jj-lee Sep 8, 2026
d2007da
fix(test): set git identity in env_promote test for CI runners
jeonghun-jj-lee Sep 8, 2026
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
616 changes: 616 additions & 0 deletions packages/amico-run/src/env_verb.ts

Large diffs are not rendered by default.

199 changes: 199 additions & 0 deletions packages/amico-run/src/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// environment.ts — pure logic for research environment entities (issue #881).
//
// Schema definition, validation, slug generation, scaffolding data, TOML
// rendering, and registry parsing. NO filesystem I/O — that lives in
// env_verb.ts. This module is the unit-testable core.

import { parse as parseToml } from "smol-toml";

// ── schema types ────────────────────────────────────────────────────────────

export const CURRENT_ENV_SCHEMA_VERSION = 1;

export interface EnvironmentToml {
schema_version: number;
name: string;
slug: string;
created: string; // YYYY-MM-DD
description?: string;
tags?: string[];
domain?: { platform?: string; field?: string };
authors?: { lead?: string; collaborators?: string[] };
repo?: { remote?: string };
paths?: Record<string, string>;
}

export interface EnvironmentRegistryEntry {
slug: string;
path: string;
}

/** The prescribed directory layout for a Research Environment (PRD #880).
* Environments hold shared knowledge — not project-level work. Projects are
* always separate repos linked by [environment].slug. */
export const ENV_SCAFFOLD_DIRS = [
"insights",
"methods",
"context",
"literature",
"lib",
"templates",
"config",
] as const;

// ── validation ──────────────────────────────────────────────────────────────

export type ValidationResult =
| { ok: true }
| { ok: false; errors: string[] };

const REQUIRED_FIELDS: (keyof EnvironmentToml)[] = [
"schema_version",
"name",
"slug",
"created",
];

export function validateEnvironmentToml(data: unknown): ValidationResult {
if (typeof data !== "object" || data === null) {
return { ok: false, errors: ["research-environment.toml must be a TOML table (object)"] };
}

const obj = data as Record<string, unknown>;
const errors: string[] = [];

for (const field of REQUIRED_FIELDS) {
if (obj[field] === undefined || obj[field] === null) {
errors.push(`missing required field: ${field}`);
}
}

if (typeof obj.schema_version !== "undefined" && typeof obj.schema_version !== "number") {
errors.push("schema_version must be an integer");
}

if (
typeof obj.schema_version === "number" &&
obj.schema_version > CURRENT_ENV_SCHEMA_VERSION
) {
errors.push(
`schema_version ${obj.schema_version} exceeds current (${CURRENT_ENV_SCHEMA_VERSION}) — manifest is unreadable by this version`,
);
}

return errors.length === 0 ? { ok: true } : { ok: false, errors };
}

// ── slug generation ─────────────────────────────────────────────────────────

export function nameToSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/-{2,}/g, "-")
.replace(/^-+|-+$/g, "");
}

// ── TOML rendering ──────────────────────────────────────────────────────────

/** Render an EnvironmentToml to a TOML string. Hand-rendered for readability. */
export function renderEnvironmentToml(e: EnvironmentToml): string {
const lines: string[] = [];
lines.push(`schema_version = ${e.schema_version}`);
lines.push(`name = ${q(e.name)}`);
lines.push(`slug = ${q(e.slug)}`);
lines.push(`created = ${q(e.created)}`);

if (e.description) {
lines.push(`description = ${q(e.description)}`);
}

if (e.tags && e.tags.length > 0) {
lines.push(`tags = [${e.tags.map(q).join(", ")}]`);
}

if (e.domain) {
lines.push("");
lines.push("[domain]");
if (e.domain.platform) lines.push(`platform = ${q(e.domain.platform)}`);
if (e.domain.field) lines.push(`field = ${q(e.domain.field)}`);
}

if (e.authors) {
lines.push("");
lines.push("[authors]");
if (e.authors.lead) lines.push(`lead = ${q(e.authors.lead)}`);
if (e.authors.collaborators && e.authors.collaborators.length > 0) {
lines.push(`collaborators = [${e.authors.collaborators.map(q).join(", ")}]`);
}
}

if (e.repo) {
lines.push("");
lines.push("[repo]");
if (e.repo.remote) lines.push(`remote = ${q(e.repo.remote)}`);
}

if (e.paths && Object.keys(e.paths).length > 0) {
lines.push("");
lines.push("[paths]");
for (const [key, value] of Object.entries(e.paths)) {
lines.push(`${key} = ${q(value)}`);
}
}

lines.push(""); // trailing newline
return lines.join("\n");
}

function q(s: string): string {
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
}

// ── registry parsing ────────────────────────────────────────────────────────

/** Parse the `~/.amico/environments.toml` registry. */
export function parseEnvironmentRegistry(toml: string): EnvironmentRegistryEntry[] {
if (!toml.trim()) return [];

let parsed: Record<string, unknown>;
try {
parsed = parseToml(toml) as Record<string, unknown>;
} catch {
return [];
}

const envs = parsed.environments;
if (!Array.isArray(envs)) return [];

const entries: EnvironmentRegistryEntry[] = [];
for (const entry of envs) {
if (
typeof entry === "object" &&
entry !== null &&
typeof (entry as Record<string, unknown>).slug === "string" &&
typeof (entry as Record<string, unknown>).path === "string"
) {
entries.push({
slug: (entry as Record<string, unknown>).slug as string,
path: (entry as Record<string, unknown>).path as string,
});
}
}

return entries;
}

/** Render an environment registry to TOML. */
export function renderEnvironmentRegistry(entries: EnvironmentRegistryEntry[]): string {
if (entries.length === 0) return "";

const lines: string[] = [];
for (const entry of entries) {
lines.push("[[environments]]");
lines.push(`slug = ${q(entry.slug)}`);
lines.push(`path = ${q(entry.path)}`);
lines.push("");
}
return lines.join("\n");
}
11 changes: 11 additions & 0 deletions packages/amico-run/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface ProjectToml {
related_projects?: string[];
doi?: string;
};
environment?: {
slug: string;
path?: string;
};
}

// ── validation ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -167,6 +171,13 @@ export function renderProjectToml(p: ProjectToml): string {
if (p.links.doi) lines.push(`doi = ${q(p.links.doi)}`);
}

if (p.environment) {
lines.push("");
lines.push("[environment]");
lines.push(`slug = ${q(p.environment.slug)}`);
if (p.environment.path) lines.push(`path = ${q(p.environment.path)}`);
}

lines.push(""); // trailing newline
return lines.join("\n");
}
Expand Down
13 changes: 13 additions & 0 deletions packages/amico-run/src/verbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { planVerb } from "./plan_verb.js";
import { handoffVerb } from "./handoff_verb.js";
import { campaignVerb } from "./campaign_verb.js";
import { projectVerb } from "./project_verb.js";
import { envVerb } from "./env_verb.js";
import { sessionsVerb } from "./sessions_verb.js";
import { sotaVerb } from "./sota_verb.js";
export interface VerbResult {
Expand Down Expand Up @@ -263,6 +264,17 @@ const sota: Verb = {
run: sotaVerb,
};

// env — the research-environment entity: create (scaffold + git init +
// registry) and register (add an existing environment to the local registry).
// Part of #881 (sub-issue of #880 Research Environments).
const env: Verb = {
name: "env",
summary: "create a scaffolded research environment / register an existing one in the local registry",
generalizes: "the amicode research-environment entity lifecycle (PRD #880)",
slice: "research environments (#881)",
run: envVerb,
};

export const SPINE_VERBS: Verb[] = [
catalog,
vault,
Expand All @@ -277,6 +289,7 @@ export const SPINE_VERBS: Verb[] = [
papers,
campaign,
project,
env,
sessions,
sota,
];
Loading
Loading