From 4a081181df14a8db1512169198659f15d0325d96 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 13:25:22 -0400 Subject: [PATCH 01/22] chore: integration branch for Research Projects (#663) Parent: https://github.com/harmoniqs/amicode/issues/663 Sub-issues: #664, #665, #666, #667, #668, #669, #670 From 1dce8b5e0c7f5bfd9c7ba73679adb4b174c063b8 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 13:28:42 -0400 Subject: [PATCH 02/22] =?UTF-8?q?docs:=20glossary=20updates=20+=20ADR=2000?= =?UTF-8?q?12=20=E2=80=94=20Research/Dev=20Project=20flavors=20(#664)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expand Project entry in CONTEXT.md with Research Project and Dev Project as flavor definitions (additive, no breaking change) - Clarify Campaign entry: campaigns are distinct from Sessions; ledger location is project-aware - Add 'Study' to the Project avoid-list (rejected alternative) - Commit ADR 0012 (Research Projects as a structured flavor of Project) Closes #664 --- CONTEXT.md | 11 +++--- docs/adr/0012-research-projects.md | 54 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 docs/adr/0012-research-projects.md diff --git a/CONTEXT.md b/CONTEXT.md index 549f45b6..3c9be23c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -29,8 +29,11 @@ _Avoid_: User, tenant, owner (interchangeably) ### Work organization **Project**: -The unit of work organization — a directory registered with amicode where files, solves, and Sessions live. The canonical default Project is `~/armonia/` (the armonia workspace created by bootstrap-armonia.sh); the agent session cwd resolves there unless overridden. Every Session belongs to exactly one Project. -_Avoid_: Workspace, folder (as a concept name), repo +The unit of work organization — a directory registered with amicode where files, solves, and Sessions live. Every Session belongs to exactly one Project. Two flavors: +- **Research Project** — identified by a `project.toml` manifest at its root. Prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. +- **Dev Project** — the existing git-repo model; any registered directory without `project.toml`. Canonical default: `~/armonia/`. + +_Avoid_: Workspace, folder (as a concept name), repo, Study (rejected alternative — researchers think in "projects") **Session**: One agent conversation, bound to exactly one Project at creation and never re-parented. Sessions are children of a Project — surfaced nested under their Project, never as a global flat list. @@ -68,8 +71,8 @@ The development mode: issue DAG → TDD slices → CI/review → landed delta _Avoid_: autobuild ("build" already means CI to everyone) **Campaign**: -One bounded run of either autonomous mode, with a ledger and a closing artifact — the umbrella word for what a director executes. Copilot sessions are not campaigns; campaign-internal state (receipts, dispatch logs, scratch) crosses a campaign boundary only by distilling into issues, vault cards, or the artifact banks. -_Avoid_: session (a copilot session is never a campaign) +One bounded run of either autonomous mode, with a ledger and a closing artifact — the umbrella word for what a director executes. Copilot sessions are not campaigns; campaign-internal state (receipts, dispatch logs, scratch) crosses a campaign boundary only by distilling into issues, vault cards, or the artifact banks. Within a Research Project, campaign ledgers live at `ledger/campaigns/campaign--.md`; outside a project, they live in the personal vault's `sessions/` directory. +_Avoid_: session (a copilot session is never a campaign; a campaign ledger is never a session ledger) **Gate pack**: The typed set of gates + phase templates an autonomous mode binds — the entire mode-specific part of the loop, held as committed data rather than prose, so the same director core runs any pack. diff --git a/docs/adr/0012-research-projects.md b/docs/adr/0012-research-projects.md new file mode 100644 index 00000000..aec68735 --- /dev/null +++ b/docs/adr/0012-research-projects.md @@ -0,0 +1,54 @@ +# ADR 0012: Research Projects as a structured flavor of Project + +**Status:** proposed + +**Date:** 2026-08-31 + +## Context + +"Project" in Amicode means "a directory registered with amicode" — a workstation root (canonical default: `~/armonia/`). Researchers using Amicode organically create git repos for investigations (`diraq-esr-demo`, `fluxonium-demo`) but these lack prescribed structure, manifests, or autoresearch integration. Problem workspaces (`~/.amico/problems/`) are domain-pack-specific (pulse design only) and nearly unused in practice; the autoresearch loop operates through vault session ledgers, completely disconnected from any project structure. The project selector UI (`PromptProjectSelector`, 595 lines) is fully implemented but hidden in the Amicode webview behind an `!inAmicode()` gate. External users have no concept of armonia. + +## Decision + +Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `project.toml`, with a prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." + +### Alternatives considered + +- **(B) Full redefine** — make "Project" mean only the research entity; rename the old concept to "Workspace." Rejected: breaks the glossary across CONTEXT.md, 4+ ADRs, the entire opencode session model, and the app UI. Migration cost outweighs the cleaner naming. +- **(C) New "Study" entity** — add a separate entity alongside the existing Project. Rejected: researchers think in "projects," not "studies." Two overlapping concepts creates permanent confusion. + +### Key structural decisions + +- **Workspace-backed selector.** The project selector is un-gated in the Amicode webview, shows workspace folders typed by `project.toml` presence, and grouped into Research and Development sections. Adding a project adds it to the VS Code workspace. Add-only; removal uses VS Code's native UI. +- **Project-specific skills.** Each research project can ship a `skills/` directory. On workspace folder changes, the extension scans for `project.toml` and adds each project's `skills/` as a skill source, triggering a `prepareOpencodeProject()` re-run. Merge priority: project > custom > workspace > shipped. +- **Gated paper-writing.** A shipped `paper-writer` skill enforces an outline-first gate: the agent refuses to write paper content until `paper/outline.md` is finalized by the user. Formatting proceeds section-by-section with user approval. Content provenance is maintained (every paragraph traces to an outline bullet). The agent does not generate novel claims, interpret results, or write the abstract. +- **Campaign ledgers.** Autoresearch execution records use `ledger/campaigns/` within the project (not vault `sessions/`), avoiding collision with the Session glossary term (one agent conversation). +- **Armonia untouched for now.** Armonia's current CONTEXT.md definition, ArmoniaService, and sidebar panel remain as-is. The future vision is to reposition Armonia as a multi-collaborator coordination layer, but that is a separate decision gated on this one succeeding. + +## Consequences + +### What changes + +- CONTEXT.md gains Research Project and Dev Project as flavor definitions under Project +- `ProjectTable` gains `project.toml` awareness for type detection +- `PromptProjectSelector` is un-gated in the Amicode webview, enriched with type grouping and research metadata +- Skill merge chain extended with project source: project > custom > workspace > shipped +- `prepareOpencodeProject()` triggered on workspace folder changes (project add/remove) +- Autoresearch director reads the research project directory as its context source +- Campaign ledgers at `ledger/campaigns/`, not vault `sessions/` +- Problem workspaces continue for the copilot interview but are not part of the research project layout +- Multi-root workspaces supported; session binding is explicit via the selector chip, immutable after creation +- Paper writing is gated behind user-authored, user-finalized outlines; the agent is a typesetter, not a co-author + +### Cross-repo impact + +Skill auto-loading and session binding require changes to the opencode engine (vendored fork), which ships on its own release cadence. These are cross-repo changes that need coordinated PRs. + +### Risks accepted + +- **Intermediate skill visibility:** Until per-session skill scoping is implemented, all project skills from all open projects are visible to all sessions regardless of binding. This is cosmetic — it doesn't cause incorrect behavior, but it means session binding is weaker than it appears. +- **Soft behavioral gates:** The paper-writer skill's content-provenance and forbidden-actions rules are LLM instructions, not code. They work as well as the model's instruction-following allows. + +### Reversibility + +If Research Projects don't get adoption, the rollback is low-cost: `project.toml` is additive (ignore it and the directory is a regular git repo), the selector enrichment is behind the existing `!inAmicode()` gate (re-enable it), and the skill auto-loading is a no-op when no projects have `skills/`. The CONTEXT.md glossary additions are the hardest to undo but are also the lowest-risk (they extend, not replace). Armonia's current definition is untouched by this decision. From 5c7dcfb9cfa96ce5765ad537b2145735098cf547 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 13:36:34 -0400 Subject: [PATCH 03/22] =?UTF-8?q?feat:=20entity=20schema=20+=20scaffolding?= =?UTF-8?q?=20CLI=20=E2=80=94=20amico=20project=20create/import=20(#665)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Define ProjectToml schema with validation (schema_version, name, slug, question, status, created, tags, authors, venue, domain_pack, links) - Implement nameToSlug() for deterministic kebab-case slug generation - Scaffold full Research Project layout: scripts/, data/{raw,processed,plots}, analysis/, paper/{outline.md,main.tex,references.bib,latexmkrc,.gitignore, figures/,supplementary/}, ledger/{hypotheses,observations,literature, campaigns}, config/{system.toml,lab.toml}, skills/ - Venue-aware LaTeX templates: PRL/PRX → REVTeX, others → minimal article - paper/outline.md scaffolded with YAML frontmatter (status: draft) - Root .gitignore: jld2/hdf5/h5 + Python + .DS_Store - amico project create : scaffold + git init + initial commit - amico project import []: non-destructive scaffold over existing dir - Both commands idempotent (safe to re-run) - Register 'project' as spine verb in verbs.ts - 38 tests: pure logic (schema, slug, scaffold, templates) + integration (create, import, idempotent, venue-aware) Closes #665 --- packages/amico-run/src/project.ts | 383 ++++++++++++++++++ packages/amico-run/src/project_verb.ts | 263 ++++++++++++ packages/amico-run/src/verbs.ts | 13 +- packages/amico-run/test/project_verb.test.ts | 400 +++++++++++++++++++ 4 files changed, 1058 insertions(+), 1 deletion(-) create mode 100644 packages/amico-run/src/project.ts create mode 100644 packages/amico-run/src/project_verb.ts create mode 100644 packages/amico-run/test/project_verb.test.ts diff --git a/packages/amico-run/src/project.ts b/packages/amico-run/src/project.ts new file mode 100644 index 00000000..3e9199b2 --- /dev/null +++ b/packages/amico-run/src/project.ts @@ -0,0 +1,383 @@ +// project.ts — pure logic for research project entities (issue #665). +// +// Schema definition, validation, slug generation, scaffolding, and template +// rendering. NO filesystem I/O — that lives in project_verb.ts. This module +// is the unit-testable core. + +// ── schema types ──────────────────────────────────────────────────────────── + +export const PROJECT_STATUSES = [ + "proposing", + "designing", + "running", + "analyzing", + "writing", + "complete", +] as const; + +export type ProjectStatus = (typeof PROJECT_STATUSES)[number]; + +export interface ProjectToml { + schema_version: number; + name: string; + slug: string; + question: string; + status: ProjectStatus; + created: string; // YYYY-MM-DD + tags?: string[]; + authors?: { + lead?: string; + collaborators?: string[]; + }; + venue?: { + name?: string; + deadline?: string; + }; + domain_pack?: { + name?: string; + }; + links?: { + related_projects?: string[]; + doi?: string; + }; +} + +// ── validation ────────────────────────────────────────────────────────────── + +export type ValidationResult = + | { ok: true } + | { ok: false; errors: string[] }; + +const REQUIRED_FIELDS: (keyof ProjectToml)[] = [ + "schema_version", + "name", + "slug", + "question", + "status", + "created", +]; + +export function validateProjectToml(data: unknown): ValidationResult { + if (typeof data !== "object" || data === null) { + return { ok: false, errors: ["project.toml must be a TOML table (object)"] }; + } + + const obj = data as Record; + 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.status === "string" && !(PROJECT_STATUSES as readonly string[]).includes(obj.status)) { + errors.push( + `invalid status "${obj.status}" — must be one of: ${PROJECT_STATUSES.join(", ")}`, + ); + } + + 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, ""); +} + +// ── scaffolding data ──────────────────────────────────────────────────────── + +/** The prescribed directory layout for a Research Project (PRD #663). */ +export const SCAFFOLD_DIRS = [ + "scripts", + "scripts/testbed", + "data/raw", + "data/processed", + "data/plots", + "analysis", + "paper/figures", + "paper/supplementary", + "ledger/hypotheses", + "ledger/observations", + "ledger/literature", + "ledger/campaigns", + "config", + "skills", +] as const; + +// ── TOML rendering ────────────────────────────────────────────────────────── + +/** Render a ProjectToml to a TOML string. We hand-render to control section + * ordering and comments — smol-toml's stringify would work but produces less + * readable output for a manifest the user will edit. */ +export function renderProjectToml(p: ProjectToml): string { + const lines: string[] = []; + lines.push(`schema_version = ${p.schema_version}`); + lines.push(`name = ${q(p.name)}`); + lines.push(`slug = ${q(p.slug)}`); + lines.push(`question = ${q(p.question)}`); + lines.push(`status = ${q(p.status)}`); + lines.push(`created = ${q(p.created)}`); + + if (p.tags && p.tags.length > 0) { + lines.push(`tags = [${p.tags.map(q).join(", ")}]`); + } + + if (p.authors) { + lines.push(""); + lines.push("[authors]"); + if (p.authors.lead) lines.push(`lead = ${q(p.authors.lead)}`); + if (p.authors.collaborators && p.authors.collaborators.length > 0) { + lines.push(`collaborators = [${p.authors.collaborators.map(q).join(", ")}]`); + } + } + + if (p.venue) { + lines.push(""); + lines.push("[venue]"); + if (p.venue.name) lines.push(`name = ${q(p.venue.name)}`); + if (p.venue.deadline) lines.push(`deadline = ${q(p.venue.deadline)}`); + } + + if (p.domain_pack) { + lines.push(""); + lines.push("[domain_pack]"); + if (p.domain_pack.name) lines.push(`name = ${q(p.domain_pack.name)}`); + } + + if (p.links) { + lines.push(""); + lines.push("[links]"); + if (p.links.related_projects && p.links.related_projects.length > 0) { + lines.push(`related_projects = [${p.links.related_projects.map(q).join(", ")}]`); + } + if (p.links.doi) lines.push(`doi = ${q(p.links.doi)}`); + } + + lines.push(""); // trailing newline + return lines.join("\n"); +} + +function q(s: string): string { + return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +} + +// ── template content ──────────────────────────────────────────────────────── + +/** Venue → LaTeX document class mapping. */ +export function venueToDocumentClass(venue?: string): "revtex" | "minimal" { + if (!venue) return "minimal"; + const v = venue.toLowerCase(); + if (v.includes("physical review letters") || v === "prl") return "revtex"; + if (v.includes("physical review x") || v === "prx") return "revtex"; + return "minimal"; +} + +export function renderMainTex(name: string, venue?: string): string { + const cls = venueToDocumentClass(venue); + if (cls === "revtex") { + return `\\documentclass[aps,prl,twocolumn,superscriptaddress]{revtex4-2} +\\usepackage{amsmath,amssymb,graphicx,hyperref} + +\\begin{document} + +\\title{${texEscape(name)}} +\\author{TODO} +\\affiliation{TODO} + +\\begin{abstract} +TODO +\\end{abstract} + +\\maketitle + +\\section{Introduction} +\\label{sec:intro} + +% TODO + +\\bibliography{references} + +\\end{document} +`; + } + return `\\documentclass[11pt]{article} +\\usepackage{amsmath,amssymb,graphicx,hyperref} + +\\title{${texEscape(name)}} +\\author{TODO} +\\date{\\today} + +\\begin{document} +\\maketitle + +\\begin{abstract} +TODO +\\end{abstract} + +\\section{Introduction} +\\label{sec:intro} + +% TODO + +\\bibliographystyle{plain} +\\bibliography{references} + +\\end{document} +`; +} + +function texEscape(s: string): string { + return s.replace(/[&%$#_{}~^\\]/g, (c) => `\\${c}`); +} + +export function renderOutlineMd(name: string, question: string, venue?: string): string { + const cls = venueToDocumentClass(venue); + const header = `--- +status: draft +last_reviewed: +sections_approved: [] +--- + +# ${name} + +**Research question:** ${question} + +`; + if (cls === "revtex") { + return `${header}## Outline (PRL/PRX compressed format) + +### Abstract +- [ ] TODO + +### Introduction +- [ ] TODO + +### Results +- [ ] TODO + +### Discussion +- [ ] TODO + +### Methods +- [ ] TODO + +### Supplementary +- [ ] TODO +`; + } + return `${header}## Outline + +### Abstract +- [ ] TODO + +### Introduction +- [ ] TODO + +### Methods +- [ ] TODO + +### Results +- [ ] TODO + +### Discussion +- [ ] TODO + +### Conclusion +- [ ] TODO +`; +} + +export function renderReadme(name: string, question: string): string { + return `# ${name} + +${question} + +## Layout + +- \`scripts/\` — experiment scripts (\`testbed/\` for scratch work) +- \`data/\` — raw, processed, and plot outputs +- \`analysis/\` — analysis notebooks and post-processing +- \`paper/\` — manuscript (\`outline.md\` → \`main.tex\`) +- \`ledger/\` — hypotheses, observations, literature, and campaign logs +- \`config/\` — system and lab configuration +- \`skills/\` — project-specific Amico skills +`; +} + +export const ROOT_GITIGNORE = `# Data artifacts (large binary files) +data/raw/**/*.jld2 +data/raw/**/*.hdf5 +data/raw/**/*.h5 + +# Python +*.pyc +__pycache__/ + +# OS +.DS_Store +`; + +export const PAPER_GITIGNORE = `# LaTeX build artifacts +*.aux +*.bbl +*.blg +*.log +*.out +*.toc +*.synctex.gz +*.fdb_latexmk +*.fls +*.pdf +`; + +export const PAPER_LATEXMKRC = `$pdflatex = 'pdflatex -interaction=nonstopmode %O %S'; +`; + +export const SCRIPTS_README = `# Scripts + +Experiment scripts for this project. Use \`testbed/\` for scratch work and quick experiments. +`; + +/** Build the full scaffold manifest: relative paths and their contents. Files + * with `null` content are directories (created with mkdirSync). */ +export function scaffoldManifest( + p: ProjectToml, +): { path: string; content: string | null }[] { + const items: { path: string; content: string | null }[] = []; + + // directories + for (const dir of SCAFFOLD_DIRS) { + items.push({ path: dir, content: null }); + } + + // root files + items.push({ path: "project.toml", content: renderProjectToml(p) }); + items.push({ path: "README.md", content: renderReadme(p.name, p.question) }); + items.push({ path: ".gitignore", content: ROOT_GITIGNORE }); + + // scripts/ + items.push({ path: "scripts/README.md", content: SCRIPTS_README }); + + // paper/ + items.push({ path: "paper/outline.md", content: renderOutlineMd(p.name, p.question, p.venue?.name) }); + items.push({ path: "paper/main.tex", content: renderMainTex(p.name, p.venue?.name) }); + items.push({ path: "paper/references.bib", content: "" }); + items.push({ path: "paper/latexmkrc", content: PAPER_LATEXMKRC }); + items.push({ path: "paper/.gitignore", content: PAPER_GITIGNORE }); + + // config/ stubs + items.push({ path: "config/system.toml", content: "# System configuration\n" }); + items.push({ path: "config/lab.toml", content: "# Lab configuration\n" }); + + return items; +} diff --git a/packages/amico-run/src/project_verb.ts b/packages/amico-run/src/project_verb.ts new file mode 100644 index 00000000..917c8338 --- /dev/null +++ b/packages/amico-run/src/project_verb.ts @@ -0,0 +1,263 @@ +// project_verb.ts — CLI wrapper for `amico project create` and `amico project import`. +// Pure logic lives in project.ts; this module handles filesystem I/O, git init, +// flag parsing, and the verb dispatch. Part of #665. +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { homedir } from "node:os"; +import { parse as parseToml } from "smol-toml"; +import { + nameToSlug, + scaffoldManifest, + validateProjectToml, + type ProjectToml, +} from "./project.js"; +import type { VerbResult } from "./verbs.js"; + +function flagValue(argv: string[], name: string): string | undefined { + const i = argv.indexOf(name); + return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; +} + +/** Extract the first positional argument (not a --flag or a flag's value). */ +function positionalArg(argv: string[]): string | undefined { + for (let i = 0; i < argv.length; i++) { + if (argv[i].startsWith("--")) { + i++; // skip the flag's value + continue; + } + return argv[i]; + } + return undefined; +} + +function today(): string { + return new Date().toISOString().slice(0, 10); +} + +// ── create ────────────────────────────────────────────────────────────────── + +export function projectCreate(argv: string[]): VerbResult { + const fail = (error: string): VerbResult => ({ + json: { verb: "project", subcommand: "create", error }, + code: 64, + }); + + // The project name is the first positional argument (non-flag) + const name = positionalArg(argv); + if (!name) return fail("project name is required: amico project create "); + + const slug = nameToSlug(name); + const defaultPath = join(homedir(), "projects", slug); + const projectDir = resolve(flagValue(argv, "--path") ?? defaultPath); + + // Idempotent: if project.toml already exists, validate and return + const tomlPath = join(projectDir, "project.toml"); + if (existsSync(tomlPath)) { + try { + const existing = parseToml(readFileSync(tomlPath, "utf8")) as unknown as ProjectToml; + const v = validateProjectToml(existing); + if (v.ok) { + return { + json: { + verb: "project", + subcommand: "create", + created: false, + idempotent: true, + path: projectDir, + slug: existing.slug, + }, + code: 0, + }; + } + } catch { + // If the existing TOML is invalid, fall through and overwrite + } + } + + const domain = flagValue(argv, "--domain"); + const venue = flagValue(argv, "--venue"); + const deadline = flagValue(argv, "--deadline"); + + const project: ProjectToml = { + schema_version: 1, + name, + slug, + question: flagValue(argv, "--question") ?? "TODO", + status: "proposing", + created: today(), + tags: [], + authors: { lead: flagValue(argv, "--author") }, + ...(domain ? { domain_pack: { name: domain } } : {}), + ...(venue ? { venue: { name: venue, ...(deadline ? { deadline } : {}) } } : {}), + }; + + // Create directory and scaffold + try { + mkdirSync(projectDir, { recursive: true }); + } catch (e) { + return fail(`failed to create directory: ${e instanceof Error ? e.message : String(e)}`); + } + + const manifest = scaffoldManifest(project); + try { + for (const item of manifest) { + const fullPath = join(projectDir, item.path); + if (item.content === null) { + mkdirSync(fullPath, { recursive: true }); + } else { + // Don't overwrite existing files (idempotent) + if (!existsSync(fullPath)) { + mkdirSync(join(fullPath, ".."), { recursive: true }); + writeFileSync(fullPath, item.content); + } + } + } + } catch (e) { + return fail(`failed to scaffold: ${e instanceof Error ? e.message : String(e)}`); + } + + // git init + initial commit + try { + if (!existsSync(join(projectDir, ".git"))) { + execFileSync("git", ["init"], { cwd: projectDir, stdio: "ignore" }); + execFileSync("git", ["add", "."], { cwd: projectDir, stdio: "ignore" }); + execFileSync("git", ["commit", "-m", `init: scaffold research project "${name}"`], { + cwd: projectDir, + stdio: "ignore", + }); + } + } catch (e) { + // git failure is a warning, not a hard error — the scaffold is on disk + return { + json: { + verb: "project", + subcommand: "create", + created: true, + path: projectDir, + slug, + warning: `git init failed: ${e instanceof Error ? e.message : String(e)}`, + }, + code: 0, + }; + } + + return { + json: { + verb: "project", + subcommand: "create", + created: true, + path: projectDir, + slug, + }, + code: 0, + }; +} + +// ── import ────────────────────────────────────────────────────────────────── + +export function projectImport(argv: string[]): VerbResult { + const fail = (error: string): VerbResult => ({ + json: { verb: "project", subcommand: "import", error }, + code: 64, + }); + + // The directory is the first positional argument, or "." by default + const dir = resolve(positionalArg(argv) ?? "."); + + if (!existsSync(dir)) return fail(`directory not found: ${dir}`); + + const tomlPath = join(dir, "project.toml"); + + // Idempotent: if project.toml already exists, validate and return + if (existsSync(tomlPath)) { + try { + const existing = parseToml(readFileSync(tomlPath, "utf8")) as unknown as ProjectToml; + const v = validateProjectToml(existing); + if (v.ok) { + return { + json: { + verb: "project", + subcommand: "import", + imported: false, + idempotent: true, + path: dir, + slug: existing.slug, + }, + code: 0, + }; + } + } catch { + // Invalid TOML; fall through + } + } + + // For import, infer name from directory basename + const basename = dir.split("/").pop() || "unnamed"; + const name = flagValue(argv, "--name") ?? basename; + const slug = nameToSlug(name); + const question = flagValue(argv, "--question") ?? "TODO"; + const status = (flagValue(argv, "--status") ?? "running") as ProjectToml["status"]; + + const project: ProjectToml = { + schema_version: 1, + name, + slug, + question, + status, + created: today(), + }; + + // Scaffold missing directories without overwriting existing files + const manifest = scaffoldManifest(project); + const scaffolded: string[] = []; + + try { + for (const item of manifest) { + const fullPath = join(dir, item.path); + if (item.content === null) { + if (!existsSync(fullPath)) { + mkdirSync(fullPath, { recursive: true }); + scaffolded.push(item.path + "/"); + } + } else { + if (!existsSync(fullPath)) { + mkdirSync(join(fullPath, ".."), { recursive: true }); + writeFileSync(fullPath, item.content); + scaffolded.push(item.path); + } + } + } + } catch (e) { + return fail(`failed to scaffold: ${e instanceof Error ? e.message : String(e)}`); + } + + return { + json: { + verb: "project", + subcommand: "import", + imported: true, + path: dir, + slug, + scaffolded, + }, + code: 0, + }; +} + +// ── dispatch ──────────────────────────────────────────────────────────────── + +export function projectVerb(argv: string[]): VerbResult { + const sub = argv[0]; + const rest = argv.slice(1); + if (sub === "create") return projectCreate(rest); + if (sub === "import") return projectImport(rest); + return { + json: { + verb: "project", + error: `unknown subcommand ${sub ? `"${sub}"` : "(none)"}`, + usage: "amico project create [--path ] [--domain ] [--venue ] [--deadline ] | amico project import [] [--name ] [--question ] [--status ]", + }, + code: 64, + }; +} diff --git a/packages/amico-run/src/verbs.ts b/packages/amico-run/src/verbs.ts index 28f233cd..a8e50dfa 100644 --- a/packages/amico-run/src/verbs.ts +++ b/packages/amico-run/src/verbs.ts @@ -24,6 +24,7 @@ import { fleetVerb } from "./fleet_verb.js"; import { specVerb } from "./spec_verb.js"; import { planVerb } from "./plan_verb.js"; import { handoffVerb } from "./handoff_verb.js"; +import { projectVerb } from "./project_verb.js"; export interface VerbResult { json: unknown; // structured result (stdout as JSON for the CLI; tool content for MCP) @@ -201,4 +202,14 @@ const papers: Verb = { run: papersVerb, }; -export const SPINE_VERBS: Verb[] = [catalog, vault, device, note, ledger, profile, fleet, spec, plan, handoff, papers]; +// project — the research-project entity: create (scaffold + git init) and +// import (non-destructive scaffold over an existing directory). Part of #665. +const project: Verb = { + name: "project", + summary: "create a scaffolded research project / import an existing directory as a research project", + generalizes: "the amicode research-project entity lifecycle (PRD #663)", + slice: "research projects (#665)", + run: projectVerb, +}; + +export const SPINE_VERBS: Verb[] = [catalog, vault, device, note, ledger, profile, fleet, spec, plan, handoff, papers, project]; diff --git a/packages/amico-run/test/project_verb.test.ts b/packages/amico-run/test/project_verb.test.ts new file mode 100644 index 00000000..9515077b --- /dev/null +++ b/packages/amico-run/test/project_verb.test.ts @@ -0,0 +1,400 @@ +// `amico project` — research project entity: schema validation, scaffolding, +// and CLI verbs (create / import). Pure logic in project.ts; verb I/O in +// project_verb.ts. Part of #665. +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtempSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + validateProjectToml, + nameToSlug, + scaffoldManifest, + venueToDocumentClass, + renderMainTex, + renderOutlineMd, + renderProjectToml, + SCAFFOLD_DIRS, + ROOT_GITIGNORE, + PAPER_GITIGNORE, + PAPER_LATEXMKRC, + type ProjectToml, +} from "../src/project.js"; + +// ── pure logic: schema validation ────────────────────────────────────────── + +describe("validateProjectToml", () => { + const valid: ProjectToml = { + schema_version: 1, + name: "Diraq ESR X-gate study", + slug: "diraq-esr-x-gate-study", + question: "Can we achieve F > 0.999 for a 29Si-robust X gate?", + status: "proposing", + created: "2026-08-31", + tags: ["transmon", "esr"], + authors: { lead: "JJ Lee" }, + domain_pack: { name: "quantum-control" }, + }; + + it("accepts a valid project.toml with all required fields", () => { + const result = validateProjectToml(valid); + expect(result.ok).toBe(true); + }); + + it("accepts a minimal project.toml (required fields only)", () => { + const minimal: ProjectToml = { + schema_version: 1, + name: "My Project", + slug: "my-project", + question: "What happens?", + status: "proposing", + created: "2026-08-31", + }; + const result = validateProjectToml(minimal); + expect(result.ok).toBe(true); + }); + + it("rejects missing required field: name", () => { + const bad = { ...valid, name: undefined } as unknown as ProjectToml; + const result = validateProjectToml(bad); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.errors.some((e: string) => e.includes("name"))).toBe(true); + }); + + it("rejects missing required field: schema_version", () => { + const bad = { ...valid, schema_version: undefined } as unknown as ProjectToml; + const result = validateProjectToml(bad); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.errors.some((e: string) => e.includes("schema_version"))).toBe(true); + }); + + it("rejects invalid status enum value", () => { + const bad = { ...valid, status: "thinking" } as unknown as ProjectToml; + const result = validateProjectToml(bad); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.errors.some((e: string) => e.includes("status"))).toBe(true); + }); + + it("accepts all valid status values", () => { + for (const s of ["proposing", "designing", "running", "analyzing", "writing", "complete"]) { + const result = validateProjectToml({ ...valid, status: s as ProjectToml["status"] }); + expect(result.ok).toBe(true); + } + }); + + it("tolerates unknown extra fields (forward-compatible)", () => { + const extended = { ...valid, future_field: "surprise" } as unknown as ProjectToml; + const result = validateProjectToml(extended); + expect(result.ok).toBe(true); + }); +}); + +// ── pure logic: slug generation ──────────────────────────────────────────── + +describe("nameToSlug", () => { + it("converts a project name to kebab-case slug", () => { + expect(nameToSlug("Diraq ESR X-gate study")).toBe("diraq-esr-x-gate-study"); + }); + + it("strips non-alphanumeric characters", () => { + expect(nameToSlug("My Cool Project!!!")).toBe("my-cool-project"); + }); + + it("collapses multiple hyphens", () => { + expect(nameToSlug("a -- b --- c")).toBe("a-b-c"); + }); + + it("trims leading/trailing hyphens", () => { + expect(nameToSlug(" --hello-- ")).toBe("hello"); + }); +}); + +// ── pure logic: scaffold manifest ────────────────────────────────────────── + +describe("scaffoldManifest", () => { + const project: ProjectToml = { + schema_version: 1, + name: "Test Project", + slug: "test-project", + question: "Does it work?", + status: "proposing", + created: "2026-08-31", + }; + + it("includes all prescribed directories from the PRD", () => { + const manifest = scaffoldManifest(project); + const dirs = manifest.filter((m) => m.content === null).map((m) => m.path); + for (const expected of SCAFFOLD_DIRS) { + expect(dirs).toContain(expected); + } + }); + + it("produces project.toml, README.md, .gitignore at root", () => { + const manifest = scaffoldManifest(project); + const files = manifest.filter((m) => m.content !== null).map((m) => m.path); + expect(files).toContain("project.toml"); + expect(files).toContain("README.md"); + expect(files).toContain(".gitignore"); + }); + + it("produces paper/ files: outline.md, main.tex, references.bib, latexmkrc, .gitignore", () => { + const manifest = scaffoldManifest(project); + const files = manifest.filter((m) => m.content !== null).map((m) => m.path); + expect(files).toContain("paper/outline.md"); + expect(files).toContain("paper/main.tex"); + expect(files).toContain("paper/references.bib"); + expect(files).toContain("paper/latexmkrc"); + expect(files).toContain("paper/.gitignore"); + }); + + it("produces scripts/README.md", () => { + const manifest = scaffoldManifest(project); + const files = manifest.filter((m) => m.content !== null).map((m) => m.path); + expect(files).toContain("scripts/README.md"); + }); + + it("produces config stubs: system.toml, lab.toml", () => { + const manifest = scaffoldManifest(project); + const files = manifest.filter((m) => m.content !== null).map((m) => m.path); + expect(files).toContain("config/system.toml"); + expect(files).toContain("config/lab.toml"); + }); +}); + +// ── pure logic: TOML rendering round-trip ────────────────────────────────── + +describe("renderProjectToml", () => { + it("produces parseable TOML with all required fields", () => { + const project: ProjectToml = { + schema_version: 1, + name: "Test Project", + slug: "test-project", + question: "Does it work?", + status: "proposing", + created: "2026-08-31", + }; + const toml = renderProjectToml(project); + expect(toml).toContain('name = "Test Project"'); + expect(toml).toContain("schema_version = 1"); + expect(toml).toContain('status = "proposing"'); + }); +}); + +// ── pure logic: venue-aware templates ────────────────────────────────────── + +describe("venueToDocumentClass", () => { + it("maps Physical Review Letters to revtex", () => { + expect(venueToDocumentClass("Physical Review Letters")).toBe("revtex"); + }); + + it("maps Physical Review X to revtex", () => { + expect(venueToDocumentClass("Physical Review X")).toBe("revtex"); + }); + + it("maps PRL shorthand to revtex", () => { + expect(venueToDocumentClass("PRL")).toBe("revtex"); + }); + + it("maps unknown venue to minimal", () => { + expect(venueToDocumentClass("Nature Physics")).toBe("minimal"); + }); + + it("maps undefined venue to minimal", () => { + expect(venueToDocumentClass(undefined)).toBe("minimal"); + }); +}); + +describe("renderMainTex", () => { + it("uses revtex4-2 document class for PRL", () => { + const tex = renderMainTex("My Paper", "Physical Review Letters"); + expect(tex).toContain("revtex4-2"); + expect(tex).toContain("\\title{My Paper}"); + }); + + it("uses article document class for unknown venues", () => { + const tex = renderMainTex("My Paper", "Nature Physics"); + expect(tex).toContain("\\documentclass[11pt]{article}"); + expect(tex).not.toContain("revtex"); + }); + + it("uses article when no venue specified", () => { + const tex = renderMainTex("My Paper"); + expect(tex).toContain("\\documentclass[11pt]{article}"); + }); +}); + +describe("renderOutlineMd", () => { + it("includes YAML frontmatter with draft status", () => { + const md = renderOutlineMd("Test", "What?"); + expect(md).toMatch(/^---\nstatus: draft/); + expect(md).toContain("sections_approved: []"); + }); + + it("uses compressed PRL format for Physical Review Letters", () => { + const md = renderOutlineMd("Test", "What?", "Physical Review Letters"); + expect(md).toContain("PRL/PRX compressed format"); + expect(md).not.toContain("### Conclusion"); + }); + + it("uses full section format for unknown venues", () => { + const md = renderOutlineMd("Test", "What?"); + expect(md).toContain("### Conclusion"); + }); +}); + +// ── pure logic: gitignore content ────────────────────────────────────────── + +describe("gitignore content", () => { + it("root .gitignore includes jld2/hdf5/h5 patterns", () => { + expect(ROOT_GITIGNORE).toContain("*.jld2"); + expect(ROOT_GITIGNORE).toContain("*.hdf5"); + expect(ROOT_GITIGNORE).toContain("*.h5"); + expect(ROOT_GITIGNORE).toContain("__pycache__/"); + expect(ROOT_GITIGNORE).toContain(".DS_Store"); + }); + + it("paper .gitignore excludes LaTeX build artifacts", () => { + expect(PAPER_GITIGNORE).toContain("*.aux"); + expect(PAPER_GITIGNORE).toContain("*.bbl"); + expect(PAPER_GITIGNORE).toContain("*.synctex.gz"); + expect(PAPER_GITIGNORE).toContain("*.fdb_latexmk"); + expect(PAPER_GITIGNORE).toContain("*.pdf"); + }); + + it("paper/latexmkrc has the correct content", () => { + expect(PAPER_LATEXMKRC).toContain("pdflatex -interaction=nonstopmode"); + }); +}); + +// ── integration: project create verb ─────────────────────────────────────── + +import { projectCreate, projectImport } from "../src/project_verb.js"; + +describe("projectCreate", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), "amico-project-test-")); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("creates a project directory with the full prescribed layout", () => { + const projectDir = join(tmpDir, "my-test-project"); + const result = projectCreate(["My Test Project", "--path", projectDir, "--question", "Does TDD work?"]); + + expect((result.json as Record).created).toBe(true); + expect(result.code).toBe(0); + + // Verify project.toml exists and is valid + expect(existsSync(join(projectDir, "project.toml"))).toBe(true); + const tomlContent = readFileSync(join(projectDir, "project.toml"), "utf8"); + expect(tomlContent).toContain("My Test Project"); + expect(tomlContent).toContain("my-test-project"); + + // Verify prescribed directories + for (const dir of ["scripts", "scripts/testbed", "data/raw", "data/processed", + "data/plots", "analysis", "paper/figures", "paper/supplementary", + "ledger/hypotheses", "ledger/observations", "ledger/literature", + "ledger/campaigns", "config", "skills"]) { + expect(existsSync(join(projectDir, dir))).toBe(true); + } + + // Verify root files + expect(existsSync(join(projectDir, "README.md"))).toBe(true); + expect(existsSync(join(projectDir, ".gitignore"))).toBe(true); + + // Verify paper files + expect(existsSync(join(projectDir, "paper/outline.md"))).toBe(true); + expect(existsSync(join(projectDir, "paper/main.tex"))).toBe(true); + expect(existsSync(join(projectDir, "paper/references.bib"))).toBe(true); + expect(existsSync(join(projectDir, "paper/latexmkrc"))).toBe(true); + expect(existsSync(join(projectDir, "paper/.gitignore"))).toBe(true); + + // Verify git repository + expect(existsSync(join(projectDir, ".git"))).toBe(true); + }); + + it("uses REVTeX template when --venue is Physical Review Letters", () => { + const projectDir = join(tmpDir, "prl-project"); + projectCreate(["PRL Paper", "--path", projectDir, "--venue", "Physical Review Letters"]); + + const tex = readFileSync(join(projectDir, "paper/main.tex"), "utf8"); + expect(tex).toContain("revtex4-2"); + + const outline = readFileSync(join(projectDir, "paper/outline.md"), "utf8"); + expect(outline).toContain("PRL/PRX compressed format"); + }); + + it("is idempotent — re-running returns success without overwriting", () => { + const projectDir = join(tmpDir, "idempotent-project"); + const first = projectCreate(["Idempotent", "--path", projectDir]); + expect((first.json as Record).created).toBe(true); + + const second = projectCreate(["Idempotent", "--path", projectDir]); + expect(second.code).toBe(0); + expect((second.json as Record).idempotent).toBe(true); + }); + + it("returns error when no name is provided", () => { + const result = projectCreate(["--path", join(tmpDir, "bad")]); + expect(result.code).toBe(64); + expect((result.json as Record).error).toBeDefined(); + }); +}); + +// ── integration: project import verb ─────────────────────────────────────── + +describe("projectImport", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), "amico-project-import-")); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("creates project.toml and scaffolds missing dirs in an existing directory", () => { + const existingDir = join(tmpDir, "existing-project"); + mkdirSync(join(existingDir, "scripts"), { recursive: true }); + writeFileSync(join(existingDir, "scripts/my_solve.jl"), "# existing file"); + + const result = projectImport([existingDir, "--name", "Existing Project", "--question", "What?"]); + + expect((result.json as Record).imported).toBe(true); + expect(result.code).toBe(0); + + // project.toml was created + expect(existsSync(join(existingDir, "project.toml"))).toBe(true); + + // Existing file was NOT overwritten + expect(readFileSync(join(existingDir, "scripts/my_solve.jl"), "utf8")).toBe("# existing file"); + + // Missing dirs were scaffolded + expect(existsSync(join(existingDir, "scripts/testbed"))).toBe(true); + expect(existsSync(join(existingDir, "data/raw"))).toBe(true); + expect(existsSync(join(existingDir, "ledger/campaigns"))).toBe(true); + expect(existsSync(join(existingDir, "paper/outline.md"))).toBe(true); + }); + + it("is idempotent — re-importing returns success without overwriting", () => { + const dir = join(tmpDir, "idempotent-import"); + mkdirSync(dir, { recursive: true }); + + const first = projectImport([dir, "--name", "Test"]); + expect((first.json as Record).imported).toBe(true); + + const second = projectImport([dir]); + expect(second.code).toBe(0); + expect((second.json as Record).idempotent).toBe(true); + }); + + it("returns error for nonexistent directory", () => { + const result = projectImport([join(tmpDir, "does-not-exist")]); + expect(result.code).toBe(64); + }); +}); From 7ec2a74db378ba88e40ff4fd619c7aeb8dc94ea2 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 13:41:30 -0400 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20project=20type=20detection=20?= =?UTF-8?q?=E2=80=94=20research=20vs=20dev=20awareness=20(#666)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add detectProjectType(dir) — checks for project.toml at directory root; presence → 'research', absence → 'dev' - Extend ProjectDirEntry with type field, populated in listProjectDirs() - Extend PromptProject interface in the overlay with optional type field - Type is re-evaluated on each resolution (no caching) so directories that gain project.toml mid-session update their type - 7 tests: detection, type change, listProjectDirs integration Closes #666 --- .../components/prompt-project-selector.tsx | 1 + .../extension/src/amicode_service/project.ts | 5 +- packages/extension/src/project/detect.ts | 24 ++++++ .../extension/test/project/detect.test.ts | 80 +++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 packages/extension/src/project/detect.ts create mode 100644 packages/extension/test/project/detect.test.ts diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx index fae3ded5..cef93234 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx @@ -28,6 +28,7 @@ export type PromptProject = { sandboxes?: string[] icon?: { color?: string; url?: string; override?: string } server?: { key: string; name: string } + type?: "research" | "dev" } export type PromptProjectControls = { diff --git a/packages/extension/src/amicode_service/project.ts b/packages/extension/src/amicode_service/project.ts index cd330a3e..4f655000 100644 --- a/packages/extension/src/amicode_service/project.ts +++ b/packages/extension/src/amicode_service/project.ts @@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process" import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs" import { homedir } from "node:os" import path from "node:path" +import { detectProjectType, type ProjectType } from "../project/detect" /** The default parent when the client sends none — the webview doesn't know the * user's home dir, so the server owns the default (created on first use). */ @@ -109,7 +110,7 @@ export function createProject(rawBody: string): string { return JSON.stringify(createProjectAt(plan.target, plan.slug)) } -export type ProjectDirEntry = { slug: string; path: string } +export type ProjectDirEntry = { slug: string; path: string; type: ProjectType } /** Enumerate the immediate subdirectories of the projects parent — each folder * IS a project (amicode is folder-first). This is the source of truth for the @@ -132,7 +133,7 @@ export function listProjectDirs( if (!exists(parentDir)) return [] return readEntries(parentDir) .filter((e) => e.isDirectory && !e.name.startsWith(".")) - .map((e) => ({ slug: e.name, path: path.join(parentDir, e.name) })) + .map((e) => ({ slug: e.name, path: path.join(parentDir, e.name), type: detectProjectType(path.join(parentDir, e.name)) })) .sort((a, b) => a.slug.localeCompare(b.slug)) } diff --git a/packages/extension/src/project/detect.ts b/packages/extension/src/project/detect.ts new file mode 100644 index 00000000..c892bcbc --- /dev/null +++ b/packages/extension/src/project/detect.ts @@ -0,0 +1,24 @@ +// detect.ts — project type detection (#666). +// A Research Project is identified by a `project.toml` at its root. +// Detection is by file presence only (fast stat, no content parsing). +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +export type ProjectType = "research" | "dev"; + +/** + * Detect whether a directory is a Research Project or a Dev Project. + * A Research Project has a `project.toml` manifest at its root. + * Everything else is a Dev Project (the existing git-repo model). + * + * Re-evaluated on each call — no caching — so a directory that gains + * `project.toml` after initial registration updates its type on next + * resolution. + */ +export function detectProjectType(dir: string): ProjectType { + try { + return existsSync(join(dir, "project.toml")) ? "research" : "dev"; + } catch { + return "dev"; + } +} diff --git a/packages/extension/test/project/detect.test.ts b/packages/extension/test/project/detect.test.ts new file mode 100644 index 00000000..c1cbd4ab --- /dev/null +++ b/packages/extension/test/project/detect.test.ts @@ -0,0 +1,80 @@ +// Project type detection tests — #666. +// detectProjectType(dir) returns "research" if project.toml exists, "dev" otherwise. +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { detectProjectType } from "../../src/project/detect"; +import { listProjectDirs } from "../../src/amicode_service/project"; + +describe("detectProjectType", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), "amicode-detect-")); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("returns 'research' when project.toml exists at the root", () => { + writeFileSync(join(tmpDir, "project.toml"), 'schema_version = 1\nname = "test"\n'); + expect(detectProjectType(tmpDir)).toBe("research"); + }); + + it("returns 'dev' when project.toml does not exist", () => { + expect(detectProjectType(tmpDir)).toBe("dev"); + }); + + it("returns 'dev' for a nonexistent directory", () => { + expect(detectProjectType(join(tmpDir, "nope"))).toBe("dev"); + }); + + it("detects type change: directory gains project.toml after first resolution", () => { + expect(detectProjectType(tmpDir)).toBe("dev"); + writeFileSync(join(tmpDir, "project.toml"), 'schema_version = 1\n'); + expect(detectProjectType(tmpDir)).toBe("research"); + }); +}); + +// ── integration: listProjectDirs carries type ────────────────────────────── + +describe("listProjectDirs with type detection", () => { + let parentDir: string; + + beforeEach(() => { + parentDir = mkdtempSync(join(tmpdir(), "amicode-list-")); + // Create two project directories + mkdirSync(join(parentDir, "research-proj")); + writeFileSync(join(parentDir, "research-proj", "project.toml"), 'schema_version = 1\n'); + mkdirSync(join(parentDir, "dev-proj")); + }); + + afterEach(() => { + rmSync(parentDir, { recursive: true, force: true }); + }); + + it("returns type 'research' for project with project.toml", () => { + const projects = listProjectDirs(parentDir); + const research = projects.find((p) => p.slug === "research-proj"); + expect(research).toBeDefined(); + expect(research!.type).toBe("research"); + }); + + it("returns type 'dev' for project without project.toml", () => { + const projects = listProjectDirs(parentDir); + const dev = projects.find((p) => p.slug === "dev-proj"); + expect(dev).toBeDefined(); + expect(dev!.type).toBe("dev"); + }); + + it("all entries carry the type field", () => { + const projects = listProjectDirs(parentDir); + expect(projects).toHaveLength(2); + for (const p of projects) { + expect(p.type).toBeDefined(); + expect(["research", "dev"]).toContain(p.type); + } + }); +}); From cf073fd706d2a31e756d0f9ecc921662d8c06749 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 13:44:55 -0400 Subject: [PATCH 05/22] =?UTF-8?q?feat:=20paper-writer=20skill=20=E2=80=94?= =?UTF-8?q?=20gated=20agentic=20paper-writing=20workflow=20(#669)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ship paper-writer/SKILL.md in the public skill library - Hard gates: outline-first (refuse without finalized outline), section-by- section approval (track in sections_approved[]), abstract prohibition - Soft gates: content provenance (trace to outline bullets), forbidden actions (no novel conclusions, no interpretation, no abstract), data grounding (cite data/ files) - Seven capabilities: sentence expansion, data citation, figure placement, bibliography management, gap detection, proofreading, equation formatting - Four venue-aware outline templates: PRL compressed, PRX Quantum full, arXiv general, minimal - surface: public, overridable per-project via skills/paper-writer/ Closes #669 --- .../extension/skills/paper-writer/SKILL.md | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 packages/extension/skills/paper-writer/SKILL.md diff --git a/packages/extension/skills/paper-writer/SKILL.md b/packages/extension/skills/paper-writer/SKILL.md new file mode 100644 index 00000000..b345ab6c --- /dev/null +++ b/packages/extension/skills/paper-writer/SKILL.md @@ -0,0 +1,309 @@ +--- +name: paper-writer +description: Gated agentic paper-writing workflow — outline-first, section-by-section, content-provenance. Use when the user asks to write, format, or edit sections of a research paper. +agents: [] +surface: public +--- + +# Paper Writer + +A gated workflow for formatting research papers from a user-authored outline. +The researcher owns the intellectual content; the agent is a typesetter, not a +co-author. + +## Gate model + +This skill enforces two classes of gate: + +- **Hard gates** — code-enforced by checking files and frontmatter before + acting. The agent reads `paper/outline.md`, inspects its YAML frontmatter, + and refuses to proceed if the gate conditions are not met. These are + deterministic. +- **Soft gates** — LLM instructions that guide behavior probabilistically. + They work as well as the model's instruction-following allows. This skill + is honest about the distinction. + +## Hard gates + +### 1. Outline-first gate + +**Before writing any content in `paper/main.tex`**, check that +`paper/outline.md` exists AND its YAML frontmatter has `status: final` (not +`draft`). + +If the outline is missing or still `status: draft`, **refuse** with: + +> "I need a finalized outline before writing the paper. Your outline is +> currently [missing / draft]. Please review `paper/outline.md`, mark each +> section as ready, and set `status: final` in the frontmatter." + +The user controls when the outline is final. The agent never changes the +outline's `status` field. + +### 2. Section-by-section approval + +Format **one section at a time** in this order: + +1. Read the section's outline bullets from `paper/outline.md` +2. Expand the bullets into LaTeX prose in `paper/main.tex` +3. Present the formatted section to the user for review +4. Wait for explicit approval before proceeding to the next section + +After user approval, add the section name to the `sections_approved[]` array +in `paper/outline.md` frontmatter. Never format a section already in the +approved list unless the user explicitly asks for a revision. + +### 3. Abstract prohibition + +The abstract is **always** user-authored. Never generate an abstract. If the +user asks the agent to write the abstract, respond: + +> "The abstract is your synthesis of the work — I can proofread or format it, +> but I shouldn't write it. Draft your abstract in the outline and I'll +> typeset it." + +## Soft gates (LLM instructions) + +These are behavioral rules, not code-enforceable constraints. They guide the +agent's output quality but are probabilistic. + +### Content provenance + +Every paragraph in `paper/main.tex` must trace back to a specific bullet in +`paper/outline.md`. When expanding a bullet into prose, cite the outline +bullet in a LaTeX comment: + +```latex +% outline: Results > bullet 3 +The optimized pulse achieves $F = 0.9995$ in 42~ns... +``` + +### Forbidden actions + +The agent MUST NOT: + +1. **Generate novel conclusions** — only format what the outline says +2. **Interpret results** — report numbers; do not explain their significance + beyond what the outline states +3. **Generate the abstract** — always user-authored (hard gate above) +4. **Select emphasis** — do not add "importantly," "notably," "remarkably" or + similar editorial emphasis not present in the outline +5. **Invent methodology details** — only include methods described in the + outline or referenced in `scripts/` +6. **Hallucinate citations** — only cite references that exist in + `paper/references.bib` + +### Data grounding + +Quantitative claims (fidelities, gate times, error rates, etc.) must cite +their source file in `data/`. When expanding an outline bullet that contains a +number, verify the number appears in a referenced data file. If the number +cannot be traced to `data/`, flag it: + +```latex +% WARNING: F = 0.9995 not found in data/ — verify before submission +``` + +## Capabilities + +The paper-writer skill supports seven operations: + +1. **Sentence expansion from outline** — expand outline bullets into + well-formed LaTeX paragraphs, one section at a time +2. **Data citation with inline results** — embed quantitative results from + `data/` files inline, with provenance comments +3. **Figure placement and captioning** — insert `\includegraphics` for files + in `paper/figures/`, draft captions from outline descriptions +4. **Bibliography management** — add BibTeX entries to `paper/references.bib`, + insert `\cite{}` references where the outline indicates citations +5. **Outline gap detection** — identify outline sections that are too thin to + expand into a full section and suggest what's missing +6. **Proofreading** — fix grammar, punctuation, and LaTeX formatting in + already-approved sections (does not change technical content) +7. **Equation formatting** — convert inline math descriptions in the outline + to properly formatted LaTeX equations + +## Venue-aware outline templates + +When scaffolding `paper/outline.md` for a new project, use the template +matching the project's venue. These templates define the section structure the +paper-writer expects. + +### PRL (Physical Review Letters) — compressed format + +```markdown +--- +status: draft +last_reviewed: +sections_approved: [] +--- + +# [Title] + +**Research question:** [question] + +## Outline (PRL compressed format) + +### Abstract +- [ ] [User-authored summary — max 600 words] + +### Introduction +- [ ] Context and motivation (1-2 paragraphs) +- [ ] State of the art and gap +- [ ] Our contribution (one sentence) + +### Results +- [ ] Main result with key figure +- [ ] Supporting measurements +- [ ] Comparison with prior work + +### Discussion +- [ ] Interpretation of results +- [ ] Limitations +- [ ] Outlook + +### Methods +- [ ] Experimental/computational setup +- [ ] Key parameters and their justification + +### Supplementary +- [ ] Extended data tables +- [ ] Additional figures +- [ ] Detailed derivations +``` + +### PRX Quantum — full format + +```markdown +--- +status: draft +last_reviewed: +sections_approved: [] +--- + +# [Title] + +**Research question:** [question] + +## Outline (PRX Quantum full format) + +### Abstract +- [ ] [User-authored summary] + +### Introduction +- [ ] Broad context (1 paragraph) +- [ ] Specific problem and prior work (2-3 paragraphs) +- [ ] Gap in existing approaches +- [ ] Our approach and key results (1 paragraph) +- [ ] Paper organization + +### Background +- [ ] Notation and conventions +- [ ] Review of relevant theory +- [ ] System model + +### Methods +- [ ] Problem formulation +- [ ] Optimization approach +- [ ] Computational details + +### Results +- [ ] Main result +- [ ] Systematic study / parameter sweep +- [ ] Robustness analysis +- [ ] Comparison with baselines + +### Discussion +- [ ] Interpretation +- [ ] Connections to related work +- [ ] Limitations and assumptions +- [ ] Future directions + +### Conclusion +- [ ] Summary of contributions +- [ ] Open questions + +### Acknowledgments +- [ ] Funding, collaborators +``` + +### arXiv general + +```markdown +--- +status: draft +last_reviewed: +sections_approved: [] +--- + +# [Title] + +**Research question:** [question] + +## Outline + +### Abstract +- [ ] [User-authored summary] + +### Introduction +- [ ] Motivation and context +- [ ] Prior work +- [ ] Our contribution + +### Methods +- [ ] Approach description +- [ ] Implementation details + +### Results +- [ ] Key findings +- [ ] Supporting evidence + +### Discussion +- [ ] Implications +- [ ] Limitations + +### Conclusion +- [ ] Summary and outlook +``` + +### Minimal (no venue) + +```markdown +--- +status: draft +last_reviewed: +sections_approved: [] +--- + +# [Title] + +**Research question:** [question] + +## Outline + +### Abstract +- [ ] [User-authored summary] + +### Introduction +- [ ] TODO + +### Methods +- [ ] TODO + +### Results +- [ ] TODO + +### Discussion +- [ ] TODO + +### Conclusion +- [ ] TODO +``` + +## Overriding per-project + +A Research Project can override this skill by placing a custom +`skills/paper-writer/SKILL.md` in its project directory. The project version +shadows the shipped version (project > custom > workspace > shipped in the +skill merge chain). This lets a team enforce lab-specific writing conventions +or venue requirements. From 438e0e9e005e0e81d6edb4df3881c86a87831107 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 13:49:06 -0400 Subject: [PATCH 06/22] feat: skill auto-loading from project directories (#668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add resolveProjectSkills() — scans workspace folders with project.toml for skills/ directory, labels entries with source 'project' - Extend mergeSkillEntries() signature to project > custom > workspace > shipped (4 params); existing callers pass [] for project - Extend SkillIndexEntry source union with 'project' - buildSkillIndexSection annotates project skills with (project) label - Shadow warning logged when a project skill overrides a shipped skill - Add workspaceFolders option to OpencodeConfigOptions for passing VS Code workspace folders to the skill pipeline - 7 new tests + 4 existing tests updated for new signature Closes #668 --- packages/extension/src/opencode_config.ts | 11 +- .../extension/src/scores/package_skills.ts | 7 +- .../src/scores/user_skill_providers.ts | 33 ++++- .../test/scores/project_skills.test.ts | 130 ++++++++++++++++++ .../test/scores/user_skill_providers.test.ts | 8 +- 5 files changed, 178 insertions(+), 11 deletions(-) create mode 100644 packages/extension/test/scores/project_skills.test.ts diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 93ff48ed..9e8241b5 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -16,7 +16,7 @@ import { type LibraryRoot, type LibraryRootSpec, } from "./scores/package_skills"; -import { resolveUserSkills, resolveWorkspaceSkills, mergeSkillEntries } from "./scores/user_skill_providers"; +import { resolveUserSkills, resolveWorkspaceSkills, resolveProjectSkills, mergeSkillEntries } from "./scores/user_skill_providers"; import { readSolverModeState } from "./solver_mode"; import { studioPathsOrLegacy } from "@amicode/schema"; import { opencodeConfigDir } from "./opencode_xdg"; @@ -556,6 +556,10 @@ export interface OpencodeConfigOptions { /** Absolute path to the workspace .opencode/skills/ directory (issue #573). * Auto-loaded skills labeled (workspace). Undefined = no workspace skills. */ workspaceSkillsDir?: string; + /** Absolute paths to workspace folders (VS Code multi-root). Used for + * research-project skill discovery (#668): each folder with project.toml + * has its skills/ scanned. Undefined = no project skills. */ + workspaceFolders?: string[]; /** Stable project dir to (re)use across activations; created if missing and * safe to re-prepare (every write below is overwrite-idempotent). Default: * a fresh mkdtemp — but note the app PERSISTS the selected project per @@ -736,7 +740,10 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro const workspaceEntries = opts.workspaceSkillsDir ? resolveWorkspaceSkills(opts.workspaceSkillsDir) : []; - skillEntries = mergeSkillEntries(customEntries, workspaceEntries, shippedEntries); + const projectEntries = opts.workspaceFolders + ? resolveProjectSkills(opts.workspaceFolders) + : []; + skillEntries = mergeSkillEntries(projectEntries, customEntries, workspaceEntries, shippedEntries); const section = buildSkillIndexSection(skillEntries); if (section) finalContent = finalContent + "\n\n" + section; } catch (e) { diff --git a/packages/extension/src/scores/package_skills.ts b/packages/extension/src/scores/package_skills.ts index c002fa4d..14ecba00 100644 --- a/packages/extension/src/scores/package_skills.ts +++ b/packages/extension/src/scores/package_skills.ts @@ -22,7 +22,7 @@ import { parse as parseYaml } from "yaml"; // same parser as scores/loader.ts // Content is read on demand by the agent — never baked into the prompt or the // .vsix. Errors mirror the entitlements philosophy: skip + warn, never throw. export interface SkillIndexEntry { - source: "library" | "package" | "custom" | "workspace"; // platform | co-located | user-added | workspace .opencode/skills/ + source: "library" | "package" | "custom" | "workspace" | "project"; // platform | co-located | user-added | workspace .opencode/skills/ | research project skills/ package?: string; // absent for library entries (spec §3) name: string; description: string; @@ -256,6 +256,7 @@ export function stageOpencodeSkills(stageRoot: string, entries: SkillIndexEntry[ export function buildSkillIndexSection(entries: SkillIndexEntry[]): string { if (entries.length === 0) return ""; // no section at all (spec §3) const platform = entries.filter((e) => e.source === "library"); + const project = entries.filter((e) => e.source === "project"); const custom = entries.filter((e) => e.source === "custom"); const workspace = entries.filter((e) => e.source === "workspace"); const pkg = entries.filter((e) => e.source === "package"); @@ -272,6 +273,10 @@ export function buildSkillIndexSection(entries: SkillIndexEntry[]): string { (e) => `- **${e.name}** (platform reference) — ${e.description}\n - Use as physics reference — inline the constants; authored scripts stay self-contained (no \`include\` of demo-repo files).`, ), + ...project.map((e) => { + const label = (e as any).overridesShipped ? "(project, overrides platform)" : "(project)"; + return `- **${e.name}** ${label} — ${e.description}`; + }), ...custom.map((e) => { const label = (e as any).overridesShipped ? "(custom, overrides platform)" : "(custom)"; return `- **${e.name}** ${label} — ${e.description}`; diff --git a/packages/extension/src/scores/user_skill_providers.ts b/packages/extension/src/scores/user_skill_providers.ts index 396798bd..15a8e1df 100644 --- a/packages/extension/src/scores/user_skill_providers.ts +++ b/packages/extension/src/scores/user_skill_providers.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { parse as parseYaml } from "yaml"; +import { detectProjectType } from "../project/detect"; import type { SkillIndexEntry } from "./package_skills"; // --- Types --- @@ -85,17 +86,33 @@ export function resolveWorkspaceSkills(wsSkillsDir: string): SkillIndexEntry[] { return scanSkillDirectory(wsSkillsDir, "workspace"); } +/** Resolve project skills from research project workspace folders (#668). + * For each folder with `project.toml`, scan its `skills/` directory. + * Non-research directories are skipped. Order-preserving: first folder wins + * on name collision (consistent with workspace-folder order). */ +export function resolveProjectSkills(workspaceFolders: string[]): SkillIndexEntry[] { + const out: SkillIndexEntry[] = []; + for (const folder of workspaceFolders) { + if (detectProjectType(folder) !== "research") continue; + const skillsDir = path.join(folder, "skills"); + if (!fs.existsSync(skillsDir)) continue; + out.push(...scanSkillDirectory(skillsDir, "project" as SkillIndexEntry["source"])); + } + return out; +} + /** A merged entry may carry an `overridesShipped` flag when a custom/workspace * skill shadows a platform (library/package) skill of the same name. */ export interface MergedSkillEntry extends SkillIndexEntry { overridesShipped?: boolean; } -/** Merge skill entries with shadow semantics: custom > workspace > shipped. - * First match by name wins (resolution order). If a custom or workspace entry +/** Merge skill entries with shadow semantics: project > custom > workspace > shipped. + * First match by name wins (resolution order). If a higher-priority entry * shadows a shipped skill, the winner carries `overridesShipped: true` so the * Skill Index can label it appropriately. */ export function mergeSkillEntries( + project: SkillIndexEntry[], custom: SkillIndexEntry[], workspace: SkillIndexEntry[], shipped: SkillIndexEntry[], @@ -104,14 +121,22 @@ export function mergeSkillEntries( const shippedNames = new Set(shipped.map((e) => e.name)); const out: MergedSkillEntry[] = []; - // Custom first (highest priority) + // Project first (highest priority) + for (const e of project) { + if (seen.has(e.name)) continue; + seen.add(e.name); + const overrides = shippedNames.has(e.name); + if (overrides) console.warn(`amicode: project skill "${e.name}" shadows shipped skill`); + out.push(overrides ? { ...e, overridesShipped: true } : e); + } + // Custom second for (const e of custom) { if (seen.has(e.name)) continue; seen.add(e.name); const overrides = shippedNames.has(e.name); out.push(overrides ? { ...e, overridesShipped: true } : e); } - // Workspace second + // Workspace third for (const e of workspace) { if (seen.has(e.name)) continue; seen.add(e.name); diff --git a/packages/extension/test/scores/project_skills.test.ts b/packages/extension/test/scores/project_skills.test.ts new file mode 100644 index 00000000..2efb9f75 --- /dev/null +++ b/packages/extension/test/scores/project_skills.test.ts @@ -0,0 +1,130 @@ +// Skill auto-loading from research project directories — #668. +// resolveProjectSkills scans workspace folders with project.toml for skills/. +// mergeSkillEntries extended with project source (highest priority). +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + resolveProjectSkills, + mergeSkillEntries, +} from "../../src/scores/user_skill_providers"; +import type { SkillIndexEntry } from "../../src/scores/package_skills"; + +describe("resolveProjectSkills", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), "amicode-project-skills-")); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("discovers skills from a research project's skills/ directory", () => { + const projectDir = join(tmpDir, "my-research"); + mkdirSync(join(projectDir, "skills", "my-analysis"), { recursive: true }); + writeFileSync(join(projectDir, "project.toml"), 'schema_version = 1\n'); + writeFileSync(join(projectDir, "skills", "my-analysis", "SKILL.md"), `--- +name: my-analysis +description: Custom analysis skill for this project +agents: [] +surface: public +--- +# My Analysis +`); + + const skills = resolveProjectSkills([projectDir]); + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe("my-analysis"); + expect(skills[0].source).toBe("project"); + expect(skills[0].description).toBe("Custom analysis skill for this project"); + }); + + it("ignores directories without project.toml (dev projects)", () => { + const devDir = join(tmpDir, "dev-repo"); + mkdirSync(join(devDir, "skills", "some-skill"), { recursive: true }); + writeFileSync(join(devDir, "skills", "some-skill", "SKILL.md"), `--- +name: some-skill +description: A skill in a dev project +agents: [] +--- +# Some Skill +`); + + const skills = resolveProjectSkills([devDir]); + expect(skills).toHaveLength(0); + }); + + it("returns empty for a research project with no skills/ directory", () => { + const projectDir = join(tmpDir, "no-skills-proj"); + mkdirSync(projectDir); + writeFileSync(join(projectDir, "project.toml"), 'schema_version = 1\n'); + + const skills = resolveProjectSkills([projectDir]); + expect(skills).toHaveLength(0); + }); + + it("scans multiple research projects from workspace folders", () => { + const proj1 = join(tmpDir, "proj1"); + const proj2 = join(tmpDir, "proj2"); + mkdirSync(join(proj1, "skills", "skill-a"), { recursive: true }); + mkdirSync(join(proj2, "skills", "skill-b"), { recursive: true }); + writeFileSync(join(proj1, "project.toml"), 'schema_version = 1\n'); + writeFileSync(join(proj2, "project.toml"), 'schema_version = 1\n'); + writeFileSync(join(proj1, "skills", "skill-a", "SKILL.md"), `--- +name: skill-a +description: Skill A +--- +`); + writeFileSync(join(proj2, "skills", "skill-b", "SKILL.md"), `--- +name: skill-b +description: Skill B +--- +`); + + const skills = resolveProjectSkills([proj1, proj2]); + expect(skills).toHaveLength(2); + expect(skills.map((s) => s.name).sort()).toEqual(["skill-a", "skill-b"]); + }); +}); + +describe("mergeSkillEntries with project source", () => { + const shipped: SkillIndexEntry[] = [ + { source: "library", name: "debugging", description: "shipped debugging", path: "/lib/debugging/SKILL.md" }, + { source: "library", name: "tdd", description: "shipped tdd", path: "/lib/tdd/SKILL.md" }, + ]; + + it("project skills have highest priority (shadow custom, workspace, shipped)", () => { + const project: SkillIndexEntry[] = [ + { source: "project" as SkillIndexEntry["source"], name: "debugging", description: "project debugging", path: "/proj/skills/debugging/SKILL.md" }, + ]; + const merged = mergeSkillEntries(project, [], [], shipped); + const debug = merged.find((e) => e.name === "debugging"); + expect(debug).toBeDefined(); + expect(debug!.source).toBe("project"); + expect(debug!.description).toBe("project debugging"); + expect(debug!.overridesShipped).toBe(true); + }); + + it("falls back to existing priority when no project skills: custom > workspace > shipped", () => { + const custom: SkillIndexEntry[] = [ + { source: "custom", name: "debugging", description: "custom debugging", path: "/custom/debugging/SKILL.md" }, + ]; + const merged = mergeSkillEntries([], custom, [], shipped); + const debug = merged.find((e) => e.name === "debugging"); + expect(debug!.source).toBe("custom"); + expect(debug!.overridesShipped).toBe(true); + }); + + it("multi-project collision: first project in folder order wins", () => { + const project: SkillIndexEntry[] = [ + { source: "project" as SkillIndexEntry["source"], name: "analyze", description: "first", path: "/proj1/skills/analyze/SKILL.md" }, + { source: "project" as SkillIndexEntry["source"], name: "analyze", description: "second", path: "/proj2/skills/analyze/SKILL.md" }, + ]; + const merged = mergeSkillEntries(project, [], [], shipped); + const analyze = merged.find((e) => e.name === "analyze"); + expect(analyze!.description).toBe("first"); + }); +}); diff --git a/packages/extension/test/scores/user_skill_providers.test.ts b/packages/extension/test/scores/user_skill_providers.test.ts index bdea1896..79e8dca7 100644 --- a/packages/extension/test/scores/user_skill_providers.test.ts +++ b/packages/extension/test/scores/user_skill_providers.test.ts @@ -180,7 +180,7 @@ describe("mergeSkillEntries (issue #573 — shadow semantics: custom > workspace { source: "library", name: "transmon", description: "Transmon physics", path: "/lib/transmon/SKILL.md" }, ]; - const merged = mergeSkillEntries(custom, workspace, shipped); + const merged = mergeSkillEntries([], custom, workspace, shipped); // atoms from custom wins, transmon passes through expect(merged).toHaveLength(2); const atoms = merged.find((e) => e.name === "atoms")!; @@ -199,7 +199,7 @@ describe("mergeSkillEntries (issue #573 — shadow semantics: custom > workspace { source: "library", name: "tdd", description: "Standard TDD", path: "/lib/tdd/SKILL.md" }, ]; - const merged = mergeSkillEntries(custom, workspace, shipped); + const merged = mergeSkillEntries([], custom, workspace, shipped); expect(merged).toHaveLength(1); expect(merged[0].source).toBe("workspace"); expect(merged[0].description).toBe("Team TDD rules"); @@ -214,7 +214,7 @@ describe("mergeSkillEntries (issue #573 — shadow semantics: custom > workspace ]; const shipped: SkillIndexEntry[] = []; - const merged = mergeSkillEntries(custom, workspace, shipped); + const merged = mergeSkillEntries([], custom, workspace, shipped); expect(merged).toHaveLength(1); expect(merged[0].source).toBe("custom"); }); @@ -227,7 +227,7 @@ describe("mergeSkillEntries (issue #573 — shadow semantics: custom > workspace { source: "library", name: "atoms", description: "Original", path: "/lib/atoms/SKILL.md" }, ]; - const merged = mergeSkillEntries(custom, [], shipped); + const merged = mergeSkillEntries([], custom, [], shipped); const atoms = merged.find((e) => e.name === "atoms")!; // The entry should carry a flag indicating it overrides a platform skill expect((atoms as any).overridesShipped).toBe(true); From b61b2e217240a1826b96290a4f40429333b452de Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 13:53:03 -0400 Subject: [PATCH 07/22] feat: autoresearch director project integration (#670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stack_state.ts: inject '## Active Research Project' block into system prompt when AMICODE_WORKSPACE_FOLDERS contains a folder with project.toml (project name, question, status, path, layout summary, project paths) - autoresearch/SKILL.md: add project_contract frontmatter, project-aware path resolution table (campaign ledger, hypotheses, observations, scripts, data, config), vault fallback for non-project sessions - hypothesis-review/SKILL.md: project-aware hypothesis reading (check /ledger/hypotheses/ when project-bound, vault otherwise) - 3 new stack_state tests: project-bound, non-project, dev-project - director-core/SKILL.md update deferred: lives in harmoniqs/armonissima (read-only mount), requires a separate PR to that repo Partial close — closes the amicode-side work for #670. The armonissima PR (director-core LEDGER DISCOVERY RULE v2) is a follow-up. --- .../extension/opencode-plugin/stack_state.ts | 65 +++++++++++++++++++ .../extension/skills/autoresearch/SKILL.md | 23 ++++++- .../skills/hypothesis-review/SKILL.md | 10 +-- packages/extension/test/stack_state.test.ts | 48 ++++++++++++++ 4 files changed, 141 insertions(+), 5 deletions(-) diff --git a/packages/extension/opencode-plugin/stack_state.ts b/packages/extension/opencode-plugin/stack_state.ts index d735e4bc..46f401b8 100644 --- a/packages/extension/opencode-plugin/stack_state.ts +++ b/packages/extension/opencode-plugin/stack_state.ts @@ -607,6 +607,66 @@ function buildMemoryIndexSection(memoryIndexLines: string[]): string { // ── Public: compose the full per-session block ─────────────────────────────── +// ── Active Research Project injection (#670) ───────────────────────────────── + +/** Read workspace folders from AMICODE_WORKSPACE_FOLDERS (colon-separated, set + * by the extension at server-spool), find the first with a project.toml, and + * return a context block. Returns null if no research project is bound. */ +function buildActiveProjectSection(): string | null { + const raw = process.env.AMICODE_WORKSPACE_FOLDERS; + if (!raw || !raw.trim()) return null; + + const folders = raw.split(":").filter(Boolean); + for (const folder of folders) { + const tomlPath = path.join(folder, "project.toml"); + if (!fs.existsSync(tomlPath)) continue; + + // Minimal TOML parsing (no smol-toml in the Bun plugin runtime). + // Extract name, question, status from the flat top-level fields. + let tomlText: string; + try { + tomlText = fs.readFileSync(tomlPath, "utf8"); + } catch { + continue; + } + + const name = extractTomlString(tomlText, "name") ?? path.basename(folder); + const question = extractTomlString(tomlText, "question") ?? ""; + const status = extractTomlString(tomlText, "status") ?? "unknown"; + + const lines = [ + "## Active Research Project", + "", + `**${name}** (${status})`, + `**Question:** ${question}`, + `**Path:** \`${folder}\``, + "", + "Layout: `scripts/`, `data/{raw,processed,plots}`, `analysis/`, `paper/`, " + + "`ledger/{hypotheses,observations,campaigns}`, `config/`, `skills/`", + "", + "When operating on this project, use project paths (not vault paths):", + "- Hypotheses: `/ledger/hypotheses/`", + "- Campaign ledgers: `/ledger/campaigns/`", + "- Observations: `/ledger/observations/`", + "- Scripts: `/scripts/`", + "- Data: `/data/`", + "- Config: `/config/`", + "- Paper: `/paper/`", + "", + "The checkout registry (`CHECKOUTS.md`) stays vault-based (fleet-wide).", + ]; + return lines.join("\n"); + } + return null; +} + +/** Extract a simple string value from TOML text (regex, not a full parser). + * Only handles `key = "value"` at the top level. */ +function extractTomlString(text: string, key: string): string | undefined { + const m = text.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m")); + return m?.[1]; +} + /** Read the current stack state (solver mode, routing, active problem, live * runs, fleet, and the personal-vault user-memory sections) and compose a * markdown block to inject into the agent's system prompt. Returns null @@ -631,6 +691,11 @@ export function buildStackStateBlock(): string | null { const fleet = buildFleetSection(); if (fleet) parts.push(fleet); + // Active Research Project (#670): inject project metadata when a session + // is bound to a workspace folder with project.toml. + const project = buildActiveProjectSection(); + if (project) parts.push(project); + // User-memory sections (live reads from the personal vault — splice order // parity with the retired boot-time file splice: about → recent → demos → // mount stack → memory index). diff --git a/packages/extension/skills/autoresearch/SKILL.md b/packages/extension/skills/autoresearch/SKILL.md index 301a3781..debecc0f 100644 --- a/packages/extension/skills/autoresearch/SKILL.md +++ b/packages/extension/skills/autoresearch/SKILL.md @@ -7,13 +7,17 @@ vault_contract: folders: [sessions, experiments, specs] note_types: [session, experiment, spec, hypothesis] frontmatter: [session_id, status, tags] +project_contract: + folders: [ledger/hypotheses, ledger/observations, ledger/campaigns, scripts, data, config] --- # Autoresearch — the director's protocol > **Install conventions** — this skill references *your personal vault* (the writable > vault your Amicode studio mounts) for the session ledger, experiment notes, and specs, -> and your compute fleet where one exists. The protocol is engine- and install-neutral; +> and your compute fleet where one exists. **When operating on a Research Project** +> (detected by `project.toml` in the workspace), use project paths instead of vault paths +> — see the path resolution table below. The protocol is engine- and install-neutral; > bindings for a given engine stay engine-side (the opencode binding of the director role > is the `autoresearch` primary agent card). @@ -25,6 +29,23 @@ The operating principle: **the context window is a cache; the vault is the datab Every piece of load-bearing state lives in a vault note; the context window holds only the working set. Compaction (manual or auto) then costs nothing but a cache refill. +## Path resolution (project-aware) + +When the system prompt includes an `## Active Research Project` block (injected by +`stack_state.ts` when a workspace folder has `project.toml`), use project paths. +When no project is bound, use vault paths. The checkout registry is always vault-based +(fleet-wide coordination). + +| Artifact | Project-bound | Vault (no project) | +|----------|---------------|--------------------| +| Campaign ledger | `/ledger/campaigns/campaign--.md` | `/sessions/session--.md` | +| Hypotheses | `/ledger/hypotheses/` | `/hypotheses/` | +| Observations | `/ledger/observations/` | `/experiments/` | +| Scripts | `/scripts/` | (none — vault has no scripts) | +| Data | `/data/` | (none) | +| Config | `/config/` | (none) | +| Checkout registry | `/sessions/CHECKOUTS.md` | `/sessions/CHECKOUTS.md` | + ## The session ledger (create at kickoff, before any work) Path: `/sessions/session--.md`. Nine sections, in order: diff --git a/packages/extension/skills/hypothesis-review/SKILL.md b/packages/extension/skills/hypothesis-review/SKILL.md index f725fa33..88cf4d1a 100644 --- a/packages/extension/skills/hypothesis-review/SKILL.md +++ b/packages/extension/skills/hypothesis-review/SKILL.md @@ -10,13 +10,15 @@ vault_contract: # Hypothesis Review -Rank open vault hypotheses to identify the highest-value experiments. +Rank open hypotheses to identify the highest-value experiments. ## Process -1. Glob `hypotheses/` across **every mounted vault** in the Armonia stack (see the - `amico-vault` skill for the mount set and read precedence) for notes with - `status: open` or `status: untested` +1. **Locate hypotheses.** When the system prompt includes an `## Active Research + Project` block, read from `/ledger/hypotheses/`. Otherwise, glob + `hypotheses/` across **every mounted vault** in the Armonia stack (see the + `amico-vault` skill for the mount set and read precedence). Filter for notes + with `status: open` or `status: untested`. 2. For each hypothesis, read the body and assess: - **Impact if confirmed** (numeric): high=3 (unlocks fidelity breakthrough or new platform), medium=2 (improves existing result), low=1 (incremental or niche) - **Ease of test** (numeric): easy=3 (1 cold-start experiment), moderate=2 (needs specific setup or 2-3 experiments), hard=1 (requires engineering or new infrastructure) diff --git a/packages/extension/test/stack_state.test.ts b/packages/extension/test/stack_state.test.ts index e3e70a50..10ce00f3 100644 --- a/packages/extension/test/stack_state.test.ts +++ b/packages/extension/test/stack_state.test.ts @@ -249,6 +249,53 @@ describe("user-memory section text (parity oracle vs the retired user_splice.ts) }); }); +// ── Active Research Project injection (#670) ───────────────────────────────── + +describe("Active Research Project injection (#670)", () => { + it("project-bound session → '## Active Research Project' block in context", () => { + const projDir = mkTmp("research-proj-"); + fs.writeFileSync( + path.join(projDir, "project.toml"), + 'schema_version = 1\nname = "My Research"\nquestion = "Does it work?"\nstatus = "running"\n', + ); + const stubs = stubAllSeams({}); + process.env.AMICODE_WORKSPACE_FOLDERS = projDir; + try { + const block = buildStackStateBlock() ?? ""; + expect(block).toContain("## Active Research Project"); + expect(block).toContain("**My Research** (running)"); + expect(block).toContain("**Question:** Does it work?"); + expect(block).toContain(`**Path:** \`${projDir}\``); + expect(block).toContain("Hypotheses: `/ledger/hypotheses/`"); + } finally { + restoreSeams(stubs); + } + }); + + it("non-project session → no Active Research Project block", () => { + const stubs = stubAllSeams({}); + // No AMICODE_WORKSPACE_FOLDERS set (cleared by stubAllSeams) + try { + const block = buildStackStateBlock() ?? ""; + expect(block).not.toContain("## Active Research Project"); + } finally { + restoreSeams(stubs); + } + }); + + it("dev project (no project.toml) → no Active Research Project block", () => { + const devDir = mkTmp("dev-proj-"); + const stubs = stubAllSeams({}); + process.env.AMICODE_WORKSPACE_FOLDERS = devDir; + try { + const block = buildStackStateBlock() ?? ""; + expect(block).not.toContain("## Active Research Project"); + } finally { + restoreSeams(stubs); + } + }); +}); + // ── Caps + composition ─────────────────────────────────────────────────────── describe("caps + composition", () => { @@ -389,6 +436,7 @@ const SEAM_KEYS = [ "AMICODE_CONNECTIONS_FILE", "AMICODE_PROBLEMS_DIR", "AMICODE_RUNS_DIR", + "AMICODE_WORKSPACE_FOLDERS", ] as const; let fixtureRoot: string | undefined; From 96f3a0fd47abcbb6e72ff21f15de2a6cbbd92aac Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 13:56:42 -0400 Subject: [PATCH 08/22] =?UTF-8?q?feat:=20selector=20UI=20enrichment=20?= =?UTF-8?q?=E2=80=94=20type-grouped=20project=20selector=20(#667)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Un-gate the breadcrumb bar in new-session-view.tsx: remove !inAmicode() guard so the project selector renders inside the Amicode webview - Remove amicode#203 project-name suppression: project name now always shown in the trigger button - Add type grouping in the dropdown: Research section header over research projects, Development section header over dev projects - Add status badge for research projects (proposing/designing/running/ analyzing/writing/complete) in dropdown items - Extend PromptProject type with optional status field - Remove unused inAmicode import from both files - Update structural test to verify gate is removed (not present) Closes #667 --- .../components/prompt-project-selector.tsx | 48 +++++++++++++------ .../new-session/new-session-view.test.ts | 25 ++++------ .../pages/new-session/new-session-view.tsx | 9 ++-- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx index cef93234..470119c4 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx @@ -16,7 +16,6 @@ import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" import { getProjectAvatarVariant } from "@/context/layout" import { useLanguage } from "@/context/language" import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" -import { inAmicode } from "@/pages/session/use-amicode-commands" import { pathKey } from "@/utils/path-key" import { handleDocumentSearchKeydown } from "@/utils/search-keydown" import { createMenuDismissController } from "@/utils/menu-dismiss-controller" @@ -29,6 +28,7 @@ export type PromptProject = { icon?: { color?: string; url?: string; override?: string } server?: { key: string; name: string } type?: "research" | "dev" + status?: string // research project lifecycle status (proposing/designing/running/analyzing/writing/complete) } export type PromptProjectControls = { @@ -363,11 +363,27 @@ export function PromptProjectSelector(props: { when={props.controller.servers().length > 1} fallback={ - - {(project) => ( - - )} - + {/* amicode#663: type-grouped listing (Research then Dev) */} + p.type === "research")}> +
+ Research +
+ p.type === "research")}> + {(project) => ( + + )} + +
+ p.type !== "research")}> +
+ Development +
+ p.type !== "research")}> + {(project) => ( + + )} + +
} > @@ -494,14 +510,11 @@ function ProjectTrigger(props: ComponentProps<"button"> & { controller: PromptPr /> )} - {/* amicode#203 (Kate): don't show the project name in the chat — the folder - icon + chevron keep the switcher reachable without naming the project. - Only suppressed inside the amicode webview. */} - - - {project() ? displayName(project()!) : local.controller.labels.new()} - - + {/* amicode#663: project name now always shown — the selector is enriched + with type grouping and status badges, so the name provides context. */} + + {project() ? displayName(project()!) : local.controller.labels.new()} + ) @@ -542,6 +555,13 @@ function ProjectItem(props: { variant={getProjectAvatarVariant(props.project.icon?.color)} /> {displayName(props.project)} + {/* amicode#663: research status badge */} + + + {props.project.status} + + diff --git a/packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.test.ts b/packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.test.ts index 0b39f095..2f9a07db 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.test.ts @@ -2,25 +2,18 @@ import { describe, expect, test } from "bun:test" import { readFileSync } from "node:fs" import { join } from "node:path" -// amicode#326 — the breadcrumb bar (project selector + workspace selector + -// git status) below the new-session composer must be hidden when running inside -// the Amicode webview. Sessions already scope to all workspace folders via the -// multi-directory engine (opencode#215), so the bar is redundant chrome. -// -// Source assertion: the view must gate the breadcrumb section on !inAmicode(). -// No component-render harness exists (no @solidjs/testing-library), so we -// verify structurally — the same pattern as prompt-input-clipboard-structure.test.ts. +// amicode#663 — the breadcrumb bar (project selector + workspace selector + +// git status) below the new-session composer is now SHOWN in the Amicode +// webview, enriched with type grouping (Research/Dev). The !inAmicode() gate +// was removed in #667 (Selector UI enrichment). const source = readFileSync(join(import.meta.dir, "new-session-view.tsx"), "utf8") -describe("breadcrumb bar hidden in Amicode (amicode#326)", () => { - test("imports inAmicode", () => { - expect(source).toContain("inAmicode") +describe("breadcrumb bar shown in Amicode (#663, #667)", () => { + test("no longer imports inAmicode (gate removed)", () => { + expect(source).not.toContain("inAmicode") }) - test("the project-selected breadcrumb block is gated on !inAmicode()", () => { - // The breadcrumb row renders inside a - // that must also check !inAmicode(). We verify both conditions appear - // together in the guard. - expect(source).toMatch(/!inAmicode\(\).*project\.selected\(\)/) + test("the project-selected breadcrumb block is NOT gated on !inAmicode()", () => { + expect(source).not.toMatch(/!inAmicode\(\)/) }) }) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.tsx b/packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.tsx index b5472725..d049b27a 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.tsx +++ b/packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.tsx @@ -14,7 +14,6 @@ import { PromptProjectSelector, type PromptProjectController, } from "@/components/prompt-project-selector" -import { inAmicode } from "@/pages/session/use-amicode-commands" import { StatusPopoverV2 } from "@/components/status-popover" import { SessionChatsDropdown } from "@/components/session/session-header" import { useLanguage } from "@/context/language" @@ -74,11 +73,9 @@ export function NewSessionView(props: { - {/* amicode#326: hide the breadcrumb bar (project selector + - workspace selector + git status) inside the Amicode webview — - sessions already scope to all workspace folders via the - multi-directory engine (opencode#215). */} - + {/* amicode#663: un-gated — the project selector now renders inside + the Amicode webview with type grouping (Research/Dev). */} +
Date: Mon, 31 Aug 2026 14:45:42 -0400 Subject: [PATCH 09/22] feat: add reports/ folder to Research Project layout - Add reports/{weekly,presentations,milestones} to SCAFFOLD_DIRS - Ship reports/weekly/template.md with YAML frontmatter (date, author, period, status) and section headings (Progress, Key Results, Blockers, Next Week, Notes) - Update renderReadme() with reports/ line - Update stack_state.ts project injection: layout string + Reports path - Update CONTEXT.md Research Project definition with reports/ - Update ADR 0012 prescribed layout enumeration - Update autoresearch/SKILL.md project_contract + path table - 3 TDD cycles: scaffoldManifest (2 new tests), integration test (extended), stack_state injection (extended) --- CONTEXT.md | 6 +++- docs/adr/0012-research-projects.md | 2 +- packages/amico-run/src/project.ts | 31 +++++++++++++++++++ packages/amico-run/test/project_verb.test.ts | 25 ++++++++++++++- .../extension/opencode-plugin/stack_state.ts | 3 +- .../extension/skills/autoresearch/SKILL.md | 3 +- packages/extension/test/stack_state.test.ts | 2 ++ 7 files changed, 67 insertions(+), 5 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 3c9be23c..2ae75449 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -30,7 +30,7 @@ _Avoid_: User, tenant, owner (interchangeably) **Project**: The unit of work organization — a directory registered with amicode where files, solves, and Sessions live. Every Session belongs to exactly one Project. Two flavors: -- **Research Project** — identified by a `project.toml` manifest at its root. Prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. +- **Research Project** — identified by a `project.toml` manifest at its root. Prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. - **Dev Project** — the existing git-repo model; any registered directory without `project.toml`. Canonical default: `~/armonia/`. _Avoid_: Workspace, folder (as a concept name), repo, Study (rejected alternative — researchers think in "projects") @@ -121,6 +121,10 @@ _Avoid_: Dashboard (as the surface name), widget panel A sandboxed ES-module card rendered in an iframe within Home. Authored by the agent (`amicode_author_widget` tool) or shipped as a builtin. Communicates with the host via the bridge protocol (postMessage). Two size classes: hero (full panel width) and tile (half-width, 2-across). Each has a TOML manifest, a JS module, and optional config fields. _Avoid_: Card (ambiguous — the UI has many cards), tile (as the concept name — tile is a size class) +**Sidebar**: +The webview in the VS Code activity bar container, showing project navigation and system status. Contains action buttons (open chat, create project), a session-aware unified project tree (Research Projects with lifecycle metadata expanding into file trees; Dev Projects as plain expandable folders), and a collapsible fleet section (deferred). The sidebar is navigation chrome — it follows the active session's project binding but never drives session switching. Destinations open in the editor area. +_Avoid_: Explorer (VS Code's native file explorer is separate), Panel (the in-app dismissible drawer is a different concept) + ### Orthogonal axes **Domain Pack**: diff --git a/docs/adr/0012-research-projects.md b/docs/adr/0012-research-projects.md index aec68735..2e9b09a0 100644 --- a/docs/adr/0012-research-projects.md +++ b/docs/adr/0012-research-projects.md @@ -10,7 +10,7 @@ ## Decision -Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `project.toml`, with a prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." +Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `project.toml`, with a prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." ### Alternatives considered diff --git a/packages/amico-run/src/project.ts b/packages/amico-run/src/project.ts index 3e9199b2..9f1571c3 100644 --- a/packages/amico-run/src/project.ts +++ b/packages/amico-run/src/project.ts @@ -110,6 +110,9 @@ export const SCAFFOLD_DIRS = [ "ledger/observations", "ledger/literature", "ledger/campaigns", + "reports/weekly", + "reports/presentations", + "reports/milestones", "config", "skills", ] as const; @@ -309,11 +312,36 @@ ${question} - \`analysis/\` — analysis notebooks and post-processing - \`paper/\` — manuscript (\`outline.md\` → \`main.tex\`) - \`ledger/\` — hypotheses, observations, literature, and campaign logs +- \`reports/\` — weekly updates, presentations, and milestone reports - \`config/\` — system and lab configuration - \`skills/\` — project-specific Amico skills `; } +export const WEEKLY_REPORT_TEMPLATE = `--- +date: YYYY-MM-DD +author: +period: YYYY-MM-DD to YYYY-MM-DD +status: draft +--- + +# Weekly Update — [period] + +## Progress +- + +## Key Results +- + +## Blockers +- + +## Next Week +- + +## Notes +`; + export const ROOT_GITIGNORE = `# Data artifacts (large binary files) data/raw/**/*.jld2 data/raw/**/*.hdf5 @@ -379,5 +407,8 @@ export function scaffoldManifest( items.push({ path: "config/system.toml", content: "# System configuration\n" }); items.push({ path: "config/lab.toml", content: "# Lab configuration\n" }); + // reports/ + items.push({ path: "reports/weekly/template.md", content: WEEKLY_REPORT_TEMPLATE }); + return items; } diff --git a/packages/amico-run/test/project_verb.test.ts b/packages/amico-run/test/project_verb.test.ts index 9515077b..903a9417 100644 --- a/packages/amico-run/test/project_verb.test.ts +++ b/packages/amico-run/test/project_verb.test.ts @@ -159,6 +159,28 @@ describe("scaffoldManifest", () => { expect(files).toContain("config/system.toml"); expect(files).toContain("config/lab.toml"); }); + + it("produces reports/ dirs and weekly template", () => { + const manifest = scaffoldManifest(project); + const dirs = manifest.filter((m) => m.content === null).map((m) => m.path); + expect(dirs).toContain("reports/weekly"); + expect(dirs).toContain("reports/presentations"); + expect(dirs).toContain("reports/milestones"); + + const files = manifest.filter((m) => m.content !== null).map((m) => m.path); + expect(files).toContain("reports/weekly/template.md"); + }); + + it("weekly template has YAML frontmatter with date and period fields", () => { + const manifest = scaffoldManifest(project); + const template = manifest.find((m) => m.path === "reports/weekly/template.md"); + expect(template).toBeDefined(); + expect(template!.content).toContain("date:"); + expect(template!.content).toContain("period:"); + expect(template!.content).toContain("## Progress"); + expect(template!.content).toContain("## Key Results"); + expect(template!.content).toContain("## Blockers"); + }); }); // ── pure logic: TOML rendering round-trip ────────────────────────────────── @@ -298,7 +320,8 @@ describe("projectCreate", () => { for (const dir of ["scripts", "scripts/testbed", "data/raw", "data/processed", "data/plots", "analysis", "paper/figures", "paper/supplementary", "ledger/hypotheses", "ledger/observations", "ledger/literature", - "ledger/campaigns", "config", "skills"]) { + "ledger/campaigns", "reports/weekly", "reports/presentations", + "reports/milestones", "config", "skills"]) { expect(existsSync(join(projectDir, dir))).toBe(true); } diff --git a/packages/extension/opencode-plugin/stack_state.ts b/packages/extension/opencode-plugin/stack_state.ts index 46f401b8..854838a8 100644 --- a/packages/extension/opencode-plugin/stack_state.ts +++ b/packages/extension/opencode-plugin/stack_state.ts @@ -642,7 +642,7 @@ function buildActiveProjectSection(): string | null { `**Path:** \`${folder}\``, "", "Layout: `scripts/`, `data/{raw,processed,plots}`, `analysis/`, `paper/`, " + - "`ledger/{hypotheses,observations,campaigns}`, `config/`, `skills/`", + "`ledger/{hypotheses,observations,campaigns}`, `reports/{weekly,presentations,milestones}`, `config/`, `skills/`", "", "When operating on this project, use project paths (not vault paths):", "- Hypotheses: `/ledger/hypotheses/`", @@ -652,6 +652,7 @@ function buildActiveProjectSection(): string | null { "- Data: `/data/`", "- Config: `/config/`", "- Paper: `/paper/`", + "- Reports: `/reports/`", "", "The checkout registry (`CHECKOUTS.md`) stays vault-based (fleet-wide).", ]; diff --git a/packages/extension/skills/autoresearch/SKILL.md b/packages/extension/skills/autoresearch/SKILL.md index debecc0f..9abe35b5 100644 --- a/packages/extension/skills/autoresearch/SKILL.md +++ b/packages/extension/skills/autoresearch/SKILL.md @@ -8,7 +8,7 @@ vault_contract: note_types: [session, experiment, spec, hypothesis] frontmatter: [session_id, status, tags] project_contract: - folders: [ledger/hypotheses, ledger/observations, ledger/campaigns, scripts, data, config] + folders: [ledger/hypotheses, ledger/observations, ledger/campaigns, scripts, data, reports, config] --- # Autoresearch — the director's protocol @@ -44,6 +44,7 @@ When no project is bound, use vault paths. The checkout registry is always vault | Scripts | `/scripts/` | (none — vault has no scripts) | | Data | `/data/` | (none) | | Config | `/config/` | (none) | +| Reports | `/reports/` | (none) | | Checkout registry | `/sessions/CHECKOUTS.md` | `/sessions/CHECKOUTS.md` | ## The session ledger (create at kickoff, before any work) diff --git a/packages/extension/test/stack_state.test.ts b/packages/extension/test/stack_state.test.ts index 10ce00f3..e6f6c9ae 100644 --- a/packages/extension/test/stack_state.test.ts +++ b/packages/extension/test/stack_state.test.ts @@ -267,6 +267,8 @@ describe("Active Research Project injection (#670)", () => { expect(block).toContain("**Question:** Does it work?"); expect(block).toContain(`**Path:** \`${projDir}\``); expect(block).toContain("Hypotheses: `/ledger/hypotheses/`"); + expect(block).toContain("Reports: `/reports/`"); + expect(block).toContain("`reports/{weekly,presentations,milestones}`"); } finally { restoreSeams(stubs); } From a6150f28d0ef2a14430d94231cf37015b20f2937 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 14:58:17 -0400 Subject: [PATCH 10/22] =?UTF-8?q?fix:=20rename=20project.toml=20=E2=86=92?= =?UTF-8?q?=20.amico=20=E2=80=94=20avoid=20Julia=20Project.toml=20collisio?= =?UTF-8?q?n=20on=20macOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Julia projects use Project.toml (capital P). On macOS's case-insensitive filesystem, our project.toml collides with it — existsSync('project.toml') finds 'Project.toml' and the import silently skips writing the manifest. Every Julia-based research project hits this. Rename to .amico: hidden dotfile, zero ecosystem collision, short. Updated across 13 files: - detect.ts: check for .amico instead of project.toml - project.ts + project_verb.ts: scaffold .amico, idempotency checks - stack_state.ts: project injection reads .amico - user_skill_providers.ts + opencode_config.ts: skill discovery - CONTEXT.md + ADR 0012: glossary and decision text - autoresearch/SKILL.md: project_contract references - All test files (detect, project_verb, stack_state, project_skills) 82 tests green across both packages. --- CONTEXT.md | 4 ++-- docs/adr/0012-research-projects.md | 10 +++++----- packages/amico-run/src/project.ts | 4 ++-- packages/amico-run/src/project_verb.ts | 8 ++++---- packages/amico-run/test/project_verb.test.ts | 20 +++++++++---------- .../extension/opencode-plugin/stack_state.ts | 6 +++--- .../extension/skills/autoresearch/SKILL.md | 4 ++-- packages/extension/src/opencode_config.ts | 2 +- packages/extension/src/project/detect.ts | 8 ++++---- .../src/scores/user_skill_providers.ts | 2 +- .../extension/test/project/detect.test.ts | 19 +++++++++--------- .../test/scores/project_skills.test.ts | 12 +++++------ packages/extension/test/stack_state.test.ts | 4 ++-- 13 files changed, 51 insertions(+), 52 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 2ae75449..d7ef80d3 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -30,8 +30,8 @@ _Avoid_: User, tenant, owner (interchangeably) **Project**: The unit of work organization — a directory registered with amicode where files, solves, and Sessions live. Every Session belongs to exactly one Project. Two flavors: -- **Research Project** — identified by a `project.toml` manifest at its root. Prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. -- **Dev Project** — the existing git-repo model; any registered directory without `project.toml`. Canonical default: `~/armonia/`. +- **Research Project** — identified by a `.amico` manifest at its root. Prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. +- **Dev Project** — the existing git-repo model; any registered directory without `.amico`. Canonical default: `~/armonia/`. _Avoid_: Workspace, folder (as a concept name), repo, Study (rejected alternative — researchers think in "projects") diff --git a/docs/adr/0012-research-projects.md b/docs/adr/0012-research-projects.md index 2e9b09a0..4714477d 100644 --- a/docs/adr/0012-research-projects.md +++ b/docs/adr/0012-research-projects.md @@ -10,7 +10,7 @@ ## Decision -Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `project.toml`, with a prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." +Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `.amico`, with a prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." ### Alternatives considered @@ -19,8 +19,8 @@ Introduce "Research Project" as a structured flavor of the existing Project conc ### Key structural decisions -- **Workspace-backed selector.** The project selector is un-gated in the Amicode webview, shows workspace folders typed by `project.toml` presence, and grouped into Research and Development sections. Adding a project adds it to the VS Code workspace. Add-only; removal uses VS Code's native UI. -- **Project-specific skills.** Each research project can ship a `skills/` directory. On workspace folder changes, the extension scans for `project.toml` and adds each project's `skills/` as a skill source, triggering a `prepareOpencodeProject()` re-run. Merge priority: project > custom > workspace > shipped. +- **Workspace-backed selector.** The project selector is un-gated in the Amicode webview, shows workspace folders typed by `.amico` presence, and grouped into Research and Development sections. Adding a project adds it to the VS Code workspace. Add-only; removal uses VS Code's native UI. +- **Project-specific skills.** Each research project can ship a `skills/` directory. On workspace folder changes, the extension scans for `.amico` and adds each project's `skills/` as a skill source, triggering a `prepareOpencodeProject()` re-run. Merge priority: project > custom > workspace > shipped. - **Gated paper-writing.** A shipped `paper-writer` skill enforces an outline-first gate: the agent refuses to write paper content until `paper/outline.md` is finalized by the user. Formatting proceeds section-by-section with user approval. Content provenance is maintained (every paragraph traces to an outline bullet). The agent does not generate novel claims, interpret results, or write the abstract. - **Campaign ledgers.** Autoresearch execution records use `ledger/campaigns/` within the project (not vault `sessions/`), avoiding collision with the Session glossary term (one agent conversation). - **Armonia untouched for now.** Armonia's current CONTEXT.md definition, ArmoniaService, and sidebar panel remain as-is. The future vision is to reposition Armonia as a multi-collaborator coordination layer, but that is a separate decision gated on this one succeeding. @@ -30,7 +30,7 @@ Introduce "Research Project" as a structured flavor of the existing Project conc ### What changes - CONTEXT.md gains Research Project and Dev Project as flavor definitions under Project -- `ProjectTable` gains `project.toml` awareness for type detection +- `ProjectTable` gains `.amico` awareness for type detection - `PromptProjectSelector` is un-gated in the Amicode webview, enriched with type grouping and research metadata - Skill merge chain extended with project source: project > custom > workspace > shipped - `prepareOpencodeProject()` triggered on workspace folder changes (project add/remove) @@ -51,4 +51,4 @@ Skill auto-loading and session binding require changes to the opencode engine (v ### Reversibility -If Research Projects don't get adoption, the rollback is low-cost: `project.toml` is additive (ignore it and the directory is a regular git repo), the selector enrichment is behind the existing `!inAmicode()` gate (re-enable it), and the skill auto-loading is a no-op when no projects have `skills/`. The CONTEXT.md glossary additions are the hardest to undo but are also the lowest-risk (they extend, not replace). Armonia's current definition is untouched by this decision. +If Research Projects don't get adoption, the rollback is low-cost: `.amico` is additive (ignore it and the directory is a regular git repo), the selector enrichment is behind the existing `!inAmicode()` gate (re-enable it), and the skill auto-loading is a no-op when no projects have `skills/`. The CONTEXT.md glossary additions are the hardest to undo but are also the lowest-risk (they extend, not replace). Armonia's current definition is untouched by this decision. diff --git a/packages/amico-run/src/project.ts b/packages/amico-run/src/project.ts index 9f1571c3..f1efa510 100644 --- a/packages/amico-run/src/project.ts +++ b/packages/amico-run/src/project.ts @@ -59,7 +59,7 @@ const REQUIRED_FIELDS: (keyof ProjectToml)[] = [ export function validateProjectToml(data: unknown): ValidationResult { if (typeof data !== "object" || data === null) { - return { ok: false, errors: ["project.toml must be a TOML table (object)"] }; + return { ok: false, errors: [".amico manifest must be a TOML table (object)"] }; } const obj = data as Record; @@ -389,7 +389,7 @@ export function scaffoldManifest( } // root files - items.push({ path: "project.toml", content: renderProjectToml(p) }); + items.push({ path: ".amico", content: renderProjectToml(p) }); items.push({ path: "README.md", content: renderReadme(p.name, p.question) }); items.push({ path: ".gitignore", content: ROOT_GITIGNORE }); diff --git a/packages/amico-run/src/project_verb.ts b/packages/amico-run/src/project_verb.ts index 917c8338..785588b6 100644 --- a/packages/amico-run/src/project_verb.ts +++ b/packages/amico-run/src/project_verb.ts @@ -51,8 +51,8 @@ export function projectCreate(argv: string[]): VerbResult { const defaultPath = join(homedir(), "projects", slug); const projectDir = resolve(flagValue(argv, "--path") ?? defaultPath); - // Idempotent: if project.toml already exists, validate and return - const tomlPath = join(projectDir, "project.toml"); + // Idempotent: if .amico already exists, validate and return + const tomlPath = join(projectDir, ".amico"); if (existsSync(tomlPath)) { try { const existing = parseToml(readFileSync(tomlPath, "utf8")) as unknown as ProjectToml; @@ -167,9 +167,9 @@ export function projectImport(argv: string[]): VerbResult { if (!existsSync(dir)) return fail(`directory not found: ${dir}`); - const tomlPath = join(dir, "project.toml"); + const tomlPath = join(dir, ".amico"); - // Idempotent: if project.toml already exists, validate and return + // Idempotent: if .amico already exists, validate and return if (existsSync(tomlPath)) { try { const existing = parseToml(readFileSync(tomlPath, "utf8")) as unknown as ProjectToml; diff --git a/packages/amico-run/test/project_verb.test.ts b/packages/amico-run/test/project_verb.test.ts index 903a9417..e8c2a02f 100644 --- a/packages/amico-run/test/project_verb.test.ts +++ b/packages/amico-run/test/project_verb.test.ts @@ -36,12 +36,12 @@ describe("validateProjectToml", () => { domain_pack: { name: "quantum-control" }, }; - it("accepts a valid project.toml with all required fields", () => { + it("accepts a valid .amico with all required fields", () => { const result = validateProjectToml(valid); expect(result.ok).toBe(true); }); - it("accepts a minimal project.toml (required fields only)", () => { + it("accepts a minimal .amico (required fields only)", () => { const minimal: ProjectToml = { schema_version: 1, name: "My Project", @@ -129,10 +129,10 @@ describe("scaffoldManifest", () => { } }); - it("produces project.toml, README.md, .gitignore at root", () => { + it("produces .amico, README.md, .gitignore at root", () => { const manifest = scaffoldManifest(project); const files = manifest.filter((m) => m.content !== null).map((m) => m.path); - expect(files).toContain("project.toml"); + expect(files).toContain(".amico"); expect(files).toContain("README.md"); expect(files).toContain(".gitignore"); }); @@ -310,9 +310,9 @@ describe("projectCreate", () => { expect((result.json as Record).created).toBe(true); expect(result.code).toBe(0); - // Verify project.toml exists and is valid - expect(existsSync(join(projectDir, "project.toml"))).toBe(true); - const tomlContent = readFileSync(join(projectDir, "project.toml"), "utf8"); + // Verify .amico exists and is valid + expect(existsSync(join(projectDir, ".amico"))).toBe(true); + const tomlContent = readFileSync(join(projectDir, ".amico"), "utf8"); expect(tomlContent).toContain("My Test Project"); expect(tomlContent).toContain("my-test-project"); @@ -381,7 +381,7 @@ describe("projectImport", () => { rmSync(tmpDir, { recursive: true, force: true }); }); - it("creates project.toml and scaffolds missing dirs in an existing directory", () => { + it("creates .amico and scaffolds missing dirs in an existing directory", () => { const existingDir = join(tmpDir, "existing-project"); mkdirSync(join(existingDir, "scripts"), { recursive: true }); writeFileSync(join(existingDir, "scripts/my_solve.jl"), "# existing file"); @@ -391,8 +391,8 @@ describe("projectImport", () => { expect((result.json as Record).imported).toBe(true); expect(result.code).toBe(0); - // project.toml was created - expect(existsSync(join(existingDir, "project.toml"))).toBe(true); + // .amico was created + expect(existsSync(join(existingDir, ".amico"))).toBe(true); // Existing file was NOT overwritten expect(readFileSync(join(existingDir, "scripts/my_solve.jl"), "utf8")).toBe("# existing file"); diff --git a/packages/extension/opencode-plugin/stack_state.ts b/packages/extension/opencode-plugin/stack_state.ts index 854838a8..50efe460 100644 --- a/packages/extension/opencode-plugin/stack_state.ts +++ b/packages/extension/opencode-plugin/stack_state.ts @@ -610,7 +610,7 @@ function buildMemoryIndexSection(memoryIndexLines: string[]): string { // ── Active Research Project injection (#670) ───────────────────────────────── /** Read workspace folders from AMICODE_WORKSPACE_FOLDERS (colon-separated, set - * by the extension at server-spool), find the first with a project.toml, and + * by the extension at server-spool), find the first with a .amico, and * return a context block. Returns null if no research project is bound. */ function buildActiveProjectSection(): string | null { const raw = process.env.AMICODE_WORKSPACE_FOLDERS; @@ -618,7 +618,7 @@ function buildActiveProjectSection(): string | null { const folders = raw.split(":").filter(Boolean); for (const folder of folders) { - const tomlPath = path.join(folder, "project.toml"); + const tomlPath = path.join(folder, ".amico"); if (!fs.existsSync(tomlPath)) continue; // Minimal TOML parsing (no smol-toml in the Bun plugin runtime). @@ -693,7 +693,7 @@ export function buildStackStateBlock(): string | null { if (fleet) parts.push(fleet); // Active Research Project (#670): inject project metadata when a session - // is bound to a workspace folder with project.toml. + // is bound to a workspace folder with .amico. const project = buildActiveProjectSection(); if (project) parts.push(project); diff --git a/packages/extension/skills/autoresearch/SKILL.md b/packages/extension/skills/autoresearch/SKILL.md index 9abe35b5..496fecc0 100644 --- a/packages/extension/skills/autoresearch/SKILL.md +++ b/packages/extension/skills/autoresearch/SKILL.md @@ -16,7 +16,7 @@ project_contract: > **Install conventions** — this skill references *your personal vault* (the writable > vault your Amicode studio mounts) for the session ledger, experiment notes, and specs, > and your compute fleet where one exists. **When operating on a Research Project** -> (detected by `project.toml` in the workspace), use project paths instead of vault paths +> (detected by `.amico` in the workspace), use project paths instead of vault paths > — see the path resolution table below. The protocol is engine- and install-neutral; > bindings for a given engine stay engine-side (the opencode binding of the director role > is the `autoresearch` primary agent card). @@ -32,7 +32,7 @@ working set. Compaction (manual or auto) then costs nothing but a cache refill. ## Path resolution (project-aware) When the system prompt includes an `## Active Research Project` block (injected by -`stack_state.ts` when a workspace folder has `project.toml`), use project paths. +`stack_state.ts` when a workspace folder has `.amico`), use project paths. When no project is bound, use vault paths. The checkout registry is always vault-based (fleet-wide coordination). diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 9e8241b5..211ff731 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -557,7 +557,7 @@ export interface OpencodeConfigOptions { * Auto-loaded skills labeled (workspace). Undefined = no workspace skills. */ workspaceSkillsDir?: string; /** Absolute paths to workspace folders (VS Code multi-root). Used for - * research-project skill discovery (#668): each folder with project.toml + * research-project skill discovery (#668): each folder with .amico * has its skills/ scanned. Undefined = no project skills. */ workspaceFolders?: string[]; /** Stable project dir to (re)use across activations; created if missing and diff --git a/packages/extension/src/project/detect.ts b/packages/extension/src/project/detect.ts index c892bcbc..dd5e83d8 100644 --- a/packages/extension/src/project/detect.ts +++ b/packages/extension/src/project/detect.ts @@ -1,5 +1,5 @@ // detect.ts — project type detection (#666). -// A Research Project is identified by a `project.toml` at its root. +// A Research Project is identified by a `.amico` manifest at its root. // Detection is by file presence only (fast stat, no content parsing). import { existsSync } from "node:fs"; import { join } from "node:path"; @@ -8,16 +8,16 @@ export type ProjectType = "research" | "dev"; /** * Detect whether a directory is a Research Project or a Dev Project. - * A Research Project has a `project.toml` manifest at its root. + * A Research Project has a `.amico` manifest at its root. * Everything else is a Dev Project (the existing git-repo model). * * Re-evaluated on each call — no caching — so a directory that gains - * `project.toml` after initial registration updates its type on next + * `.amico` after initial registration updates its type on next * resolution. */ export function detectProjectType(dir: string): ProjectType { try { - return existsSync(join(dir, "project.toml")) ? "research" : "dev"; + return existsSync(join(dir, ".amico")) ? "research" : "dev"; } catch { return "dev"; } diff --git a/packages/extension/src/scores/user_skill_providers.ts b/packages/extension/src/scores/user_skill_providers.ts index 15a8e1df..fbb0965b 100644 --- a/packages/extension/src/scores/user_skill_providers.ts +++ b/packages/extension/src/scores/user_skill_providers.ts @@ -87,7 +87,7 @@ export function resolveWorkspaceSkills(wsSkillsDir: string): SkillIndexEntry[] { } /** Resolve project skills from research project workspace folders (#668). - * For each folder with `project.toml`, scan its `skills/` directory. + * For each folder with `.amico`, scan its `skills/` directory. * Non-research directories are skipped. Order-preserving: first folder wins * on name collision (consistent with workspace-folder order). */ export function resolveProjectSkills(workspaceFolders: string[]): SkillIndexEntry[] { diff --git a/packages/extension/test/project/detect.test.ts b/packages/extension/test/project/detect.test.ts index c1cbd4ab..935a6b27 100644 --- a/packages/extension/test/project/detect.test.ts +++ b/packages/extension/test/project/detect.test.ts @@ -1,5 +1,5 @@ // Project type detection tests — #666. -// detectProjectType(dir) returns "research" if project.toml exists, "dev" otherwise. +// detectProjectType(dir) returns "research" if .amico exists, "dev" otherwise. import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -18,12 +18,12 @@ describe("detectProjectType", () => { rmSync(tmpDir, { recursive: true, force: true }); }); - it("returns 'research' when project.toml exists at the root", () => { - writeFileSync(join(tmpDir, "project.toml"), 'schema_version = 1\nname = "test"\n'); + it("returns 'research' when .amico exists at the root", () => { + writeFileSync(join(tmpDir, ".amico"), 'schema_version = 1\nname = "test"\n'); expect(detectProjectType(tmpDir)).toBe("research"); }); - it("returns 'dev' when project.toml does not exist", () => { + it("returns 'dev' when .amico does not exist", () => { expect(detectProjectType(tmpDir)).toBe("dev"); }); @@ -31,9 +31,9 @@ describe("detectProjectType", () => { expect(detectProjectType(join(tmpDir, "nope"))).toBe("dev"); }); - it("detects type change: directory gains project.toml after first resolution", () => { + it("detects type change: directory gains .amico after first resolution", () => { expect(detectProjectType(tmpDir)).toBe("dev"); - writeFileSync(join(tmpDir, "project.toml"), 'schema_version = 1\n'); + writeFileSync(join(tmpDir, ".amico"), 'schema_version = 1\n'); expect(detectProjectType(tmpDir)).toBe("research"); }); }); @@ -45,9 +45,8 @@ describe("listProjectDirs with type detection", () => { beforeEach(() => { parentDir = mkdtempSync(join(tmpdir(), "amicode-list-")); - // Create two project directories mkdirSync(join(parentDir, "research-proj")); - writeFileSync(join(parentDir, "research-proj", "project.toml"), 'schema_version = 1\n'); + writeFileSync(join(parentDir, "research-proj", ".amico"), 'schema_version = 1\n'); mkdirSync(join(parentDir, "dev-proj")); }); @@ -55,14 +54,14 @@ describe("listProjectDirs with type detection", () => { rmSync(parentDir, { recursive: true, force: true }); }); - it("returns type 'research' for project with project.toml", () => { + it("returns type 'research' for project with .amico", () => { const projects = listProjectDirs(parentDir); const research = projects.find((p) => p.slug === "research-proj"); expect(research).toBeDefined(); expect(research!.type).toBe("research"); }); - it("returns type 'dev' for project without project.toml", () => { + it("returns type 'dev' for project without .amico", () => { const projects = listProjectDirs(parentDir); const dev = projects.find((p) => p.slug === "dev-proj"); expect(dev).toBeDefined(); diff --git a/packages/extension/test/scores/project_skills.test.ts b/packages/extension/test/scores/project_skills.test.ts index 2efb9f75..2b370851 100644 --- a/packages/extension/test/scores/project_skills.test.ts +++ b/packages/extension/test/scores/project_skills.test.ts @@ -1,5 +1,5 @@ // Skill auto-loading from research project directories — #668. -// resolveProjectSkills scans workspace folders with project.toml for skills/. +// resolveProjectSkills scans workspace folders with .amico for skills/. // mergeSkillEntries extended with project source (highest priority). import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; @@ -25,7 +25,7 @@ describe("resolveProjectSkills", () => { it("discovers skills from a research project's skills/ directory", () => { const projectDir = join(tmpDir, "my-research"); mkdirSync(join(projectDir, "skills", "my-analysis"), { recursive: true }); - writeFileSync(join(projectDir, "project.toml"), 'schema_version = 1\n'); + writeFileSync(join(projectDir, ".amico"), 'schema_version = 1\n'); writeFileSync(join(projectDir, "skills", "my-analysis", "SKILL.md"), `--- name: my-analysis description: Custom analysis skill for this project @@ -42,7 +42,7 @@ surface: public expect(skills[0].description).toBe("Custom analysis skill for this project"); }); - it("ignores directories without project.toml (dev projects)", () => { + it("ignores directories without .amico (dev projects)", () => { const devDir = join(tmpDir, "dev-repo"); mkdirSync(join(devDir, "skills", "some-skill"), { recursive: true }); writeFileSync(join(devDir, "skills", "some-skill", "SKILL.md"), `--- @@ -60,7 +60,7 @@ agents: [] it("returns empty for a research project with no skills/ directory", () => { const projectDir = join(tmpDir, "no-skills-proj"); mkdirSync(projectDir); - writeFileSync(join(projectDir, "project.toml"), 'schema_version = 1\n'); + writeFileSync(join(projectDir, ".amico"), 'schema_version = 1\n'); const skills = resolveProjectSkills([projectDir]); expect(skills).toHaveLength(0); @@ -71,8 +71,8 @@ agents: [] const proj2 = join(tmpDir, "proj2"); mkdirSync(join(proj1, "skills", "skill-a"), { recursive: true }); mkdirSync(join(proj2, "skills", "skill-b"), { recursive: true }); - writeFileSync(join(proj1, "project.toml"), 'schema_version = 1\n'); - writeFileSync(join(proj2, "project.toml"), 'schema_version = 1\n'); + writeFileSync(join(proj1, ".amico"), 'schema_version = 1\n'); + writeFileSync(join(proj2, ".amico"), 'schema_version = 1\n'); writeFileSync(join(proj1, "skills", "skill-a", "SKILL.md"), `--- name: skill-a description: Skill A diff --git a/packages/extension/test/stack_state.test.ts b/packages/extension/test/stack_state.test.ts index e6f6c9ae..6b6d77a4 100644 --- a/packages/extension/test/stack_state.test.ts +++ b/packages/extension/test/stack_state.test.ts @@ -255,7 +255,7 @@ describe("Active Research Project injection (#670)", () => { it("project-bound session → '## Active Research Project' block in context", () => { const projDir = mkTmp("research-proj-"); fs.writeFileSync( - path.join(projDir, "project.toml"), + path.join(projDir, ".amico"), 'schema_version = 1\nname = "My Research"\nquestion = "Does it work?"\nstatus = "running"\n', ); const stubs = stubAllSeams({}); @@ -285,7 +285,7 @@ describe("Active Research Project injection (#670)", () => { } }); - it("dev project (no project.toml) → no Active Research Project block", () => { + it("dev project (no .amico) → no Active Research Project block", () => { const devDir = mkTmp("dev-proj-"); const stubs = stubAllSeams({}); process.env.AMICODE_WORKSPACE_FOLDERS = devDir; From 55871706ebacc6a3988216b93e09ae84a6d2376e Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:03:13 -0400 Subject: [PATCH 11/22] =?UTF-8?q?fix:=20rename=20.amico=20=E2=86=92=20rese?= =?UTF-8?q?arch-project.toml=20=E2=80=94=20visible,=20self-documenting=20m?= =?UTF-8?q?anifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .amico was too opaque for a manifest researchers should see and understand. research-project.toml is explicit, visible in the tree, self-documenting ('what is this file?'), and still zero collision with Julia's Project.toml. Updated across 13 files (same set as the previous rename). --- CONTEXT.md | 4 ++-- docs/adr/0012-research-projects.md | 10 +++++----- packages/amico-run/src/project.ts | 4 ++-- packages/amico-run/src/project_verb.ts | 8 ++++---- packages/amico-run/test/project_verb.test.ts | 8 ++++---- .../extension/opencode-plugin/stack_state.ts | 6 +++--- .../extension/skills/autoresearch/SKILL.md | 4 ++-- packages/extension/src/opencode_config.ts | 2 +- packages/extension/src/project/detect.ts | 8 ++++---- .../src/scores/user_skill_providers.ts | 2 +- packages/extension/test/project/detect.test.ts | 18 +++++++++--------- .../test/scores/project_skills.test.ts | 8 ++++---- packages/extension/test/stack_state.test.ts | 4 ++-- 13 files changed, 43 insertions(+), 43 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index d7ef80d3..35218638 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -30,8 +30,8 @@ _Avoid_: User, tenant, owner (interchangeably) **Project**: The unit of work organization — a directory registered with amicode where files, solves, and Sessions live. Every Session belongs to exactly one Project. Two flavors: -- **Research Project** — identified by a `.amico` manifest at its root. Prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. -- **Dev Project** — the existing git-repo model; any registered directory without `.amico`. Canonical default: `~/armonia/`. +- **Research Project** — identified by a `research-project.toml` manifest at its root. Prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. +- **Dev Project** — the existing git-repo model; any registered directory without `research-project.toml`. Canonical default: `~/armonia/`. _Avoid_: Workspace, folder (as a concept name), repo, Study (rejected alternative — researchers think in "projects") diff --git a/docs/adr/0012-research-projects.md b/docs/adr/0012-research-projects.md index 4714477d..57127009 100644 --- a/docs/adr/0012-research-projects.md +++ b/docs/adr/0012-research-projects.md @@ -10,7 +10,7 @@ ## Decision -Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `.amico`, with a prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." +Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `research-project.toml`, with a prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." ### Alternatives considered @@ -19,8 +19,8 @@ Introduce "Research Project" as a structured flavor of the existing Project conc ### Key structural decisions -- **Workspace-backed selector.** The project selector is un-gated in the Amicode webview, shows workspace folders typed by `.amico` presence, and grouped into Research and Development sections. Adding a project adds it to the VS Code workspace. Add-only; removal uses VS Code's native UI. -- **Project-specific skills.** Each research project can ship a `skills/` directory. On workspace folder changes, the extension scans for `.amico` and adds each project's `skills/` as a skill source, triggering a `prepareOpencodeProject()` re-run. Merge priority: project > custom > workspace > shipped. +- **Workspace-backed selector.** The project selector is un-gated in the Amicode webview, shows workspace folders typed by `research-project.toml` presence, and grouped into Research and Development sections. Adding a project adds it to the VS Code workspace. Add-only; removal uses VS Code's native UI. +- **Project-specific skills.** Each research project can ship a `skills/` directory. On workspace folder changes, the extension scans for `research-project.toml` and adds each project's `skills/` as a skill source, triggering a `prepareOpencodeProject()` re-run. Merge priority: project > custom > workspace > shipped. - **Gated paper-writing.** A shipped `paper-writer` skill enforces an outline-first gate: the agent refuses to write paper content until `paper/outline.md` is finalized by the user. Formatting proceeds section-by-section with user approval. Content provenance is maintained (every paragraph traces to an outline bullet). The agent does not generate novel claims, interpret results, or write the abstract. - **Campaign ledgers.** Autoresearch execution records use `ledger/campaigns/` within the project (not vault `sessions/`), avoiding collision with the Session glossary term (one agent conversation). - **Armonia untouched for now.** Armonia's current CONTEXT.md definition, ArmoniaService, and sidebar panel remain as-is. The future vision is to reposition Armonia as a multi-collaborator coordination layer, but that is a separate decision gated on this one succeeding. @@ -30,7 +30,7 @@ Introduce "Research Project" as a structured flavor of the existing Project conc ### What changes - CONTEXT.md gains Research Project and Dev Project as flavor definitions under Project -- `ProjectTable` gains `.amico` awareness for type detection +- `ProjectTable` gains `research-project.toml` awareness for type detection - `PromptProjectSelector` is un-gated in the Amicode webview, enriched with type grouping and research metadata - Skill merge chain extended with project source: project > custom > workspace > shipped - `prepareOpencodeProject()` triggered on workspace folder changes (project add/remove) @@ -51,4 +51,4 @@ Skill auto-loading and session binding require changes to the opencode engine (v ### Reversibility -If Research Projects don't get adoption, the rollback is low-cost: `.amico` is additive (ignore it and the directory is a regular git repo), the selector enrichment is behind the existing `!inAmicode()` gate (re-enable it), and the skill auto-loading is a no-op when no projects have `skills/`. The CONTEXT.md glossary additions are the hardest to undo but are also the lowest-risk (they extend, not replace). Armonia's current definition is untouched by this decision. +If Research Projects don't get adoption, the rollback is low-cost: `research-project.toml` is additive (ignore it and the directory is a regular git repo), the selector enrichment is behind the existing `!inAmicode()` gate (re-enable it), and the skill auto-loading is a no-op when no projects have `skills/`. The CONTEXT.md glossary additions are the hardest to undo but are also the lowest-risk (they extend, not replace). Armonia's current definition is untouched by this decision. diff --git a/packages/amico-run/src/project.ts b/packages/amico-run/src/project.ts index f1efa510..0ebc7a73 100644 --- a/packages/amico-run/src/project.ts +++ b/packages/amico-run/src/project.ts @@ -59,7 +59,7 @@ const REQUIRED_FIELDS: (keyof ProjectToml)[] = [ export function validateProjectToml(data: unknown): ValidationResult { if (typeof data !== "object" || data === null) { - return { ok: false, errors: [".amico manifest must be a TOML table (object)"] }; + return { ok: false, errors: ["research-project.toml must be a TOML table (object)"] }; } const obj = data as Record; @@ -389,7 +389,7 @@ export function scaffoldManifest( } // root files - items.push({ path: ".amico", content: renderProjectToml(p) }); + items.push({ path: "research-project.toml", content: renderProjectToml(p) }); items.push({ path: "README.md", content: renderReadme(p.name, p.question) }); items.push({ path: ".gitignore", content: ROOT_GITIGNORE }); diff --git a/packages/amico-run/src/project_verb.ts b/packages/amico-run/src/project_verb.ts index 785588b6..c75802b6 100644 --- a/packages/amico-run/src/project_verb.ts +++ b/packages/amico-run/src/project_verb.ts @@ -51,8 +51,8 @@ export function projectCreate(argv: string[]): VerbResult { const defaultPath = join(homedir(), "projects", slug); const projectDir = resolve(flagValue(argv, "--path") ?? defaultPath); - // Idempotent: if .amico already exists, validate and return - const tomlPath = join(projectDir, ".amico"); + // Idempotent: if research-project.toml already exists, validate and return + const tomlPath = join(projectDir, "research-project.toml"); if (existsSync(tomlPath)) { try { const existing = parseToml(readFileSync(tomlPath, "utf8")) as unknown as ProjectToml; @@ -167,9 +167,9 @@ export function projectImport(argv: string[]): VerbResult { if (!existsSync(dir)) return fail(`directory not found: ${dir}`); - const tomlPath = join(dir, ".amico"); + const tomlPath = join(dir, "research-project.toml"); - // Idempotent: if .amico already exists, validate and return + // Idempotent: if research-project.toml already exists, validate and return if (existsSync(tomlPath)) { try { const existing = parseToml(readFileSync(tomlPath, "utf8")) as unknown as ProjectToml; diff --git a/packages/amico-run/test/project_verb.test.ts b/packages/amico-run/test/project_verb.test.ts index e8c2a02f..605ee7a4 100644 --- a/packages/amico-run/test/project_verb.test.ts +++ b/packages/amico-run/test/project_verb.test.ts @@ -132,7 +132,7 @@ describe("scaffoldManifest", () => { it("produces .amico, README.md, .gitignore at root", () => { const manifest = scaffoldManifest(project); const files = manifest.filter((m) => m.content !== null).map((m) => m.path); - expect(files).toContain(".amico"); + expect(files).toContain("research-project.toml"); expect(files).toContain("README.md"); expect(files).toContain(".gitignore"); }); @@ -311,8 +311,8 @@ describe("projectCreate", () => { expect(result.code).toBe(0); // Verify .amico exists and is valid - expect(existsSync(join(projectDir, ".amico"))).toBe(true); - const tomlContent = readFileSync(join(projectDir, ".amico"), "utf8"); + expect(existsSync(join(projectDir, "research-project.toml"))).toBe(true); + const tomlContent = readFileSync(join(projectDir, "research-project.toml"), "utf8"); expect(tomlContent).toContain("My Test Project"); expect(tomlContent).toContain("my-test-project"); @@ -392,7 +392,7 @@ describe("projectImport", () => { expect(result.code).toBe(0); // .amico was created - expect(existsSync(join(existingDir, ".amico"))).toBe(true); + expect(existsSync(join(existingDir, "research-project.toml"))).toBe(true); // Existing file was NOT overwritten expect(readFileSync(join(existingDir, "scripts/my_solve.jl"), "utf8")).toBe("# existing file"); diff --git a/packages/extension/opencode-plugin/stack_state.ts b/packages/extension/opencode-plugin/stack_state.ts index 50efe460..e0a23c36 100644 --- a/packages/extension/opencode-plugin/stack_state.ts +++ b/packages/extension/opencode-plugin/stack_state.ts @@ -610,7 +610,7 @@ function buildMemoryIndexSection(memoryIndexLines: string[]): string { // ── Active Research Project injection (#670) ───────────────────────────────── /** Read workspace folders from AMICODE_WORKSPACE_FOLDERS (colon-separated, set - * by the extension at server-spool), find the first with a .amico, and + * by the extension at server-spool), find the first with a research-project.toml, and * return a context block. Returns null if no research project is bound. */ function buildActiveProjectSection(): string | null { const raw = process.env.AMICODE_WORKSPACE_FOLDERS; @@ -618,7 +618,7 @@ function buildActiveProjectSection(): string | null { const folders = raw.split(":").filter(Boolean); for (const folder of folders) { - const tomlPath = path.join(folder, ".amico"); + const tomlPath = path.join(folder, "research-project.toml"); if (!fs.existsSync(tomlPath)) continue; // Minimal TOML parsing (no smol-toml in the Bun plugin runtime). @@ -693,7 +693,7 @@ export function buildStackStateBlock(): string | null { if (fleet) parts.push(fleet); // Active Research Project (#670): inject project metadata when a session - // is bound to a workspace folder with .amico. + // is bound to a workspace folder with research-project.toml. const project = buildActiveProjectSection(); if (project) parts.push(project); diff --git a/packages/extension/skills/autoresearch/SKILL.md b/packages/extension/skills/autoresearch/SKILL.md index 496fecc0..c41b8c93 100644 --- a/packages/extension/skills/autoresearch/SKILL.md +++ b/packages/extension/skills/autoresearch/SKILL.md @@ -16,7 +16,7 @@ project_contract: > **Install conventions** — this skill references *your personal vault* (the writable > vault your Amicode studio mounts) for the session ledger, experiment notes, and specs, > and your compute fleet where one exists. **When operating on a Research Project** -> (detected by `.amico` in the workspace), use project paths instead of vault paths +> (detected by `research-project.toml` in the workspace), use project paths instead of vault paths > — see the path resolution table below. The protocol is engine- and install-neutral; > bindings for a given engine stay engine-side (the opencode binding of the director role > is the `autoresearch` primary agent card). @@ -32,7 +32,7 @@ working set. Compaction (manual or auto) then costs nothing but a cache refill. ## Path resolution (project-aware) When the system prompt includes an `## Active Research Project` block (injected by -`stack_state.ts` when a workspace folder has `.amico`), use project paths. +`stack_state.ts` when a workspace folder has `research-project.toml`), use project paths. When no project is bound, use vault paths. The checkout registry is always vault-based (fleet-wide coordination). diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 211ff731..aa994ee5 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -557,7 +557,7 @@ export interface OpencodeConfigOptions { * Auto-loaded skills labeled (workspace). Undefined = no workspace skills. */ workspaceSkillsDir?: string; /** Absolute paths to workspace folders (VS Code multi-root). Used for - * research-project skill discovery (#668): each folder with .amico + * research-project skill discovery (#668): each folder with research-project.toml * has its skills/ scanned. Undefined = no project skills. */ workspaceFolders?: string[]; /** Stable project dir to (re)use across activations; created if missing and diff --git a/packages/extension/src/project/detect.ts b/packages/extension/src/project/detect.ts index dd5e83d8..4a072f82 100644 --- a/packages/extension/src/project/detect.ts +++ b/packages/extension/src/project/detect.ts @@ -1,5 +1,5 @@ // detect.ts — project type detection (#666). -// A Research Project is identified by a `.amico` manifest at its root. +// A Research Project is identified by a `research-project.toml` manifest at its root. // Detection is by file presence only (fast stat, no content parsing). import { existsSync } from "node:fs"; import { join } from "node:path"; @@ -8,16 +8,16 @@ export type ProjectType = "research" | "dev"; /** * Detect whether a directory is a Research Project or a Dev Project. - * A Research Project has a `.amico` manifest at its root. + * A Research Project has a `research-project.toml` manifest at its root. * Everything else is a Dev Project (the existing git-repo model). * * Re-evaluated on each call — no caching — so a directory that gains - * `.amico` after initial registration updates its type on next + * `research-project.toml` after initial registration updates its type on next * resolution. */ export function detectProjectType(dir: string): ProjectType { try { - return existsSync(join(dir, ".amico")) ? "research" : "dev"; + return existsSync(join(dir, "research-project.toml")) ? "research" : "dev"; } catch { return "dev"; } diff --git a/packages/extension/src/scores/user_skill_providers.ts b/packages/extension/src/scores/user_skill_providers.ts index fbb0965b..8578e3eb 100644 --- a/packages/extension/src/scores/user_skill_providers.ts +++ b/packages/extension/src/scores/user_skill_providers.ts @@ -87,7 +87,7 @@ export function resolveWorkspaceSkills(wsSkillsDir: string): SkillIndexEntry[] { } /** Resolve project skills from research project workspace folders (#668). - * For each folder with `.amico`, scan its `skills/` directory. + * For each folder with `research-project.toml`, scan its `skills/` directory. * Non-research directories are skipped. Order-preserving: first folder wins * on name collision (consistent with workspace-folder order). */ export function resolveProjectSkills(workspaceFolders: string[]): SkillIndexEntry[] { diff --git a/packages/extension/test/project/detect.test.ts b/packages/extension/test/project/detect.test.ts index 935a6b27..24f1fe34 100644 --- a/packages/extension/test/project/detect.test.ts +++ b/packages/extension/test/project/detect.test.ts @@ -1,5 +1,5 @@ // Project type detection tests — #666. -// detectProjectType(dir) returns "research" if .amico exists, "dev" otherwise. +// detectProjectType(dir) returns "research" if research-project.toml exists, "dev" otherwise. import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -18,12 +18,12 @@ describe("detectProjectType", () => { rmSync(tmpDir, { recursive: true, force: true }); }); - it("returns 'research' when .amico exists at the root", () => { - writeFileSync(join(tmpDir, ".amico"), 'schema_version = 1\nname = "test"\n'); + it("returns 'research' when research-project.toml exists at the root", () => { + writeFileSync(join(tmpDir, "research-project.toml"), 'schema_version = 1\nname = "test"\n'); expect(detectProjectType(tmpDir)).toBe("research"); }); - it("returns 'dev' when .amico does not exist", () => { + it("returns 'dev' when research-project.toml does not exist", () => { expect(detectProjectType(tmpDir)).toBe("dev"); }); @@ -31,9 +31,9 @@ describe("detectProjectType", () => { expect(detectProjectType(join(tmpDir, "nope"))).toBe("dev"); }); - it("detects type change: directory gains .amico after first resolution", () => { + it("detects type change: directory gains research-project.toml after first resolution", () => { expect(detectProjectType(tmpDir)).toBe("dev"); - writeFileSync(join(tmpDir, ".amico"), 'schema_version = 1\n'); + writeFileSync(join(tmpDir, "research-project.toml"), 'schema_version = 1\n'); expect(detectProjectType(tmpDir)).toBe("research"); }); }); @@ -46,7 +46,7 @@ describe("listProjectDirs with type detection", () => { beforeEach(() => { parentDir = mkdtempSync(join(tmpdir(), "amicode-list-")); mkdirSync(join(parentDir, "research-proj")); - writeFileSync(join(parentDir, "research-proj", ".amico"), 'schema_version = 1\n'); + writeFileSync(join(parentDir, "research-proj", "research-project.toml"), 'schema_version = 1\n'); mkdirSync(join(parentDir, "dev-proj")); }); @@ -54,14 +54,14 @@ describe("listProjectDirs with type detection", () => { rmSync(parentDir, { recursive: true, force: true }); }); - it("returns type 'research' for project with .amico", () => { + it("returns type 'research' for project with research-project.toml", () => { const projects = listProjectDirs(parentDir); const research = projects.find((p) => p.slug === "research-proj"); expect(research).toBeDefined(); expect(research!.type).toBe("research"); }); - it("returns type 'dev' for project without .amico", () => { + it("returns type 'dev' for project without research-project.toml", () => { const projects = listProjectDirs(parentDir); const dev = projects.find((p) => p.slug === "dev-proj"); expect(dev).toBeDefined(); diff --git a/packages/extension/test/scores/project_skills.test.ts b/packages/extension/test/scores/project_skills.test.ts index 2b370851..fd517de0 100644 --- a/packages/extension/test/scores/project_skills.test.ts +++ b/packages/extension/test/scores/project_skills.test.ts @@ -25,7 +25,7 @@ describe("resolveProjectSkills", () => { it("discovers skills from a research project's skills/ directory", () => { const projectDir = join(tmpDir, "my-research"); mkdirSync(join(projectDir, "skills", "my-analysis"), { recursive: true }); - writeFileSync(join(projectDir, ".amico"), 'schema_version = 1\n'); + writeFileSync(join(projectDir, "research-project.toml"), 'schema_version = 1\n'); writeFileSync(join(projectDir, "skills", "my-analysis", "SKILL.md"), `--- name: my-analysis description: Custom analysis skill for this project @@ -60,7 +60,7 @@ agents: [] it("returns empty for a research project with no skills/ directory", () => { const projectDir = join(tmpDir, "no-skills-proj"); mkdirSync(projectDir); - writeFileSync(join(projectDir, ".amico"), 'schema_version = 1\n'); + writeFileSync(join(projectDir, "research-project.toml"), 'schema_version = 1\n'); const skills = resolveProjectSkills([projectDir]); expect(skills).toHaveLength(0); @@ -71,8 +71,8 @@ agents: [] const proj2 = join(tmpDir, "proj2"); mkdirSync(join(proj1, "skills", "skill-a"), { recursive: true }); mkdirSync(join(proj2, "skills", "skill-b"), { recursive: true }); - writeFileSync(join(proj1, ".amico"), 'schema_version = 1\n'); - writeFileSync(join(proj2, ".amico"), 'schema_version = 1\n'); + writeFileSync(join(proj1, "research-project.toml"), 'schema_version = 1\n'); + writeFileSync(join(proj2, "research-project.toml"), 'schema_version = 1\n'); writeFileSync(join(proj1, "skills", "skill-a", "SKILL.md"), `--- name: skill-a description: Skill A diff --git a/packages/extension/test/stack_state.test.ts b/packages/extension/test/stack_state.test.ts index 6b6d77a4..d98e5323 100644 --- a/packages/extension/test/stack_state.test.ts +++ b/packages/extension/test/stack_state.test.ts @@ -255,7 +255,7 @@ describe("Active Research Project injection (#670)", () => { it("project-bound session → '## Active Research Project' block in context", () => { const projDir = mkTmp("research-proj-"); fs.writeFileSync( - path.join(projDir, ".amico"), + path.join(projDir, "research-project.toml"), 'schema_version = 1\nname = "My Research"\nquestion = "Does it work?"\nstatus = "running"\n', ); const stubs = stubAllSeams({}); @@ -285,7 +285,7 @@ describe("Active Research Project injection (#670)", () => { } }); - it("dev project (no .amico) → no Active Research Project block", () => { + it("dev project (no research-project.toml) → no Active Research Project block", () => { const devDir = mkTmp("dev-proj-"); const stubs = stubAllSeams({}); process.env.AMICODE_WORKSPACE_FOLDERS = devDir; From cd2b93d4a35da3b5772047a9b95180238ae5a031 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:08:22 -0400 Subject: [PATCH 12/22] =?UTF-8?q?feat:=20migrate-project=20skill=20?= =?UTF-8?q?=E2=80=94=20guided=20conversion=20of=20existing=20repos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship migrate-project/SKILL.md for converting existing research repos into Research Projects. Four-phase procedure: 1. Scan — read the directory tree, classify existing artifacts (scripts, data, reports, papers, figures) and map to the prescribed layout 2. Infer + interview — infer name/question/status from README and existing artifacts; ask only about what can't be inferred; surface naming ambiguities (e.g. report/ vs reports/) with explicit options 3. Scaffold — run amico project import with collected info, then handle approved renames (never rename without consent) 4. Verify — show what changed, confirm nothing overwritten, suggest next steps (outline, weekly template, workspace add) Handles edge cases: already a research project, monorepos, no git, conflicting directory names. --- .../extension/skills/migrate-project/SKILL.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/extension/skills/migrate-project/SKILL.md diff --git a/packages/extension/skills/migrate-project/SKILL.md b/packages/extension/skills/migrate-project/SKILL.md new file mode 100644 index 00000000..4ce483eb --- /dev/null +++ b/packages/extension/skills/migrate-project/SKILL.md @@ -0,0 +1,125 @@ +--- +name: migrate-project +description: Convert an existing research repo into a Research Project — scan, infer, interview, scaffold, verify. Use when a user opens a bare git repo and wants to adopt the prescribed layout without losing existing work. +agents: [] +surface: public +--- + +# Migrate Project + +Convert an existing research repository into a Research Project. The goal is +to adopt the prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, +`ledger/`, `reports/`, `config/`, `skills/`) without breaking or hiding +anything already there. + +## When to invoke + +- User says "import this as a research project," "convert this repo," or + "migrate this to the project layout" +- User opens a git repo that has research artifacts (Julia scripts, data + directories, reports) but no `research-project.toml` +- The onset router detects a repo with optimization scripts but no manifest + +## The procedure (4 phases) + +### Phase 1 — Scan + +Read the directory tree (max depth 3) and classify what exists: + +| Look for | Maps to | +|----------|---------| +| `*.jl` scripts (especially with `using Piccolo`, `QuantumCollocation`) | `scripts/` | +| `data/`, `results/`, `output/`, `runs/` | `data/` | +| `report/`, `reports/`, `docs/`, `notes/` | `reports/` or `analysis/` | +| `paper/`, `manuscript/`, `tex/`, `*.tex` files | `paper/` | +| `*.pdf` in a papers/ or references/ folder | existing literature | +| `src/`, `lib/` (Julia modules) | source code (stays as-is) | +| `README.md`, `PLAN.md`, `TODO.md` | metadata sources for inference | +| `Project.toml` / `Manifest.toml` | Julia project (leave alone) | +| `.git/` | already version-controlled (good) | +| `figures/`, `plots/`, `images/` | `data/plots/` or `paper/figures/` | + +Report what you find in a concise summary before asking questions. + +### Phase 2 — Infer + interview + +**Infer** from what's on disk — do not ask for information you can read: + +- **Name**: from `README.md` heading, or the directory basename +- **Question**: from `README.md` body, `PLAN.md`, or doc summaries +- **Status**: from what artifacts exist: + - Has only scripts → `"designing"` + - Has data/results → `"running"` or `"analyzing"` + - Has a paper draft → `"writing"` + - Has a completed paper → `"complete"` +- **Domain pack**: from imports (`using Piccolo` → `quantum-control`) + +**Ask** (one card, multiple questions) only about what you could NOT infer: + +1. Research question (if README didn't have one) — `kind: "text"` +2. Status confirmation — present your inference, let them correct it +3. Any naming ambiguities — e.g., "You have `report/` (singular). The + prescribed layout uses `reports/`. Options:" + - Rename `report/` → `reports/` (recommended if it only has reports) + - Keep both — `report/` stays, `reports/` is scaffolded empty + - Symlink `reports/` → `report/` + +**Do NOT ask about** things that have safe defaults: +- Tags (default: empty) +- Authors (default: from git config) +- Venue (default: none) +- Domain pack (inferred or omitted) + +### Phase 3 — Scaffold + +Run the CLI command with the collected information: + +```bash +amico project import \ + --name "" \ + --question "" \ + --status "" +``` + +Then handle any approved renames: + +```bash +# Example: user approved renaming report/ → reports/ +git mv report reports # if git-tracked +mv report reports # if untracked +``` + +**Never move or rename without explicit approval.** The import command itself +is non-destructive (scaffolds missing directories, never overwrites), but +renames are destructive and require consent. + +### Phase 4 — Verify + orient + +After scaffolding, show the user what changed: + +1. **New files created**: list them (the CLI's `scaffolded` output) +2. **Existing files untouched**: confirm nothing was overwritten +3. **Mapping summary**: "Your existing `scripts/single_qubit/` is exactly + where it should be. Your `report/` was renamed to `reports/` as approved." +4. **Next steps**: suggest 1–2 things they can do now: + - "Review `paper/outline.md` — I scaffolded a template; fill in your + outline and we can start formatting the paper" + - "Your weekly report template is at `reports/weekly/template.md`" + - "Add this folder to your VS Code workspace to enable project skills" + +## Edge cases + +- **Already a Research Project** (`research-project.toml` exists): say so, + offer to validate and fill in any missing scaffold directories +- **Monorepo with multiple projects**: ask which subdirectory to migrate; + don't migrate the root +- **No git**: warn that `amico project create` would `git init`, but import + does not. Suggest `git init` if they want version control +- **Conflicting names**: if a directory named `analysis/` exists but contains + scripts (not analysis), ask before mapping it + +## What this skill is NOT + +- Not a project creation flow (use `amico project create` for greenfield) +- Not a data migration tool (it never moves data between machines or formats) +- Not a paper-writer (that's a separate skill, invoked after migration) From 1382e7a44221c64c9d2b5b80259d3fbd5192fb53 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:16:25 -0400 Subject: [PATCH 13/22] =?UTF-8?q?fix:=20migrate-project=20skill=20?= =?UTF-8?q?=E2=80=94=20classify=20docs/=20by=20content=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/ is a catch-all researchers create when no structure exists. The skill now classifies each file inside it: - results summaries → ledger/observations/ - future directions → ledger/hypotheses/ - methodology guides → analysis/ - system model / hardware specs → config/ (or keep as docs/) No new prescribed docs/ folder needed — the content already has homes in the existing layout. --- packages/extension/skills/migrate-project/SKILL.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/extension/skills/migrate-project/SKILL.md b/packages/extension/skills/migrate-project/SKILL.md index 4ce483eb..6256d6ab 100644 --- a/packages/extension/skills/migrate-project/SKILL.md +++ b/packages/extension/skills/migrate-project/SKILL.md @@ -30,7 +30,12 @@ Read the directory tree (max depth 3) and classify what exists: |----------|---------| | `*.jl` scripts (especially with `using Piccolo`, `QuantumCollocation`) | `scripts/` | | `data/`, `results/`, `output/`, `runs/` | `data/` | -| `report/`, `reports/`, `docs/`, `notes/` | `reports/` or `analysis/` | +| `report/`, `reports/` | `reports/` | +| `docs/` — classify each file by content: | (see below) | +| - results summaries, experiment records | `ledger/observations/` | +| - future directions, open questions, ideas | `ledger/hypotheses/` | +| - methodology notes, "what worked" guides | `analysis/` | +| - system model, hardware specs, reference | `config/` (or keep as `docs/`) | | `paper/`, `manuscript/`, `tex/`, `*.tex` files | `paper/` | | `*.pdf` in a papers/ or references/ folder | existing literature | | `src/`, `lib/` (Julia modules) | source code (stays as-is) | From 1306477445bd11f1412838446943266ddf803355 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:24:51 -0400 Subject: [PATCH 14/22] =?UTF-8?q?feat:=20rewrite=20migrate-project=20skill?= =?UTF-8?q?=20=E2=80=94=20fully=20agent-driven=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the CLI-wrapper approach with an agent-native procedure: Phase 1 — Scan and classify: agent reads first 20-30 lines of every file and classifies using content heuristics (not filenames alone): observations → ledger/observations/, hypotheses → ledger/hypotheses/, methodology → analysis/, system reference → config/, etc. Detailed heuristic table with specific signals for each category. Phase 2 — Infer metadata: read README/PLAN for name, question, status. Only ask about what cannot be inferred. Status inference from artifact evidence (scripts only → designing, has data → running, etc.). Phase 3 — Present migration plan: full table showing every file with proposed destination, action (stays/move/rename/scaffold), and reason. Researcher reviews and iterates until approved — no partial execution. Phase 4 — Execute: mkdir scaffold dirs, git mv tracked files, mv untracked, write research-project.toml directly, one commit. Phase 5 — Verify: show what moved, what was created, flag empty dirs, suggest next steps. Handles: mixed-content dirs (per-file moves), name collisions, monorepos, no-git repos, large data dirs (sampled). Never overwrites, never moves without explicit approval. --- .../extension/skills/migrate-project/SKILL.md | 366 +++++++++++++----- 1 file changed, 265 insertions(+), 101 deletions(-) diff --git a/packages/extension/skills/migrate-project/SKILL.md b/packages/extension/skills/migrate-project/SKILL.md index 6256d6ab..72f66c00 100644 --- a/packages/extension/skills/migrate-project/SKILL.md +++ b/packages/extension/skills/migrate-project/SKILL.md @@ -7,124 +7,288 @@ surface: public # Migrate Project -Convert an existing research repository into a Research Project. The goal is -to adopt the prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, -`ledger/`, `reports/`, `config/`, `skills/`) without breaking or hiding -anything already there. +Convert an existing research repository into a Research Project. The agent +drives the entire migration — reading files, classifying content, building a +plan, and executing approved moves. No CLI middleman for the interactive path. ## When to invoke -- User says "import this as a research project," "convert this repo," or - "migrate this to the project layout" -- User opens a git repo that has research artifacts (Julia scripts, data - directories, reports) but no `research-project.toml` +- User says "import this as a research project," "convert this repo," + "migrate this to the project layout," or similar +- User opens a git repo with research artifacts but no `research-project.toml` - The onset router detects a repo with optimization scripts but no manifest -## The procedure (4 phases) - -### Phase 1 — Scan - -Read the directory tree (max depth 3) and classify what exists: - -| Look for | Maps to | -|----------|---------| -| `*.jl` scripts (especially with `using Piccolo`, `QuantumCollocation`) | `scripts/` | -| `data/`, `results/`, `output/`, `runs/` | `data/` | -| `report/`, `reports/` | `reports/` | -| `docs/` — classify each file by content: | (see below) | -| - results summaries, experiment records | `ledger/observations/` | -| - future directions, open questions, ideas | `ledger/hypotheses/` | -| - methodology notes, "what worked" guides | `analysis/` | -| - system model, hardware specs, reference | `config/` (or keep as `docs/`) | -| `paper/`, `manuscript/`, `tex/`, `*.tex` files | `paper/` | -| `*.pdf` in a papers/ or references/ folder | existing literature | -| `src/`, `lib/` (Julia modules) | source code (stays as-is) | -| `README.md`, `PLAN.md`, `TODO.md` | metadata sources for inference | -| `Project.toml` / `Manifest.toml` | Julia project (leave alone) | -| `.git/` | already version-controlled (good) | -| `figures/`, `plots/`, `images/` | `data/plots/` or `paper/figures/` | - -Report what you find in a concise summary before asking questions. - -### Phase 2 — Infer + interview - -**Infer** from what's on disk — do not ask for information you can read: - -- **Name**: from `README.md` heading, or the directory basename -- **Question**: from `README.md` body, `PLAN.md`, or doc summaries -- **Status**: from what artifacts exist: - - Has only scripts → `"designing"` - - Has data/results → `"running"` or `"analyzing"` - - Has a paper draft → `"writing"` - - Has a completed paper → `"complete"` -- **Domain pack**: from imports (`using Piccolo` → `quantum-control`) - -**Ask** (one card, multiple questions) only about what you could NOT infer: - -1. Research question (if README didn't have one) — `kind: "text"` -2. Status confirmation — present your inference, let them correct it -3. Any naming ambiguities — e.g., "You have `report/` (singular). The - prescribed layout uses `reports/`. Options:" - - Rename `report/` → `reports/` (recommended if it only has reports) - - Keep both — `report/` stays, `reports/` is scaffolded empty - - Symlink `reports/` → `report/` - -**Do NOT ask about** things that have safe defaults: -- Tags (default: empty) -- Authors (default: from git config) -- Venue (default: none) -- Domain pack (inferred or omitted) - -### Phase 3 — Scaffold - -Run the CLI command with the collected information: - -```bash -amico project import \ - --name "" \ - --question "" \ - --status "" +## Prescribed layout (target state) + +``` +/ + research-project.toml # manifest (schema_version, name, slug, question, status, created) + README.md + .gitignore + scripts/ # experiment scripts + testbed/ # scratch work + data/ + raw/ # optimization output, raw results + processed/ # post-processed data + plots/ # generated figures + analysis/ # methodology notes, post-processing notebooks + paper/ # manuscript (outline.md → main.tex) + outline.md + main.tex + references.bib + figures/ + supplementary/ + ledger/ + hypotheses/ # open questions, future directions + observations/ # experiment records, results summaries + literature/ # reading notes + campaigns/ # autoresearch campaign ledgers + reports/ + weekly/ # weekly updates (template.md provided) + presentations/ # slide decks + milestones/ # quarterly / grant reports + config/ # system model, lab config, hardware reference + skills/ # project-specific Amico skills ``` -Then handle any approved renames: +## The procedure + +### Phase 1 — Scan and classify + +Read the directory tree (depth 3 max). For every file and directory, read +the first 20–30 lines and classify it using the heuristics below. Build an +internal inventory before saying anything. + +#### Classification heuristics + +Read the file content — do not classify by filename alone. + +**Observations / results** — destination: `ledger/observations/` +- Contains fidelity numbers (`F =`, `1 - F`, `infidelity`, specific numeric results) +- Contains data tables, benchmarks, comparison tables +- Heading includes "results", "summary", "verification" +- Reports what happened in past tense ("achieved", "measured", "verified") + +**Hypotheses / future work** — destination: `ledger/hypotheses/` +- Contains "future", "deferred", "open question", "TODO", "next steps" +- Lists ideas that haven't been tested yet +- Uses speculative language ("could", "might", "worth trying", "if we") +- References work not yet done + +**Methodology / analysis** — destination: `analysis/` +- Contains "what worked", "guide", "optimization", "approach" +- Documents parameter choices and their rationale +- Compares approaches ("we tried X, but Y worked better") +- Has lessons-learned character ("the key insight was") + +**System reference / hardware** — destination: `config/` +- Contains Hamiltonian definitions (`$H$`, `\hat H`, `H =`) +- Lists hardware parameters (frequencies, anharmonicities, coupling strengths) +- Defines the physical model, units, conventions +- Reference material that doesn't change between experiments + +**Experiment scripts** — destination: `scripts/` +- `.jl` files with `using Piccolo`, `QuantumCollocation`, `Piccolissimo` +- Contains `solve!`, `UnitarySmoothPulseProblem`, trajectory definitions +- Already in `scripts/` → stays + +**Raw data / run output** — destination: `data/raw/` +- `.jld2`, `.hdf5`, `.h5` files +- Directories containing `result.toml`, `run.toml`, iteration logs +- Named with timestamps or run IDs (e.g., `yang_sqrtx_20260825_103713/`) + +**Plots and figures** — destination: `data/plots/` or `paper/figures/` +- `.png`, `.svg`, `.pdf` images +- If inside a `paper/` or `report/` context → `paper/figures/` +- If standalone or in a `data/` context → `data/plots/` + +**Reports** — destination: `reports/` +- Progress reports, weekly updates, preliminary reports +- Contains dated summaries aimed at an audience (team, PI, collaborators) +- Distinct from observations (observations record data; reports communicate it) + +**Literature** — stays or → `papers/` (no prescribed move) +- PDFs of referenced papers +- Already in `papers/` or `references/` → leave as-is + +**Source code** — stays as-is +- `src/`, `lib/` with Julia modules, utility functions +- Not part of the prescribed layout — the project may have its own package + +**Unclassifiable** — stays as-is +- When in doubt, do NOT propose a move +- Flag it in the plan as "unclassified — kept in place" + +#### Ambiguity rules + +- A file that fits two categories equally: pick the more specific one and + note the ambiguity in the Reason column +- A directory with mixed content (e.g., `docs/` with both results and + methodology): propose per-file moves, not a directory-level move +- A file you cannot classify after reading 30 lines: mark it "stays" with + reason "unclassified" + +### Phase 2 — Infer metadata + +Read these sources for manifest fields — do NOT ask for what you can read: -```bash -# Example: user approved renaming report/ → reports/ -git mv report reports # if git-tracked -mv report reports # if untracked +**Name** (required): +1. First `# heading` in `README.md` +2. Else: directory basename, title-cased + +**Research question** (required): +1. Look for a line starting with "question:", "research question:", or + a sentence ending with `?` in the first 20 lines of `README.md` +2. Look in `PLAN.md` for an objective or question statement +3. If not found: you MUST ask (this is the one field that cannot be guessed) + +**Status** (required — infer, then confirm): +- Only scripts exist, no data → `"designing"` +- Has data/results but no analysis → `"running"` +- Has analysis, methodology docs → `"analyzing"` +- Has paper draft or outline → `"writing"` +- Has completed, submitted, or published paper → `"complete"` +- Ambiguous → pick the furthest stage that has evidence, present your + reasoning, let the researcher correct + +**Domain pack** (optional — infer silently): +- `using Piccolo` or `using Piccolissimo` → `quantum-control` +- Otherwise: omit (no default) + +**Author** (optional — infer silently): +- `git config user.name` in the repo, or omit + +### Phase 3 — Present the migration plan + +Present a single markdown table covering **every** file and directory you +found, plus every scaffold entry that will be created. This is the plan the +researcher reviews. + +Format: + +```markdown +## Migration Plan for [name] + +**Inferred metadata:** +- **Name:** [inferred] +- **Question:** [inferred or "I need to ask"] +- **Status:** [inferred] — [one sentence of reasoning] + +| # | Current path | → Destination | Action | Reason | +|---|-------------|---------------|--------|--------| +| 1 | `scripts/single_qubit/` | `scripts/single_qubit/` | stays | already in the right place | +| 2 | `data/x_gate/` | `data/raw/x_gate/` | move | raw optimization output (.jld2 files) | +| 3 | `docs/results_summary.md` | `ledger/observations/results_summary.md` | move | fidelity numbers, verification results | +| 4 | `docs/future_directions.md` | `ledger/hypotheses/future_directions.md` | move | deferred ideas, speculative language | +| 5 | `docs/optimization_guide.md` | `analysis/optimization_guide.md` | move | methodology notes, "what worked" | +| 6 | `docs/system_model.md` | `config/system_model.md` | move | Hamiltonian, hardware params | +| 7 | `report/` | `reports/` | rename | singular → plural to match layout | +| 8 | `src/` | `src/` | stays | Julia modules, outside prescribed layout | +| 9 | `papers/yang_2020.pdf` | `papers/yang_2020.pdf` | stays | literature, keep as-is | +| 10 | `Project.toml` | `Project.toml` | stays | Julia package manifest | +| — | `analysis/` | — | scaffold | empty, new directory | +| — | `paper/outline.md` | — | scaffold | template with inferred question | +| — | `paper/main.tex` | — | scaffold | minimal article template | +| — | `ledger/campaigns/` | — | scaffold | empty, for autoresearch | +| — | `reports/weekly/template.md` | — | scaffold | weekly update template | +| — | `config/system.toml` | — | scaffold | stub | +| — | `research-project.toml` | — | scaffold | manifest | ``` -**Never move or rename without explicit approval.** The import command itself -is non-destructive (scaffolds missing directories, never overwrites), but -renames are destructive and require consent. +Then ask ONE question: + +> "Here's my migration plan. Review the table — tell me which moves to +> change, skip, or redirect, and I'll adjust. Or say 'looks good' to +> proceed." -### Phase 4 — Verify + orient +If the researcher requests changes, **update the plan table and re-present +it** — do not execute a partial plan. Iterate until they approve. -After scaffolding, show the user what changed: +### Phase 4 — Execute -1. **New files created**: list them (the CLI's `scaffolded` output) -2. **Existing files untouched**: confirm nothing was overwritten -3. **Mapping summary**: "Your existing `scripts/single_qubit/` is exactly - where it should be. Your `report/` was renamed to `reports/` as approved." -4. **Next steps**: suggest 1–2 things they can do now: - - "Review `paper/outline.md` — I scaffolded a template; fill in your - outline and we can start formatting the paper" +Once the researcher approves the plan, execute it in this order: + +1. **Create scaffold directories** — `mkdir -p` for every "scaffold" row +2. **Move files** — for each "move" row: + - `mkdir -p` the destination's parent directory + - If the file is git-tracked: `git mv ` + - If untracked: `mv ` +3. **Rename directories** — for each "rename" row: + - If git-tracked: `git mv ` + - If untracked: `mv ` +4. **Write scaffold files** — write `research-project.toml`, `README.md` + (only if none exists), `paper/outline.md`, `paper/main.tex`, + `reports/weekly/template.md`, config stubs, `.gitignore` files. + **Never overwrite an existing file.** +5. **Write `research-project.toml`** directly — use the schema from the + entity slice: + ```toml + schema_version = 1 + name = "" + slug = "" + question = "" + status = "" + created = "" + ``` +6. **Commit** — one commit with a descriptive message: + ``` + feat: migrate to research project layout + + Moved: + - docs/results_summary.md → ledger/observations/ + - docs/future_directions.md → ledger/hypotheses/ + - ... + Scaffolded: analysis/, paper/, ledger/, reports/, config/, skills/ + ``` + +### Phase 5 — Verify and orient + +After execution, show the researcher: + +1. **What moved** — list each move with old → new path +2. **What was created** — list scaffolded directories and files +3. **What stayed** — confirm nothing was overwritten or lost +4. **Empty directories cleaned** — if a directory (e.g., `docs/`) is now + empty after all its files were moved out, note it: "docs/ is now empty — + you can remove it or keep it" +5. **Next steps** — suggest 1–2 concrete actions: + - "Review `paper/outline.md` — fill in your outline sections and the + paper-writer skill can help format it" - "Your weekly report template is at `reports/weekly/template.md`" - - "Add this folder to your VS Code workspace to enable project skills" + - "Add this folder to your VS Code workspace to get project-aware skills" ## Edge cases -- **Already a Research Project** (`research-project.toml` exists): say so, - offer to validate and fill in any missing scaffold directories -- **Monorepo with multiple projects**: ask which subdirectory to migrate; - don't migrate the root -- **No git**: warn that `amico project create` would `git init`, but import - does not. Suggest `git init` if they want version control -- **Conflicting names**: if a directory named `analysis/` exists but contains - scripts (not analysis), ask before mapping it +**Already a Research Project** (`research-project.toml` exists): +Say so. Offer to re-scan and scaffold any missing directories, or to +re-classify content that's in the wrong place. + +**Monorepo / multi-project directory:** +If the root has multiple independent research directories, ask which one to +migrate. Do not migrate the root. + +**No git:** +Warn the researcher. Offer to `git init` before migrating (so moves get +history). If they decline, use plain `mv` for everything. + +**Mixed-content directories:** +When a directory like `docs/` has files that belong in different places, +propose per-file moves in the plan table — never move the whole directory. + +**Name collision on move:** +If a destination file already exists (e.g., the researcher already has +`analysis/optimization_guide.md`), flag it in the plan: "destination exists +— skip or overwrite?" Default: skip. + +**Large data directories:** +Don't read inside directories with 50+ files — classify by the directory +name and a sample of 3 filenames. Note: "sampled, not fully read." ## What this skill is NOT -- Not a project creation flow (use `amico project create` for greenfield) -- Not a data migration tool (it never moves data between machines or formats) -- Not a paper-writer (that's a separate skill, invoked after migration) +- Not for greenfield projects (use `amico project create` for new work) +- Not a data migration tool (no cross-machine, no format conversion) +- Not the paper-writer (invoke that after migration if the researcher wants + to start writing) +- Not a renaming enforcer — if the researcher says "keep `docs/` as is," + respect that unconditionally From c9d21712cc90ec047fc1490196ba75691a569ef0 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:29:32 -0400 Subject: [PATCH 15/22] =?UTF-8?q?rename:=20migrate-project=20=E2=86=92=20m?= =?UTF-8?q?igrate-research-project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{migrate-project => migrate-research-project}/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/extension/skills/{migrate-project => migrate-research-project}/SKILL.md (99%) diff --git a/packages/extension/skills/migrate-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md similarity index 99% rename from packages/extension/skills/migrate-project/SKILL.md rename to packages/extension/skills/migrate-research-project/SKILL.md index 72f66c00..13ee18b3 100644 --- a/packages/extension/skills/migrate-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -1,5 +1,5 @@ --- -name: migrate-project +name: migrate-research-project description: Convert an existing research repo into a Research Project — scan, infer, interview, scaffold, verify. Use when a user opens a bare git repo and wants to adopt the prescribed layout without losing existing work. agents: [] surface: public From 7da33109176d62757820414f29d20267f8fedd5f Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:39:51 -0400 Subject: [PATCH 16/22] =?UTF-8?q?fix:=20drop=20analysis/=20from=20prescrib?= =?UTF-8?q?ed=20layout=20=E2=80=94=20all=20scripts=20in=20scripts/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit analysis/ was a second home for runnable code (post-processing, plotting) that split the researcher's scripts across two directories. Consolidate: - All scripts (experiment + analysis) live in scripts/ - Methodology docs ('what worked' guides) go to ledger/observations/ alongside results — they're durable records of what was learned - analysis/ removed from SCAFFOLD_DIRS, README template, layout strings, CONTEXT.md, ADR 0012, and the migrate-research-project skill 136 tests green across both packages. --- CONTEXT.md | 2 +- docs/adr/0012-research-projects.md | 2 +- packages/amico-run/src/project.ts | 4 +--- packages/amico-run/test/project_verb.test.ts | 2 +- packages/extension/opencode-plugin/stack_state.ts | 2 +- .../skills/migrate-research-project/SKILL.md | 11 +++++------ 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 35218638..824c741b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -30,7 +30,7 @@ _Avoid_: User, tenant, owner (interchangeably) **Project**: The unit of work organization — a directory registered with amicode where files, solves, and Sessions live. Every Session belongs to exactly one Project. Two flavors: -- **Research Project** — identified by a `research-project.toml` manifest at its root. Prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. +- **Research Project** — identified by a `research-project.toml` manifest at its root. Prescribed layout (`scripts/`, `data/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). Self-contained: all data, campaign ledgers, and project-specific skills live inside the directory. Created via `amico project create`. - **Dev Project** — the existing git-repo model; any registered directory without `research-project.toml`. Canonical default: `~/armonia/`. _Avoid_: Workspace, folder (as a concept name), repo, Study (rejected alternative — researchers think in "projects") diff --git a/docs/adr/0012-research-projects.md b/docs/adr/0012-research-projects.md index 57127009..1ddb60ed 100644 --- a/docs/adr/0012-research-projects.md +++ b/docs/adr/0012-research-projects.md @@ -10,7 +10,7 @@ ## Decision -Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `research-project.toml`, with a prescribed layout (`scripts/`, `data/`, `analysis/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." +Introduce "Research Project" as a structured flavor of the existing Project concept. A Research Project is a self-contained, git-backed directory identified by `research-project.toml`, with a prescribed layout (`scripts/`, `data/`, `paper/`, `ledger/`, `reports/`, `config/`, `skills/`) and a linear lifecycle (proposing → designing → running → analyzing → writing → complete). The existing git-repo model continues as "Dev Project." ### Alternatives considered diff --git a/packages/amico-run/src/project.ts b/packages/amico-run/src/project.ts index 0ebc7a73..37176980 100644 --- a/packages/amico-run/src/project.ts +++ b/packages/amico-run/src/project.ts @@ -103,7 +103,6 @@ export const SCAFFOLD_DIRS = [ "data/raw", "data/processed", "data/plots", - "analysis", "paper/figures", "paper/supplementary", "ledger/hypotheses", @@ -307,9 +306,8 @@ ${question} ## Layout -- \`scripts/\` — experiment scripts (\`testbed/\` for scratch work) +- \`scripts/\` — experiment and analysis scripts (\`testbed/\` for scratch work) - \`data/\` — raw, processed, and plot outputs -- \`analysis/\` — analysis notebooks and post-processing - \`paper/\` — manuscript (\`outline.md\` → \`main.tex\`) - \`ledger/\` — hypotheses, observations, literature, and campaign logs - \`reports/\` — weekly updates, presentations, and milestone reports diff --git a/packages/amico-run/test/project_verb.test.ts b/packages/amico-run/test/project_verb.test.ts index 605ee7a4..90d9e37b 100644 --- a/packages/amico-run/test/project_verb.test.ts +++ b/packages/amico-run/test/project_verb.test.ts @@ -318,7 +318,7 @@ describe("projectCreate", () => { // Verify prescribed directories for (const dir of ["scripts", "scripts/testbed", "data/raw", "data/processed", - "data/plots", "analysis", "paper/figures", "paper/supplementary", + "data/plots", "paper/figures", "paper/supplementary", "ledger/hypotheses", "ledger/observations", "ledger/literature", "ledger/campaigns", "reports/weekly", "reports/presentations", "reports/milestones", "config", "skills"]) { diff --git a/packages/extension/opencode-plugin/stack_state.ts b/packages/extension/opencode-plugin/stack_state.ts index e0a23c36..65258a51 100644 --- a/packages/extension/opencode-plugin/stack_state.ts +++ b/packages/extension/opencode-plugin/stack_state.ts @@ -641,7 +641,7 @@ function buildActiveProjectSection(): string | null { `**Question:** ${question}`, `**Path:** \`${folder}\``, "", - "Layout: `scripts/`, `data/{raw,processed,plots}`, `analysis/`, `paper/`, " + + "Layout: `scripts/`, `data/{raw,processed,plots}`, `paper/`, " + "`ledger/{hypotheses,observations,campaigns}`, `reports/{weekly,presentations,milestones}`, `config/`, `skills/`", "", "When operating on this project, use project paths (not vault paths):", diff --git a/packages/extension/skills/migrate-research-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md index 13ee18b3..db77f767 100644 --- a/packages/extension/skills/migrate-research-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -25,13 +25,12 @@ plan, and executing approved moves. No CLI middleman for the interactive path. research-project.toml # manifest (schema_version, name, slug, question, status, created) README.md .gitignore - scripts/ # experiment scripts + scripts/ # experiment and analysis scripts testbed/ # scratch work data/ raw/ # optimization output, raw results processed/ # post-processed data plots/ # generated figures - analysis/ # methodology notes, post-processing notebooks paper/ # manuscript (outline.md → main.tex) outline.md main.tex @@ -40,7 +39,7 @@ plan, and executing approved moves. No CLI middleman for the interactive path. supplementary/ ledger/ hypotheses/ # open questions, future directions - observations/ # experiment records, results summaries + observations/ # experiment records, results summaries, methodology notes literature/ # reading notes campaigns/ # autoresearch campaign ledgers reports/ @@ -75,11 +74,12 @@ Read the file content — do not classify by filename alone. - Uses speculative language ("could", "might", "worth trying", "if we") - References work not yet done -**Methodology / analysis** — destination: `analysis/` +**Methodology / lessons learned** — destination: `ledger/observations/` - Contains "what worked", "guide", "optimization", "approach" - Documents parameter choices and their rationale - Compares approaches ("we tried X, but Y worked better") - Has lessons-learned character ("the key insight was") +- These are durable records of what was learned — they go with observations **System reference / hardware** — destination: `config/` - Contains Hamiltonian definitions (`$H$`, `\hat H`, `H =`) @@ -180,13 +180,12 @@ Format: | 2 | `data/x_gate/` | `data/raw/x_gate/` | move | raw optimization output (.jld2 files) | | 3 | `docs/results_summary.md` | `ledger/observations/results_summary.md` | move | fidelity numbers, verification results | | 4 | `docs/future_directions.md` | `ledger/hypotheses/future_directions.md` | move | deferred ideas, speculative language | -| 5 | `docs/optimization_guide.md` | `analysis/optimization_guide.md` | move | methodology notes, "what worked" | +| 5 | `docs/optimization_guide.md` | `ledger/observations/optimization_guide.md` | move | methodology notes, "what worked" | | 6 | `docs/system_model.md` | `config/system_model.md` | move | Hamiltonian, hardware params | | 7 | `report/` | `reports/` | rename | singular → plural to match layout | | 8 | `src/` | `src/` | stays | Julia modules, outside prescribed layout | | 9 | `papers/yang_2020.pdf` | `papers/yang_2020.pdf` | stays | literature, keep as-is | | 10 | `Project.toml` | `Project.toml` | stays | Julia package manifest | -| — | `analysis/` | — | scaffold | empty, new directory | | — | `paper/outline.md` | — | scaffold | template with inferred question | | — | `paper/main.tex` | — | scaffold | minimal article template | | — | `ledger/campaigns/` | — | scaffold | empty, for autoresearch | From abe980913af9e34035c6a3dbccdca27ac954f897 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:42:46 -0400 Subject: [PATCH 17/22] fix: remove remaining analysis/ references from migrate-research-project skill --- packages/extension/skills/migrate-research-project/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension/skills/migrate-research-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md index db77f767..676d9dc8 100644 --- a/packages/extension/skills/migrate-research-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -237,7 +237,7 @@ Once the researcher approves the plan, execute it in this order: - docs/results_summary.md → ledger/observations/ - docs/future_directions.md → ledger/hypotheses/ - ... - Scaffolded: analysis/, paper/, ledger/, reports/, config/, skills/ + Scaffolded: paper/, ledger/, reports/, config/, skills/ ``` ### Phase 5 — Verify and orient @@ -276,7 +276,7 @@ propose per-file moves in the plan table — never move the whole directory. **Name collision on move:** If a destination file already exists (e.g., the researcher already has -`analysis/optimization_guide.md`), flag it in the plan: "destination exists +`ledger/observations/optimization_guide.md`), flag it in the plan: "destination exists — skip or overwrite?" Default: skip. **Large data directories:** From 604f5c7b87fcfe6d048e641ea82fc2e9e63d08ad Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:43:58 -0400 Subject: [PATCH 18/22] feat: add scripts/analysis/ sub-folder for post-processing scripts All code stays in scripts/, but analysis scripts (plotting, comparison, post-processing) get their own sub-folder alongside testbed/. The migrate skill classifies analysis scripts separately: files that load results (JLD2, CairoMakie, DataFrame) go to scripts/analysis/, experiment scripts (solve!, UnitarySmoothPulseProblem) stay in scripts/. --- packages/amico-run/src/project.ts | 3 ++- packages/amico-run/test/project_verb.test.ts | 2 +- .../extension/skills/migrate-research-project/SKILL.md | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/amico-run/src/project.ts b/packages/amico-run/src/project.ts index 37176980..22ed6e18 100644 --- a/packages/amico-run/src/project.ts +++ b/packages/amico-run/src/project.ts @@ -100,6 +100,7 @@ export function nameToSlug(name: string): string { export const SCAFFOLD_DIRS = [ "scripts", "scripts/testbed", + "scripts/analysis", "data/raw", "data/processed", "data/plots", @@ -306,7 +307,7 @@ ${question} ## Layout -- \`scripts/\` — experiment and analysis scripts (\`testbed/\` for scratch work) +- \`scripts/\` — experiment and analysis scripts (\`testbed/\` for scratch, \`analysis/\` for post-processing) - \`data/\` — raw, processed, and plot outputs - \`paper/\` — manuscript (\`outline.md\` → \`main.tex\`) - \`ledger/\` — hypotheses, observations, literature, and campaign logs diff --git a/packages/amico-run/test/project_verb.test.ts b/packages/amico-run/test/project_verb.test.ts index 90d9e37b..8d0c3dae 100644 --- a/packages/amico-run/test/project_verb.test.ts +++ b/packages/amico-run/test/project_verb.test.ts @@ -317,7 +317,7 @@ describe("projectCreate", () => { expect(tomlContent).toContain("my-test-project"); // Verify prescribed directories - for (const dir of ["scripts", "scripts/testbed", "data/raw", "data/processed", + for (const dir of ["scripts", "scripts/testbed", "scripts/analysis", "data/raw", "data/processed", "data/plots", "paper/figures", "paper/supplementary", "ledger/hypotheses", "ledger/observations", "ledger/literature", "ledger/campaigns", "reports/weekly", "reports/presentations", diff --git a/packages/extension/skills/migrate-research-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md index 676d9dc8..7411fd40 100644 --- a/packages/extension/skills/migrate-research-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -27,6 +27,7 @@ plan, and executing approved moves. No CLI middleman for the interactive path. .gitignore scripts/ # experiment and analysis scripts testbed/ # scratch work + analysis/ # post-processing, plotting, comparison scripts data/ raw/ # optimization output, raw results processed/ # post-processed data @@ -92,6 +93,12 @@ Read the file content — do not classify by filename alone. - Contains `solve!`, `UnitarySmoothPulseProblem`, trajectory definitions - Already in `scripts/` → stays +**Analysis scripts** — destination: `scripts/analysis/` +- `.jl` or `.py` files that load results and produce plots or tables +- Contains `load`, `JLD2`, `CairoMakie`, `plot`, `DataFrame` +- Post-processing, comparison, Pareto-frontier scripts +- Distinct from experiment scripts: these read data, not produce it + **Raw data / run output** — destination: `data/raw/` - `.jld2`, `.hdf5`, `.h5` files - Directories containing `result.toml`, `run.toml`, iteration logs From 3bb010b5ca3098dd8535b15f7eb4eb2b427d0acb Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:46:13 -0400 Subject: [PATCH 19/22] feat: add scripts/experiment/ for optimization solves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/ now has three sub-folders: experiment/ — optimization solves, gate synthesis (bulk of scripts) analysis/ — post-processing, plotting, comparison testbed/ — scratch work, quick experiments The migrate skill classifies into experiment/ (solve!, trajectory defs) vs analysis/ (JLD2.load, CairoMakie, plot) vs testbed/ (scratch). --- packages/amico-run/src/project.ts | 5 +++-- packages/amico-run/test/project_verb.test.ts | 2 +- .../skills/migrate-research-project/SKILL.md | 11 ++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/amico-run/src/project.ts b/packages/amico-run/src/project.ts index 22ed6e18..9020a4f5 100644 --- a/packages/amico-run/src/project.ts +++ b/packages/amico-run/src/project.ts @@ -99,8 +99,9 @@ export function nameToSlug(name: string): string { /** The prescribed directory layout for a Research Project (PRD #663). */ export const SCAFFOLD_DIRS = [ "scripts", - "scripts/testbed", + "scripts/experiment", "scripts/analysis", + "scripts/testbed", "data/raw", "data/processed", "data/plots", @@ -307,7 +308,7 @@ ${question} ## Layout -- \`scripts/\` — experiment and analysis scripts (\`testbed/\` for scratch, \`analysis/\` for post-processing) +- \`scripts/\` — \`experiment/\` for solves, \`analysis/\` for post-processing, \`testbed/\` for scratch - \`data/\` — raw, processed, and plot outputs - \`paper/\` — manuscript (\`outline.md\` → \`main.tex\`) - \`ledger/\` — hypotheses, observations, literature, and campaign logs diff --git a/packages/amico-run/test/project_verb.test.ts b/packages/amico-run/test/project_verb.test.ts index 8d0c3dae..4cd5d622 100644 --- a/packages/amico-run/test/project_verb.test.ts +++ b/packages/amico-run/test/project_verb.test.ts @@ -317,7 +317,7 @@ describe("projectCreate", () => { expect(tomlContent).toContain("my-test-project"); // Verify prescribed directories - for (const dir of ["scripts", "scripts/testbed", "scripts/analysis", "data/raw", "data/processed", + for (const dir of ["scripts", "scripts/experiment", "scripts/analysis", "scripts/testbed", "data/raw", "data/processed", "data/plots", "paper/figures", "paper/supplementary", "ledger/hypotheses", "ledger/observations", "ledger/literature", "ledger/campaigns", "reports/weekly", "reports/presentations", diff --git a/packages/extension/skills/migrate-research-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md index 7411fd40..dca3b380 100644 --- a/packages/extension/skills/migrate-research-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -25,9 +25,10 @@ plan, and executing approved moves. No CLI middleman for the interactive path. research-project.toml # manifest (schema_version, name, slug, question, status, created) README.md .gitignore - scripts/ # experiment and analysis scripts - testbed/ # scratch work - analysis/ # post-processing, plotting, comparison scripts + scripts/ # all runnable code + experiment/ # optimization solves, gate synthesis + analysis/ # post-processing, plotting, comparison + testbed/ # scratch work, quick experiments data/ raw/ # optimization output, raw results processed/ # post-processed data @@ -88,10 +89,10 @@ Read the file content — do not classify by filename alone. - Defines the physical model, units, conventions - Reference material that doesn't change between experiments -**Experiment scripts** — destination: `scripts/` +**Experiment scripts** — destination: `scripts/experiment/` - `.jl` files with `using Piccolo`, `QuantumCollocation`, `Piccolissimo` - Contains `solve!`, `UnitarySmoothPulseProblem`, trajectory definitions -- Already in `scripts/` → stays +- The bulk of a researcher's scripts — optimization solves, gate synthesis **Analysis scripts** — destination: `scripts/analysis/` - `.jl` or `.py` files that load results and produce plots or tables From e67f0cb7c6ae1b93136fb4ed11ee0bf982abfdaa Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:48:50 -0400 Subject: [PATCH 20/22] =?UTF-8?q?fix:=20correct=20testbed/=20description?= =?UTF-8?q?=20=E2=80=94=20simulated=20gym=20environments=20and=20hardware?= =?UTF-8?q?=20interfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit testbed/ is not scratch work. It's where the simulated environment (the gym) and hardware-interfacing code live — the foundation that experiment scripts run against. --- packages/amico-run/src/project.ts | 6 ++++-- packages/extension/skills/migrate-research-project/SKILL.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/amico-run/src/project.ts b/packages/amico-run/src/project.ts index 9020a4f5..c03a08ef 100644 --- a/packages/amico-run/src/project.ts +++ b/packages/amico-run/src/project.ts @@ -308,7 +308,7 @@ ${question} ## Layout -- \`scripts/\` — \`experiment/\` for solves, \`analysis/\` for post-processing, \`testbed/\` for scratch +- \`scripts/\` — \`experiment/\` for solves, \`analysis/\` for post-processing, \`testbed/\` for simulated environments and hardware interfaces - \`data/\` — raw, processed, and plot outputs - \`paper/\` — manuscript (\`outline.md\` → \`main.tex\`) - \`ledger/\` — hypotheses, observations, literature, and campaign logs @@ -373,7 +373,9 @@ export const PAPER_LATEXMKRC = `$pdflatex = 'pdflatex -interaction=nonstopmode % export const SCRIPTS_README = `# Scripts -Experiment scripts for this project. Use \`testbed/\` for scratch work and quick experiments. +- \`experiment/\` — optimization solves, gate synthesis +- \`analysis/\` — post-processing, plotting, comparison +- \`testbed/\` — simulated gym environment, hardware-interfacing code `; /** Build the full scaffold manifest: relative paths and their contents. Files diff --git a/packages/extension/skills/migrate-research-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md index dca3b380..a9666f75 100644 --- a/packages/extension/skills/migrate-research-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -28,7 +28,7 @@ plan, and executing approved moves. No CLI middleman for the interactive path. scripts/ # all runnable code experiment/ # optimization solves, gate synthesis analysis/ # post-processing, plotting, comparison - testbed/ # scratch work, quick experiments + testbed/ # simulated gym environment, hardware-interfacing code data/ raw/ # optimization output, raw results processed/ # post-processed data From e32e6e55693a0a58f1d81bf14c32ee1440ff59d0 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 15:52:47 -0400 Subject: [PATCH 21/22] fix: align all skills with scripts/{experiment,analysis,testbed} layout Every skill that references scripts/ now names the sub-folders and their roles: - autoresearch/SKILL.md: path table expanded from one Scripts row to three (experiment, analysis, testbed) with descriptions - paper-writer/SKILL.md: methodology grounding references scripts/experiment/ and scripts/testbed/ specifically - stack_state.ts: layout string and path list expanded to show all three sub-folders with testbed role description - stack_state test: assertions updated to match 136 tests green. --- packages/extension/opencode-plugin/stack_state.ts | 6 ++++-- packages/extension/skills/autoresearch/SKILL.md | 4 +++- packages/extension/skills/paper-writer/SKILL.md | 3 ++- packages/extension/test/stack_state.test.ts | 4 +++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/extension/opencode-plugin/stack_state.ts b/packages/extension/opencode-plugin/stack_state.ts index 65258a51..516ebbe9 100644 --- a/packages/extension/opencode-plugin/stack_state.ts +++ b/packages/extension/opencode-plugin/stack_state.ts @@ -641,14 +641,16 @@ function buildActiveProjectSection(): string | null { `**Question:** ${question}`, `**Path:** \`${folder}\``, "", - "Layout: `scripts/`, `data/{raw,processed,plots}`, `paper/`, " + + "Layout: `scripts/{experiment,analysis,testbed}`, `data/{raw,processed,plots}`, `paper/`, " + "`ledger/{hypotheses,observations,campaigns}`, `reports/{weekly,presentations,milestones}`, `config/`, `skills/`", "", "When operating on this project, use project paths (not vault paths):", "- Hypotheses: `/ledger/hypotheses/`", "- Campaign ledgers: `/ledger/campaigns/`", "- Observations: `/ledger/observations/`", - "- Scripts: `/scripts/`", + "- Experiment scripts: `/scripts/experiment/`", + "- Analysis scripts: `/scripts/analysis/`", + "- Testbed: `/scripts/testbed/` (simulated environments, hardware interfaces)", "- Data: `/data/`", "- Config: `/config/`", "- Paper: `/paper/`", diff --git a/packages/extension/skills/autoresearch/SKILL.md b/packages/extension/skills/autoresearch/SKILL.md index c41b8c93..802dfda7 100644 --- a/packages/extension/skills/autoresearch/SKILL.md +++ b/packages/extension/skills/autoresearch/SKILL.md @@ -41,7 +41,9 @@ When no project is bound, use vault paths. The checkout registry is always vault | Campaign ledger | `/ledger/campaigns/campaign--.md` | `/sessions/session--.md` | | Hypotheses | `/ledger/hypotheses/` | `/hypotheses/` | | Observations | `/ledger/observations/` | `/experiments/` | -| Scripts | `/scripts/` | (none — vault has no scripts) | +| Experiment scripts | `/scripts/experiment/` — optimization solves, gate synthesis | (none) | +| Analysis scripts | `/scripts/analysis/` — post-processing, plotting, comparison | (none) | +| Testbed | `/scripts/testbed/` — simulated gym environments, hardware interfaces | (none) | | Data | `/data/` | (none) | | Config | `/config/` | (none) | | Reports | `/reports/` | (none) | diff --git a/packages/extension/skills/paper-writer/SKILL.md b/packages/extension/skills/paper-writer/SKILL.md index b345ab6c..b67fe512 100644 --- a/packages/extension/skills/paper-writer/SKILL.md +++ b/packages/extension/skills/paper-writer/SKILL.md @@ -89,7 +89,8 @@ The agent MUST NOT: 4. **Select emphasis** — do not add "importantly," "notably," "remarkably" or similar editorial emphasis not present in the outline 5. **Invent methodology details** — only include methods described in the - outline or referenced in `scripts/` + outline or referenced in `scripts/experiment/` (optimization solves) and + `scripts/testbed/` (system environment) 6. **Hallucinate citations** — only cite references that exist in `paper/references.bib` diff --git a/packages/extension/test/stack_state.test.ts b/packages/extension/test/stack_state.test.ts index d98e5323..91a30d94 100644 --- a/packages/extension/test/stack_state.test.ts +++ b/packages/extension/test/stack_state.test.ts @@ -267,8 +267,10 @@ describe("Active Research Project injection (#670)", () => { expect(block).toContain("**Question:** Does it work?"); expect(block).toContain(`**Path:** \`${projDir}\``); expect(block).toContain("Hypotheses: `/ledger/hypotheses/`"); + expect(block).toContain("Experiment scripts: `/scripts/experiment/`"); + expect(block).toContain("Testbed: `/scripts/testbed/`"); expect(block).toContain("Reports: `/reports/`"); - expect(block).toContain("`reports/{weekly,presentations,milestones}`"); + expect(block).toContain("`scripts/{experiment,analysis,testbed}`"); } finally { restoreSeams(stubs); } From 026dd7d8a5ba4f4f0d1fbd97a0c118c072b49608 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 31 Aug 2026 16:38:35 -0400 Subject: [PATCH 22/22] =?UTF-8?q?refactor:=20deprecate=20vault=20paths=20f?= =?UTF-8?q?rom=20autoresearch=20skills=20=E2=80=94=20project-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route all autoresearch state (campaign ledgers, hypotheses, observations, specs, checkout registry) through the Research Project directory. Remove vault_contract frontmatter and vault fallback paths from autoresearch, hypothesis-review, analyze, and deliberate skills. --- packages/extension/skills/analyze/SKILL.md | 12 ++- .../extension/skills/autoresearch/SKILL.md | 76 +++++++++---------- packages/extension/skills/deliberate/SKILL.md | 8 +- .../skills/hypothesis-review/SKILL.md | 13 ++-- .../skills/migrate-research-project/SKILL.md | 6 +- 5 files changed, 53 insertions(+), 62 deletions(-) diff --git a/packages/extension/skills/analyze/SKILL.md b/packages/extension/skills/analyze/SKILL.md index 54d75a26..77ebff27 100644 --- a/packages/extension/skills/analyze/SKILL.md +++ b/packages/extension/skills/analyze/SKILL.md @@ -3,9 +3,8 @@ name: analyze description: Post-experiment analysis and insight extraction — stagnation detection, failure classification, hyperparameter comparison. Use after optimization runs to extract patterns and generate insights. agents: [researcher, librarian, dreamer] surface: public -vault_contract: - folders: [experiments, insights] - catalog: [pulses] +project_contract: + folders: [ledger/observations, ledger/campaigns, scripts/analysis, data] --- Post-experiment analysis for Amico optimization results. @@ -23,8 +22,8 @@ The argument is: $ARGUMENTS After a batch of optimization experiments (3+), or when STRATEGY.md priorities need updating. ### Step 1: Gather Data -- Read recent experiment notes in `experiments/` across **every mounted vault** (read precedence per the `amico-vault` skill) -- Read catalog entries in `catalog/pulses/*/metadata.toml` from whichever mount holds the catalog +- Read recent experiment notes in `/ledger/observations/` +- Read catalog entries in `catalog/pulses/*/metadata.toml` if the project has a catalog - Group by (platform, gate) pairs ### Step 2: Stagnation Detection @@ -56,9 +55,8 @@ Trace chains via `warm_started_from` in catalog: - Flag chains where cold restart might beat continuing ### Step 6: Generate Outputs -- **Insight notes**: Create in `/insights/` (route per amico-vault) when patterns are clear (3+ supporting experiments) +- **Insight notes**: Create in `/ledger/observations/` when patterns are clear (3+ supporting experiments) - **Strategy suggestions**: Note which STRATEGY.md priorities are progressing vs stuck -- Use `/amico-vault` skill for correct frontmatter > **Fidelity convention:** Always report both fixed-phase and free-phase fidelity for multi-subsystem gates. Free-phase is the primary metric. Fixed-phase routinely underreports by 6–80 pp for entangling gates. Ref: [[insight-20260412-054400-synthesis-free-phase-gap-scales-with-gate-type]]. diff --git a/packages/extension/skills/autoresearch/SKILL.md b/packages/extension/skills/autoresearch/SKILL.md index 802dfda7..2ee756c1 100644 --- a/packages/extension/skills/autoresearch/SKILL.md +++ b/packages/extension/skills/autoresearch/SKILL.md @@ -3,55 +3,51 @@ name: autoresearch description: The director's loop protocol for autonomous research sessions — session-ledger discipline, the hypothesizer/experimenter/analyzer trio, deliberate spec gates, checkout registry, and compaction-any-time safety. Use when starting, running, or resuming an autoresearch loop. agents: [hypothesizer, experimenter, analyzer] surface: public -vault_contract: - folders: [sessions, experiments, specs] - note_types: [session, experiment, spec, hypothesis] - frontmatter: [session_id, status, tags] project_contract: folders: [ledger/hypotheses, ledger/observations, ledger/campaigns, scripts, data, reports, config] --- # Autoresearch — the director's protocol -> **Install conventions** — this skill references *your personal vault* (the writable -> vault your Amicode studio mounts) for the session ledger, experiment notes, and specs, -> and your compute fleet where one exists. **When operating on a Research Project** -> (detected by `research-project.toml` in the workspace), use project paths instead of vault paths -> — see the path resolution table below. The protocol is engine- and install-neutral; -> bindings for a given engine stay engine-side (the opencode binding of the director role -> is the `autoresearch` primary agent card). +> **Install conventions** — this skill operates on a **Research Project** (detected by +> `research-project.toml` in the workspace). All load-bearing state — campaign ledgers, +> hypotheses, observations, specs, scripts, data, and reports — lives in the project +> directory. The protocol is engine- and install-neutral; bindings for a given engine stay +> engine-side (the opencode binding of the director role is the `autoresearch` primary +> agent card). **Entry points:** the `autoresearch` primary agent (Tab-switch into research mode — its prompt embeds this spine), direct invocation of this skill, or the standing line in the user's autoresearch kickoff prompts. All three lead here; this file is the protocol. -The operating principle: **the context window is a cache; the vault is the database.** -Every piece of load-bearing state lives in a vault note; the context window holds only the -working set. Compaction (manual or auto) then costs nothing but a cache refill. +The operating principle: **the context window is a cache; the project is the database.** +Every piece of load-bearing state lives in project files — campaign ledgers, hypotheses, +observations, scripts. The context window holds only the working set. Compaction (manual +or auto) then costs nothing but a cache refill. -## Path resolution (project-aware) +## Project paths -When the system prompt includes an `## Active Research Project` block (injected by -`stack_state.ts` when a workspace folder has `research-project.toml`), use project paths. -When no project is bound, use vault paths. The checkout registry is always vault-based -(fleet-wide coordination). +All artifacts live inside the Research Project directory. The `## Active Research Project` +block in the system prompt (injected by `stack_state.ts` when a workspace folder has +`research-project.toml`) confirms the project root. -| Artifact | Project-bound | Vault (no project) | -|----------|---------------|--------------------| -| Campaign ledger | `/ledger/campaigns/campaign--.md` | `/sessions/session--.md` | -| Hypotheses | `/ledger/hypotheses/` | `/hypotheses/` | -| Observations | `/ledger/observations/` | `/experiments/` | -| Experiment scripts | `/scripts/experiment/` — optimization solves, gate synthesis | (none) | -| Analysis scripts | `/scripts/analysis/` — post-processing, plotting, comparison | (none) | -| Testbed | `/scripts/testbed/` — simulated gym environments, hardware interfaces | (none) | -| Data | `/data/` | (none) | -| Config | `/config/` | (none) | -| Reports | `/reports/` | (none) | -| Checkout registry | `/sessions/CHECKOUTS.md` | `/sessions/CHECKOUTS.md` | +| Artifact | Path | +|----------|------| +| Campaign ledger | `/ledger/campaigns/campaign--.md` | +| Hypotheses | `/ledger/hypotheses/` | +| Observations | `/ledger/observations/` | +| Spec cards | `/config/specs/` | +| Experiment scripts | `/scripts/experiment/` — optimization solves, gate synthesis | +| Analysis scripts | `/scripts/analysis/` — post-processing, plotting, comparison | +| Testbed | `/scripts/testbed/` — simulated gym environments, hardware interfaces | +| Data | `/data/` | +| Config | `/config/` | +| Reports | `/reports/` | +| Checkout registry | `/ledger/campaigns/CHECKOUTS.md` | -## The session ledger (create at kickoff, before any work) +## The campaign ledger (create at kickoff, before any work) -Path: `/sessions/session--.md`. Nine sections, in order: +Path: `/ledger/campaigns/campaign--.md`. Nine sections, in order: 1. Objective & standing directives 2. Hypothesis ledger (H# → verdict → evidence → wiki-links) @@ -59,7 +55,7 @@ Path: `/sessions/session--.md`. Nine sections, i in-flight subagent cast (role, session id, spec id, assigned env, expected artifacts) 4. Blocked & reasons 5. Next queue -6. Checkout topology (mirror of this session's rows in `sessions/CHECKOUTS.md`) +6. Checkout topology (mirror of this session's rows in `ledger/campaigns/CHECKOUTS.md`) 7. Gotchas & methodology 8. Loop log (append-only, one row per loop: date, H#, spec_id, review verdict, plan hash, experimenter session id, gate verdicts, advisory closures) @@ -79,12 +75,12 @@ reference the in-flight casts? Append the audit row to §9. 1. **Re-read the ledger** — from disk, never from memory. 2. **Hypothesis queue thin?** Cast the **hypothesizer** (read-only subagent): ranked hypotheses + a spec-card draft. Parent picks the winner. -3. **Spec gate (deliberate):** file the spec card to `/specs/`, run +3. **Spec gate (deliberate):** file the spec card to `/config/specs/`, run `amico spec review ` — resolve blocking findings, re-run (round budget 3). `--allow-unreviewed` is FORBIDDEN for launch-shaped work (spends compute); if compile refuses for want of review, the fix is to review, never the flag. 4. **Ledger the cast, THEN cast the experimenter** (one experiment per spec, assigned env - per `sessions/CHECKOUTS.md`; claim the checkout row first). Parallel experimenters only + per `ledger/campaigns/CHECKOUTS.md`; claim the checkout row first). Parallel experimenters only where the registry says files are disjoint. 5. **Run the gates yourself (parent, via bash):** test suites, `amico plan status` where a compiled plan exists. Verdicts are DERIVED, never self-reported. No LLM — including @@ -135,10 +131,10 @@ experiment, only the parent's own working notes); re-read + audit after every co ## Parallel sessions & shared checkouts -`sessions/CHECKOUTS.md` is the fleet-wide claim registry. Re-read it before casting any -experimenter; claim your row; release it when work lands. First-writer-wins, propagated by -the ~15-min sync; races inside the window are possible and visible — a visible conflict -beats a silent double-ownership every time. +`ledger/campaigns/CHECKOUTS.md` is the project-wide claim registry. Re-read it before +casting any experimenter; claim your row; release it when work lands. First-writer-wins; +races are possible and visible — a visible conflict beats a silent double-ownership every +time. ## Standing anti-gaming contract diff --git a/packages/extension/skills/deliberate/SKILL.md b/packages/extension/skills/deliberate/SKILL.md index 5e3fdac9..4c4715c4 100644 --- a/packages/extension/skills/deliberate/SKILL.md +++ b/packages/extension/skills/deliberate/SKILL.md @@ -4,8 +4,8 @@ description: "Use before any substantial work — a spec, adversarial review by agents: [researcher, experimenter, engineer] surface: public scenarios: [spec-underspecified-must-block, spec-adequate-must-pass] -vault_contract: - folders: [specs, plans] +project_contract: + folders: [config/specs] note_types: [spec, plan, spec-review] tags: [deliberation] --- @@ -48,7 +48,7 @@ by hand, which is a weaker claim and should read as one. One dialogue rule: **ask one question at a time.** A wall of questions gets a wall of shallow answers. -Write to `/specs/spec--.md`: +Write to `/config/specs/spec--.md`: ```yaml --- @@ -249,6 +249,6 @@ on **both** sides actually are — and whether the spec ever says. ## Related -- `amico-vault` — where specs and plans live, and the frontmatter schema +- `config/specs/` — where specs and plans live in a Research Project - `break-into-subissues`, `implement-issue` — the issue-tracker path, once a plan exists - `verification` — what a gate is and how to choose one diff --git a/packages/extension/skills/hypothesis-review/SKILL.md b/packages/extension/skills/hypothesis-review/SKILL.md index 88cf4d1a..389b0a9d 100644 --- a/packages/extension/skills/hypothesis-review/SKILL.md +++ b/packages/extension/skills/hypothesis-review/SKILL.md @@ -3,8 +3,8 @@ name: hypothesis-review description: Rank open hypotheses by testability and impact. Invoked from researcher Step 0 to prioritize hypothesis-driven experiments. agents: [researcher] surface: public -vault_contract: - folders: [hypotheses] +project_contract: + folders: [ledger/hypotheses] frontmatter: [status] --- @@ -14,11 +14,8 @@ Rank open hypotheses to identify the highest-value experiments. ## Process -1. **Locate hypotheses.** When the system prompt includes an `## Active Research - Project` block, read from `/ledger/hypotheses/`. Otherwise, glob - `hypotheses/` across **every mounted vault** in the Armonia stack (see the - `amico-vault` skill for the mount set and read precedence). Filter for notes - with `status: open` or `status: untested`. +1. **Locate hypotheses.** Read from `/ledger/hypotheses/`. Filter for + notes with `status: open` or `status: untested`. 2. For each hypothesis, read the body and assess: - **Impact if confirmed** (numeric): high=3 (unlocks fidelity breakthrough or new platform), medium=2 (improves existing result), low=1 (incremental or niche) - **Ease of test** (numeric): easy=3 (1 cold-start experiment), moderate=2 (needs specific setup or 2-3 experiments), hard=1 (requires engineering or new infrastructure) @@ -30,7 +27,7 @@ Rank open hypotheses to identify the highest-value experiments. ```yaml hypotheses: - - path: "vault/hypotheses/hypothesis-YYYYMMDD-HHMMSS-topic.md" + - path: "ledger/hypotheses/hypothesis-YYYYMMDD-HHMMSS-topic.md" title: "Free-phase may help fluxonium Y gate" impact: 3 # high — could unlock new-best on stuck gate ease: 3 # easy — 1 cold-start with free_phase=true diff --git a/packages/extension/skills/migrate-research-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md index a9666f75..65c65a1f 100644 --- a/packages/extension/skills/migrate-research-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -115,9 +115,9 @@ Read the file content — do not classify by filename alone. - Contains dated summaries aimed at an audience (team, PI, collaborators) - Distinct from observations (observations record data; reports communicate it) -**Literature** — stays or → `papers/` (no prescribed move) -- PDFs of referenced papers -- Already in `papers/` or `references/` → leave as-is +**Literature** — destination: `ledger/literature/` +- PDFs of referenced papers, reading notes +- Already in `papers/` or `references/` → move to `ledger/literature/` **Source code** — stays as-is - `src/`, `lib/` with Julia modules, utility functions