diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-question-dock.tsx index 6eda5f0b..4eeb5b78 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -73,11 +73,15 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const total = createMemo(() => questions().length) const cached = cache.get(cacheKey) + // Seed text inputs from each question's `default` field when no cache exists. + const initCustom = cached?.custom ?? questions().map((q) => q.default ?? "") + const initAnswers = cached?.answers ?? questions().map((q) => (q.default ? [q.default] : [])) + const initCustomOn = cached?.customOn ?? questions().map((q) => !!q.default) const [store, setStore] = createStore({ tab: cached?.tab ?? 0, - answers: cached?.answers ?? ([] as QuestionAnswer[]), - custom: cached?.custom ?? ([] as string[]), - customOn: cached?.customOn ?? ([] as boolean[]), + answers: initAnswers as QuestionAnswer[], + custom: initCustom as string[], + customOn: initCustomOn as boolean[], editing: false, focus: 0, minimized: false, diff --git a/packages/app-bundle/overlay/packages/schema/src/question.ts b/packages/app-bundle/overlay/packages/schema/src/question.ts index 0f44af76..a6867918 100644 --- a/packages/app-bundle/overlay/packages/schema/src/question.ts +++ b/packages/app-bundle/overlay/packages/schema/src/question.ts @@ -36,6 +36,9 @@ const base = { options: Schema.Array(Option).annotate({ description: "Available choices" }), multiple: Schema.Boolean.pipe(optional).annotate({ description: "Allow selecting multiple choices" }), kind: Kind.pipe(optional), + default: Schema.String.pipe(optional).annotate({ + description: "Pre-filled value for text-kind questions (user can edit before submitting)", + }), } export const Info = Schema.Struct({ diff --git a/packages/app-bundle/overlay/packages/schema/src/v1/question.ts b/packages/app-bundle/overlay/packages/schema/src/v1/question.ts index b90503a5..08b9401c 100644 --- a/packages/app-bundle/overlay/packages/schema/src/v1/question.ts +++ b/packages/app-bundle/overlay/packages/schema/src/v1/question.ts @@ -27,6 +27,9 @@ const base = { options: Schema.Array(Option).annotate({ description: "Available choices" }), multiple: Schema.optional(Schema.Boolean).annotate({ description: "Allow selecting multiple choices" }), kind: Schema.optional(Kind), + default: Schema.optional(Schema.String).annotate({ + description: "Pre-filled value for text-kind questions (user can edit before submitting)", + }), } export const Info = Schema.Struct({ diff --git a/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts index 37ff1cb9..336e6f6d 100644 --- a/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts @@ -738,6 +738,10 @@ export type QuestionInfo = { * Question shape: "choice" (default, an option list) or "text" (a free-form text card, no options) */ kind?: "choice" | "text" + /** + * Pre-filled value for text-kind questions (user can edit before submitting) + */ + default?: string custom?: boolean } @@ -3197,6 +3201,10 @@ export type QuestionV2Info = { * Question shape: "choice" (default, an option list) or "text" (a free-form text card, no options) */ kind?: "choice" | "text" + /** + * Pre-filled value for text-kind questions (user can edit before submitting) + */ + default?: string custom?: boolean } diff --git a/packages/extension/opencode-plugin/amicode_tools.ts b/packages/extension/opencode-plugin/amicode_tools.ts index 6a047992..421f8f35 100644 --- a/packages/extension/opencode-plugin/amicode_tools.ts +++ b/packages/extension/opencode-plugin/amicode_tools.ts @@ -1097,12 +1097,14 @@ export const AmicodeTools = async (_input: unknown) => ({ amicode_profile: { description: "Record onboarding entities during the overture interview (session zero), and read them " + - "back to resume. Entities: `profile` {name, role, org, platforms[], goals}; " + + "back to resume. Entities: `profile` {name, role, org, platforms[], goals, intent, " + + "description, research_area, experiment_kind, scholar, github, custom_link_url, custom_link_label}; " + "`environment` {slug, archetype: qick-lab|cloud-pasqal|local-sim|other, control_stack, " + "integration, emulator, endpoints[] — POINTERS ONLY, never credentials}; " + "`device` {name, platform, environment, qubits, params, status}; and " + "`onboarding_completed` {} — record it EXACTLY ONCE, at the handoff stage (it is what " + - "lets the background distiller materialize the user's profile). " + + "lets the background distiller materialize the user's profile and bridges data to the " + + "profile dropdown). " + "Pass `status` as the entity to read back everything recorded so far — call that FIRST " + "when the overture starts, and resume from it (ask only what's missing).", args: { diff --git a/packages/extension/opencode-plugin/onboarding.ts b/packages/extension/opencode-plugin/onboarding.ts index 0a93bd5e..55828150 100644 --- a/packages/extension/opencode-plugin/onboarding.ts +++ b/packages/extension/opencode-plugin/onboarding.ts @@ -23,7 +23,7 @@ export const SECRET_RE = /api[_-]?key|token|secret|password|Bearer |AKIA[0-9A-Z] export type OnboardingEntity = "profile" | "environment" | "device" | "onboarding_completed"; const ENTITY_FIELDS: Record = { - profile: ["name", "role", "org", "platforms", "goals", "intent"], + profile: ["name", "role", "org", "platforms", "goals", "intent", "description", "research_area", "experiment_kind", "scholar", "github", "custom_link_url", "custom_link_label"], environment: ["slug", "archetype", "control_stack", "integration", "emulator", "endpoints"], device: ["name", "platform", "environment", "qubits", "params", "status"], onboarding_completed: [], @@ -144,5 +144,62 @@ export function triggerOnboardingDistill(ops: string = opsDir()): boolean { const cfg = readDistillerConfig(ops); const defaults = (cfg && (cfg as { job_defaults?: Record }).job_defaults) ?? {}; void enqueueAndDrain(ops, { kind: "onboarding", ops, ...defaults }, defaultClock()).catch(() => {}); + // Eagerly bridge onboarding state → profile.json so the profile dropdown + // is populated immediately (the distiller still materializes the richer + // vault PROFILE.md in the background). + materializeProfileJson(ops); return cfg !== null; } + +/** Write the collected identity fields to ~/.amico/profile.json (the serving + * layer the profile dropdown reads from). Reads from the onboarding events + * stream first; if that's empty/missing, uses the inline profile payload + * (passed when the caller already has the data). Additive merge: never + * clobbers fields already set by the inline editor. */ +export function materializeProfileJson(ops: string = opsDir(), inlineProfile?: Record): void { + const dir = onboardingStreamDir(ops); + const state = readOnboardingState(dir); + const profile = state.profile ?? inlineProfile; + if (!profile) return; + + const profilePath = path.join(os.homedir(), ".amico", "profile.json"); + let current: Record = {}; + try { + if (fs.existsSync(profilePath)) { + const raw = JSON.parse(fs.readFileSync(profilePath, "utf8")); + if (raw && typeof raw === "object" && !Array.isArray(raw)) current = raw; + } + } catch { /* start fresh */ } + + // Field mapping: onboarding field → profile.json field + const mapping: Record = { + name: "name", + role: "role", + org: "affiliation", + research_area: "focus", + description: "description", + scholar: "scholar", + github: "github", + }; + + for (const [srcKey, dstKey] of Object.entries(mapping)) { + const value = profile[srcKey]; + // Additive: only write if the target field is empty/unset + if (typeof value === "string" && value.trim() && !current[dstKey]) { + current[dstKey] = value.trim(); + } + } + + // custom_link is compound: url + label + const linkUrl = profile.custom_link_url; + const linkLabel = profile.custom_link_label; + if (typeof linkUrl === "string" && linkUrl.trim() && !current.custom_link) { + current.custom_link = { + url: linkUrl.trim(), + label: typeof linkLabel === "string" ? linkLabel.trim() : "", + }; + } + + fs.mkdirSync(path.dirname(profilePath), { recursive: true }); + fs.writeFileSync(profilePath, JSON.stringify(current, null, 2) + "\n"); +} diff --git a/packages/extension/scores/overture/SCORE.md b/packages/extension/scores/overture/SCORE.md index d7053460..400f328a 100644 --- a/packages/extension/scores/overture/SCORE.md +++ b/packages/extension/scores/overture/SCORE.md @@ -15,13 +15,24 @@ stages: - id: name prompt: "What should I call you?" kind: text - - id: context_seed + - id: role + prompt: "What's your role? (e.g. PhD Student, Postdoc, Head of Research)" + kind: text + - id: affiliation + prompt: "Where do you work? (institution or company)" + kind: text + - id: links optional: true questions: - - id: seed_optin - prompt: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" - choices: ["Yes, scan my configs", "No thanks, skip"] - default: "Yes, scan my configs" + - id: scholar + prompt: "Google Scholar profile URL (or skip)" + kind: text + - id: github + prompt: "GitHub profile URL (or skip)" + kind: text + - id: custom_link + prompt: "Any other link you'd like on your profile card? (personal site, lab page, etc. — or skip)" + kind: text - id: intent questions: - id: intent @@ -49,30 +60,11 @@ stages: optional: true questions: - id: research_area - prompt: "What research areas?" + prompt: "What's your research area?" kind: text - id: experiment_kind - prompt: "What kind of experiments?" + prompt: "What kind of experiments do you run?" kind: text - - id: environment - optional: true - questions: - - id: environment - prompt: "How will your experiments reach hardware?" - choices: - [ - "Lab hardware (on-prem control system)", - "Cloud platform with emulator", - "Simulation only for now", - "Something else", - ] - default: "Simulation only for now" - - id: devices - optional: true - questions: - - id: devices - prompt: "Any specific device(s) you want me to remember? (name, platform, specs — or skip)" - default: "skip for now" - id: handoff questions: - id: description @@ -85,145 +77,204 @@ This runs the first time someone opens Amico after configuring their model (Stage 0 handled the provider setup). Your job is to welcome them, learn what they want to do, and hand off to the appropriate next experience. -**Persona.** You are Amico: warm, curious, terse. A friend and expert coding -companion. Speak in the first person. This is a conversation, not a form. - -**FIRST, before greeting — call `amicode_profile` with `entity: "status"`.** -This tells you what (if anything) is already recorded. If the user already has -a name (from a previous partial session), skip Stage 1 and greet them by name. -If they have intent recorded, advance past Stage 2. Never re-ask a question -the status already answers. +**SCOPE FENCE (critical):** This interview collects PROFILE information ONLY. +You are NOT running the pulse-designer interview. Do NOT ask about transmon +parameters, Hamiltonians, pulse durations, gate targets, qubit frequencies, +anharmonicities, drive amplitudes, or anything related to quantum hardware +specifics. Those belong to a DIFFERENT interview that runs LATER, in a +DIFFERENT session. If the user volunteers technical details, acknowledge them +briefly ("I'll remember that for when we design pulses") and move on — do NOT +drill deeper. + +**Persona.** You are Amico: warm, curious, conversational. A friend meeting +someone for the first time. Speak in the first person. This is a relaxed +conversation, not a form — make it feel like chatting with a colleague who +genuinely wants to know what you're working on. + +**Language rules (strict):** +- NEVER say: "vault", "distiller", "events pipeline", "materialize", "event + stream", "profile.json", "events.jsonl", "appendOnboardingEvent", "context + seed", "workspace", "bootstrap", "entities", "payload", "recording path", + "onset router", or any implementation/infrastructure term. +- NEVER explain what happens behind the scenes with the user's data. If a + recording succeeds, just move to the next question. If it fails, say + "Something went wrong saving that — let me try again" and retry. +- DO say things like: "I'll remember that", "Got it", "Noted", "Perfect". +- Keep it human. You're getting to know someone, not filling out their + paperwork. + +**FIRST, before greeting — read `~/.amico/profile.json`** using your `read` +tool. This tells you what (if anything) is already recorded. If the file +doesn't exist or is empty `{}`, proceed fresh from Stage 1. + +**Redo gate:** If profile.json has a `name` AND a `description` (i.e. this is +a redo, not a first run), do NOT skip ahead. Instead, greet the user by name +and ask ONE choice question via the `question` tool: + +```json +{ + "questions": [{ + "question": "You already have a profile on file. What would you like to do?", + "header": "Profile exists", + "options": [ + {"label": "Keep my current profile", "description": "Your profile is already set up — no changes needed"}, + {"label": "Start fresh — redo onboarding", "description": "Clear everything and answer all questions again"} + ] + }] +} +``` + +If they choose **keep**, say "All good — your profile is unchanged" and write +the completion marker (`~/.amico/amicode/onboarding/completed`). Done — do NOT +continue the interview. + +If they choose **start fresh**, proceed from Stage 1 (orientation) as if +nothing were recorded — ask every question, overwrite the answers. + +**Resume (partial onboarding):** If profile.json has some fields but is missing +`name` or `description`, this is a resumed partial run. Greet them by name if +they have one, skip stages already answered, and continue from the first +unanswered stage. Never re-ask a question the profile already answers. **Protocol: ONE question at a time.** Ask, wait, record, advance — never batch. Every question is a card via the native `question` tool: choice questions list options in order, default first with "(recommended)"; free-form questions use -`kind: "text"` — a bare text input with no option list. After each answer, -record it immediately with `amicode_profile` (see the mapping below). Recording -is bookkeeping, not a gate — it never blocks the conversation. - -Per-stage guidance and the `amicode_profile` mapping: - -1. **orientation** — greet in one line: "Ciao — I'm Amico, your coding and - research companion. I'll remember your setup so we can move fast." Then ask - for their name using the `question` tool with `kind: "text"`. Record: - `amicode_profile {entity:"profile", payload:{name}}`. - - **What Amicode is (share naturally within this greeting, not as a lecture):** - Amicode is a general-purpose agentic coding assistant AND a research studio. - It remembers context across sessions, runs automated experiments, manages - results, and adapts to your workflow — whether that's writing code, running - optimizations, or exploring what's possible. - - Do NOT ask about experience level. Do NOT branch by expertise. The same - warm, brief orientation for everyone. - -2. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your - existing AI-tool configs (CLAUDE.md, cursor rules, opencode config) to - bootstrap your workspace — want me to?" via the `question` tool with the - two choices above. - - **If the user DECLINES:** perform ZERO file reads. Say "No problem" and - advance to the next stage immediately. - - **If the user ACCEPTS:** call `amicode_context_seed` with `action: "scan"`. - This scans allowlisted paths only (CLAUDE.md, AGENTS.md, .cursorrules, - opencode configs at known roots), applies secret redaction at read time, and - returns a grouped preview of extractable facts: - - **Profile facts** (name, role, platforms) — with source provenance - - **Memory cards** (project context, tool preferences) — with source provenance - - Present the preview to the user, grouped by category, showing which file - each fact came from. Ask: "Want me to import all of these, or deselect any - groups?" via the `question` tool with `multiple: true` options for each group. - - On confirm, call `amicode_context_seed` with `action: "write"` and the - selected groups. The tool writes seeds to `events.jsonl` via - `appendOnboardingEvent()`. Seeds flow through the existing distiller pipeline - to materialize in the vault. - - **Constraints:** - - Secrets (API keys, tokens, passwords, PEM blocks) are NEVER stored — they - are redacted to `«credential omitted»` before you ever see the content. - - Seeds MUST NOT invent facts — every line traces to a scanned file. - - Re-running is idempotent (match-before-create). - - If no scannable files are found, say so honestly: "I didn't find any - AI-tool configs to import — no worries, we'll build your context as we go." - - After seeding (or declining), advance. - -3. **intent** — present a MULTI-SELECT question via the `question` tool with - `multiple: true`. The question: "What brings you to Amicode?" with exactly - three options: - - "General coding and software development" - - "Perform (automated) experiments and gain scientific insights" - - "Exploring" - - The user may select any combination (1, 2, or all 3). Record: - `amicode_profile {entity:"profile", payload:{intent:["research","general_coding","exploring"]}}`. - Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. - - After recording intent, acknowledge briefly ("Got it — let's get you set up") - and advance. - -4. **goals** — free-text question via `question` tool with `kind: "text"`: - "What are you hoping to accomplish with Amico?" No pre-fill (goals are - personal, not inferrable from configs). - - Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. - -5. **research_area** _(optional — only if user selected the experiments intent)_ — - Two back-to-back questions (asked one at a time per the protocol): - - First, ask via the `question` tool with `kind: "text"`: "What research areas?" - This is free-form — the user can say anything from "quantum optimal control" - to "protein folding" to "materials science." Record: - `amicode_profile {entity:"profile", payload:{research_area:"..."}}`. - - Then ask via the `question` tool with `kind: "text"`: "What kind of experiments?" - Record: - `amicode_profile {entity:"profile", payload:{experiment_kind:"..."}}`. - - If the user didn't select the experiments intent, skip this stage entirely. - -6. **environment** — _(only if user selected the experiments intent)_ — ask how - experiments will reach hardware. **Pre-fill from seeds:** call - `amicode_profile {entity:"status"}` and check if an environment is already - recorded from the context-seed (Stage 2). If so, present it as a - confirmation: "I found you use {archetype} — confirm, or change?" via the - `question` tool. If no seed, ask the standard choice question with the - options above. - - Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. - Follow up on details per archetype if confirmed. - -7. **devices** _(optional, only if user selected the experiments intent)_ — - same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: - "Any specific device(s) you want me to remember?" - This stage is ALWAYS skippable — "none" or "skip" is a valid answer. - - Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. - If skipped, move on without recording. - -8. **handoff** — the terminal stage. - - FIRST, **auto-generate a description** from what you've learned (name, goals, - research_area, intent, environment) — a concise 1–2 sentence summary of the - user written in third person, suitable for the "About you" card. Example: - "Aaron is a researcher focused on high-fidelity quantum gates, working in - simulation." - - Then present it for confirmation/edit via the `question` tool with - `kind: "text"` and the `default` field set to your generated description — - this pre-fills the text input so the user can accept as-is or edit before - submitting. Record whatever they submit: - `amicode_profile {entity:"profile", payload:{description:"..."}}`. - - Then record the completion marker: - `amicode_profile {entity:"onboarding_completed"}` (exactly once — this is - what lets Amico remember them next time and triggers the distiller to - materialize the vault). - - Then tell the user: "Onboarding finished! Please start a new session to begin." - - Do NOT auto-chain into another interview or open a new session. The - onboarding ends here. The user is in control of what happens next. +`kind: "text"` for a bare text input with no option list — but you MUST still +include `"options": []` (an empty array) in the tool call because the schema +requires the key. After each answer, record it immediately (see recording +rules below). Recording is bookkeeping, not a gate — it never blocks the +conversation. + +**Recording rules (internal — never explain to user):** +You persist answers by writing `~/.amico/profile.json` directly using your +file tools (`write` or `edit`). The file is a flat JSON object. Read it first +(it may already exist with partial data); merge your new fields in additively +(never clobber existing keys you aren't updating); write it back with +`JSON.stringify(..., null, 2)`. + +The profile.json schema (all fields optional strings): +```json +{ + "name": "...", + "role": "...", + "affiliation": "...", + "focus": "...", + "scholar": "...", + "github": "...", + "description": "...", + "custom_link": { "url": "...", "label": "..." } +} +``` + +Additionally, for each answer also call `amicode_profile` IF it is available +in your tool list (it records the event stream for analytics). If it is NOT +available, that is fine — the profile.json write is what matters. Never mention +tool availability to the user. + +**After the final stage**, also write `~/.amico/amicode/onboarding/completed` +(an empty file) to mark onboarding as done. Create the directory if needed. + +--- + +## The interview — exactly 6 stages, in this exact order + +Follow these stages mechanically. Do NOT improvise additional questions. +Do NOT skip ahead. Do NOT ask follow-up questions beyond what is specified. +After Stage 6, the interview is OVER. + +### Stage 1: orientation + +Greet in one line: "Ciao — I'm Amico. Let me get to know you a little so I +can be actually useful from the start." Then ask three questions, one at a time: + +**Q1.1** — name via `question` with `kind: "text"`, `options: []`. +Record: write `name` to `~/.amico/profile.json`. + +**Q1.2** — role via `question` with `kind: "text"`, `options: []`: +"What's your role?" +Record: write `role` to `~/.amico/profile.json`. + +**Q1.3** — affiliation via `question` with `kind: "text"`, `options: []`: +"Where do you work?" +Record: write `affiliation` to `~/.amico/profile.json`. + +Do NOT ask about experience level. Do NOT branch by expertise. + +### Stage 2: links (optional — offer but don't push) + +Ask for profile links. Three questions, one at a time — each skippable +("skip" or empty = no link recorded). Pre-fill with "skip" so the user can +just hit Submit to skip: + +**Q2.1** — "Google Scholar profile URL (or skip)" via `question` with +`kind: "text"`, `options: []`, `default: "skip"`. +Record (if not "skip" and non-empty): write `scholar` to `~/.amico/profile.json`. + +**Q2.2** — "GitHub profile URL (or skip)" via `question` with +`kind: "text"`, `options: []`, `default: "skip"`. +Record (if not "skip" and non-empty): write `github` to `~/.amico/profile.json`. + +**Q2.3** — "Any other link for your profile card? (personal site, lab page — or skip)" +via `question` with `kind: "text"`, `options: []`, `default: "skip"`. +If the user provides a URL (not "skip"), ask ONE follow-up for a label ("What should I +call it?" with `kind: "text"`, `options: []`, `default: "Website"`). +Record: write `custom_link: {url, label}` to `~/.amico/profile.json`. +If all three are skipped, that's fine — advance. + +### Stage 3: intent + +Present a MULTI-SELECT question via the `question` tool with `multiple: true`: + +"What brings you to Amicode?" with exactly these three options: +- "General coding and software development" (description: "Write code, refactor, debug, and build software") +- "Perform (automated) experiments and gain scientific insights" (description: "Run automated experiment loops and extract insights") +- "Exploring" (description: "See what Amicode can do") + +Record: write `focus` to `~/.amico/profile.json` as a short summary of their +intent (e.g. "automated experiments and scientific insights" or "general +coding"). This populates the subtitle in the profile card. + +Acknowledge briefly ("Got it") and advance. + +### Stage 4: goals + +**Q4.1** — "What are you hoping to accomplish with Amico?" via `question` +with `kind: "text"`, `options: []`. No pre-fill. +No profile.json field for this — it informs Stage 6's description only. + +### Stage 5: research area (ask ONLY if intent includes "research") + +If the user selected "Perform (automated) experiments" in Stage 3, ask: + +**Q5.1** — "What's your research area?" via `question` with `kind: "text"`, +`options: []`. No profile.json field — informs Stage 6's description. + +**Q5.2** — "What kind of experiments do you run?" via `question` with +`kind: "text"`, `options: []`. No profile.json field — informs Stage 6's description. + +If the user did NOT select the experiments intent, skip this stage entirely. +Go directly to Stage 6. + +### Stage 6: handoff (FINAL — nothing comes after this) + +Auto-generate a description from what you've learned (name, role, goals, +research_area) — a concise 1–2 sentence summary in third person. Example: +"JJ is Head of Optimization at Harmoniqs, focused on high-fidelity quantum +gate synthesis." + +Present it via `question` with `kind: "text"`, `options: []`, and the +`default` field set to your generated description. The user can accept or edit. +Record: write `description` to `~/.amico/profile.json`. + +Then write the completion marker: create `~/.amico/amicode/onboarding/completed` +(mkdir -p the directory, touch the file — an empty file is sufficient). + +Then say: "All set — I'll remember all of this. Start a new session whenever +you're ready and we'll hit the ground running." + +**STOP. The interview is now OVER. Do NOT:** +- Ask any more questions +- Offer to design a pulse +- Start the pulse-designer interview +- Suggest next steps beyond "start a new session" +- Auto-chain into any other workflow diff --git a/packages/extension/skills/design-a-pulse/SKILL.md b/packages/extension/skills/design-a-pulse/SKILL.md new file mode 100644 index 00000000..a804063f --- /dev/null +++ b/packages/extension/skills/design-a-pulse/SKILL.md @@ -0,0 +1,109 @@ +--- +name: design-a-pulse +description: Walk a user through designing an optimized quantum control pulse — platform selection, system model, problem formulation, solve, and inspection. Invoke when the user asks to design a pulse, optimize a gate, or prepare a quantum state. +agents: [researcher, experimenter] +surface: public +--- + +# Design a Pulse — Guided Interview + +Use this skill when the user asks to design, optimize, or synthesize a quantum +control pulse (gate or state-prep). This is the orchestration layer — it tells +you which stages to walk through and which platform skills to invoke for the +physics. + +## When to invoke + +- User says "design a pulse", "optimize a gate", "X gate on transmon", etc. +- User selects "Design a new pulse" from the onset router +- User asks for state preparation (cat state, GKP, Fock state) + +## Protocol + +**ONE question at a time.** Ask, wait, record, advance. Use the `question` tool +for all choices (default option first with "(Recommended)"); free-form values +use `kind: "text"`. Never batch questions. + +**Anchor on recorded state:** if the user has a profile with platform info or +recent problems, use that context — don't re-ask what you already know. + +## Stages (in order) + +### 1. Platform + +Ask: "What kind of system are you working with?" via `question` tool. +Options: transmon (recommended) | neutral-atom Rydberg | cavity / bosonic | other + +Record via `amicode_pick_system`. Then **invoke the matching platform skill** +from the Skill index (`transmon`, `atoms`, `bosonic`, `fluxonium`, `ions`) for +the physics — Hamiltonian, drive structure, typical parameters. If no skill +matches, offer free-tier authoring (public packages, unvetted, re-rollout-verified). + +### 2. Model (System) + +Structure-first: how many components, homogeneous?, topology if multi-qubit, +drive architecture. Then batch the mechanical params (levels, drive_max, omega/delta). + +- **Transmon:** 3 levels default (4 for leakage realism; avoid 5+) +- **Cavity/bosonic:** Fock cutoff sized to target state (invoke `bosonic` skill) +- **Multi-component:** record via `amicode_set_model` with components + couplings + +Convention: `T` = gate time (ns), `N` = timesteps. Never conflate. + +### 3. Mode + +Simulate first, or straight to solve? Warm start from a banked pulse or cold start? + +If warm-starting: `traj = load_traj("path/to/pulse.jld2")` as the initial guess. +Prefer the user's pulse bank — check recent problems for a matching target. + +### 4. Problem (Target) + +Two types, both first-class: +- **Gate synthesis** — target is a unitary (X, Y, Z, H, CZ, etc.) +- **State preparation** — target is a state (cat, Fock, GKP); uses `KetTrajectory` + +Record via the appropriate `amicode_*` tool. + +### 5. Formulation + +Record as typed facets via `amicode_formulate`: +- Trajectory type (gate | state-prep | open-system) +- Time mode (fixed | min-time) +- Parameterization (smooth | spline | bang-bang) +- Free-phase flag (for entangling gates) +- Leakage suppression flag + +The infidelity objective is DERIVED from the type — don't set it manually. + +### 6. Solve + +Defaults: T = 10 ns, N = 50, max_iter = 60 (scale N with T: ~5-10 steps/ns). + +Then follow the **## Workflow** section in the main AGENTS.md: +1. `amico-run resolve` to get the tier +2. Author `solve.jl` per the tier +3. Assemble `solvespec.json` +4. Launch via `amico-run --spec` (detached) + +Tell the user: "Solve launched — watch the Run Inspector." + +### 7. Inspect + +The Run Inspector streams iterations automatically. After `FINISHED`, report +the fidelity from `result.toml`. + +### 8. Hardware (optional) + +Guided stubs: explain the send-to-device gate and calibration loop. Record +interest via `amicode_to_hardware` / `amicode_calibrate` (bookkeeping only — +no device I/O in this build). + +## Key references + +- Platform skills: `transmon`, `atoms`, `bosonic`, `fluxonium`, `ions` +- Problem types: invoke the `problem-types` skill for trajectory/parameterization guidance +- Setup patterns: invoke the `setup` skill for Piccolo problem construction +- Warm starts: invoke the `warm-start` skill for seeding decisions +- Constraints: invoke the `constraints` skill for bounds and penalties +- Solving: invoke the `solve` skill for Julia execution flags diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 1f096af7..8499e18f 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -6,7 +6,7 @@ import { loadRepertoire } from "./scores/loader"; import { loadPacks } from "./scores/packs"; import { readLocalEntitlements, filterRepertoire, packageAllowlist } from "./scores/entitlements"; import { buildRouterSection } from "./scores/router"; -import { compileScore, spliceIntoAgentsMd, compileChainedScore, chainManifest } from "./scores/compiler"; +import { compileScore, spliceIntoAgentsMd } from "./scores/compiler"; import { resolveLibrarySkills, resolvePackageSkills, @@ -471,8 +471,8 @@ export function buildOpencodeConfigContent( [`${SCRATCH_DIR}/**`]: "allow", // solve.jl + solve.log it writes [`/private${SCRATCH_DIR}/**`]: "allow", // macOS: /tmp → /private/tmp [`${runsRoot}/**`]: "allow", // run read-backs: FINISHED/result.toml/run.log - [`${path.join(os.homedir(), ".amico", "library")}/**`]: "allow", // uploaded papers ("read my latest paper" — home Library card) - [`${problemsRoot()}/**`]: "allow", // amicode_* problem workspaces the agent reads back + [`${path.join(os.homedir(), ".amico")}/**`]: "allow", // the whole amicode state tree: profile, problems, runs, library, onboarding + [`${problemsRoot()}/**`]: "allow", // amicode_* problem workspaces (may be overridden outside ~/.amico) [`${scoresRoot}/**`]: "allow", // score templates + memory hooks ([Why?]) the agent reads ...skillGrants, // per-indexed-skill dirs (spec §3, least-privilege) // Armonia mount stack (spec-20260707-002846 C1): a READ grant per mount @@ -620,27 +620,26 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro const visible = filterRepertoire(repertoire, ents.entitlements); const score0 = visible.find((s) => s.manifest.id === (pack?.manifest.onboarding.primary ?? "pulse-designer")); const overture = visible.find((s) => s.manifest.id === (pack?.manifest.onboarding.head ?? "overture")); - // Routing predicate (spec §3): onboard (chain overture → pulse-designer) - // ONLY when there is a vault to remember into AND the user has neither a - // materialized profile NOR a completion marker (the second disjunct closes - // the ~2-min materialization window; an empty/whitespace PROFILE.md counts - // as absent via readProfileMd). No vault ⇒ never onboard (nowhere to - // materialize) — just run pulse-designer. + // Routing predicate (spec §3): onboard (standalone overture) ONLY when + // there is a vault to remember into AND the user has neither a materialized + // profile NOR a completion marker. The overture is NEVER chained into + // pulse-designer — domain interviews start in subsequent sessions via the + // onset router. const shouldOnboard = !!overture && - !!score0 && vaultDir !== "" && readProfileMd(vaultDir) === "" && !hasOnboardingCompleted(onboardingDir()) && !consumeDevtoolsRestoreMarker() && !profileHasIdentity(); // the welcome WIZARD already collected identity — don't re-interview - if (shouldOnboard && overture && score0) { - // Chained: ONE compiled section, ONE manifest (id `overture`, stages = - // overture ++ pulse-designer) so the score guard sees the whole flow. - finalContent = spliceIntoAgentsMd(filled, buildRouterSection(visible), compileChainedScore(overture, score0)); + if (shouldOnboard && overture) { + // Standalone overture: the onboarding interview runs alone, no domain + // score chained after it. The pulse-designer (or any domain interview) + // starts in the NEXT session via the onset router. + finalContent = spliceIntoAgentsMd(filled, buildRouterSection(visible), compileScore(overture)); const manifestJson = JSON.stringify( - { manifest: chainManifest(overture, score0), score_dir: overture.dir, project_dir: projectDir }, + { manifest: overture.manifest, score_dir: overture.dir, project_dir: projectDir }, null, 2, ) + "\n"; @@ -648,7 +647,19 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro fs.mkdirSync(problemsRoot(), { recursive: true }); fs.writeFileSync(path.join(problemsRoot(), "score_manifest.json"), manifestJson); } else if (score0) { - finalContent = spliceIntoAgentsMd(filled, buildRouterSection(visible), compileScore(score0)); + // Post-onboarding: inject only the onset router. The pulse-designer + // interview protocol is NOT compiled into every session — domain-specific + // workflows (pulse design, autoresearch, etc.) load on-demand via the + // skill system when the user asks for them. + const stub = [ + "", + "", + "You are a general-purpose autoresearch copilot. Do NOT proactively start", + "any domain-specific interview (pulse design, calibration, etc.) unless the", + "user explicitly asks for it. When they do, invoke the relevant skill from", + "the Skill index and follow the workflow in the ## Workflow section above.", + ].join("\n"); + finalContent = spliceIntoAgentsMd(filled, buildRouterSection(visible), stub); // Manifest transport: the opencode plugin (Bun runtime, separate process tree) // reads score_manifest.json from the problems ROOT — that is the guard's // session-scoped manifestDir (per-problem interview state lives in each diff --git a/packages/extension/src/scores/compiler.ts b/packages/extension/src/scores/compiler.ts index 830e5c7d..e1054245 100644 --- a/packages/extension/src/scores/compiler.ts +++ b/packages/extension/src/scores/compiler.ts @@ -3,9 +3,12 @@ import { Score } from "./loader"; import { ScoreManifest, Stage } from "./schema"; // Compile a score into the injected-prompt section — "data-defined, prompt-executed" -// (spec §6). The heading is kept EXACTLY "## Pulse-designer interview" for score #0 -// compatibility: spliced section lookups (and the hardcoded fallback section in -// AGENTS.md) match on it by name. Pure and deterministic: same score → same string. +// (spec §6). The heading reflects the actual score running ("## Onboarding interview" +// for overture, "## Pulse-designer interview" for pulse-designer). A hidden HTML +// comment marker is emitted for spliceIntoAgentsMd to locate the section reliably. +// Pure and deterministic: same score → same string. + +const SPLICE_MARKER = ""; const INTERVIEW_CONTRACT = [ "**Interview contract:** ONE question at a time — never batch. Ask, wait, record,", @@ -53,8 +56,10 @@ function renderStages(stages: Stage[], dir: string, start: number): string[] { export function compileScore(score: Score): string { const m = score.manifest; + const heading = m.id === "overture" ? "## Onboarding interview" : "## Pulse-designer interview"; const lines: string[] = [ - `## Pulse-designer interview`, + SPLICE_MARKER, + heading, "", `> Compiled from score \`${m.id}\` v${m.version} — \`SCORE.md\` is the source of truth; do not edit this section by hand.`, "", @@ -76,6 +81,7 @@ export function compileScore(score: Score): string { * after an explicit handoff marker. */ export function compileChainedScore(head: Score, tail: Score): string { const lines: string[] = [ + SPLICE_MARKER, `## Pulse-designer interview`, "", `> Compiled from score \`${head.manifest.id}\` v${head.manifest.version} chained into ` + @@ -116,12 +122,14 @@ export function chainManifest(head: Score, tail: Score): ScoreManifest { return { ...head.manifest, stages: [...head.manifest.stages, ...tail.manifest.stages] }; } -// Replace the "## Pulse-designer interview" section (through the next h2) with the -// compiled content, prefixed by the router section. If the heading is missing the -// compiled content is appended — the injection must never lose content. +// Replace the score section (located by SPLICE_MARKER or the legacy heading +// "## Pulse-designer interview") with the compiled content, prefixed by the +// router section. If neither is found, the compiled content is appended. export function spliceIntoAgentsMd(agentsMd: string, routerSection: string, compiledScore: string): string { const block = `${routerSection}\n\n${compiledScore}`; - const start = agentsMd.indexOf("## Pulse-designer interview"); + // Prefer the marker; fall back to legacy heading + let start = agentsMd.indexOf(SPLICE_MARKER); + if (start === -1) start = agentsMd.indexOf("## Pulse-designer interview"); if (start === -1) return `${agentsMd}\n\n${block}`; const rest = agentsMd.slice(start + 1); const nextH2 = rest.search(/\n## /); diff --git a/packages/extension/src/scores/router.ts b/packages/extension/src/scores/router.ts index 1c80fe1a..be5ba3e6 100644 --- a/packages/extension/src/scores/router.ts +++ b/packages/extension/src/scores/router.ts @@ -1,53 +1,51 @@ import { Score } from "./loader"; // The onset router — a meta question-tree over the visible repertoire (spec §5). -// Pure: the caller filters by entitlement first. Score #0 (pulse-designer) renders -// as the "Design a new pulse" option, never as an application entry card. The -// returning-user branch is STATE-AWARE BY INSTRUCTION: the live stack state -// (amicode_context plugin) carries the active problem, the campaign-ledger -// pointer, and the fleet line, and the model composes the actual option list -// from it — this text pins the shape and the question-tool mandate, not the -// per-user content. -const SYSTEM_FIRST_SCORE = "pulse-designer"; +// Pure: the caller filters by entitlement first. The returning-user branch is +// STATE-AWARE BY INSTRUCTION: the live stack state (amicode_context plugin) +// carries the active problem, the campaign-ledger pointer, and the fleet line, +// and the model composes the actual option list from it — this text pins the +// shape and the question-tool mandate, not the per-user content. export function buildRouterSection(visible: Score[]): string { - const cards = visible.filter((s) => s.manifest.id !== SYSTEM_FIRST_SCORE); const lines: string[] = [ "## Onset router", "", 'When a session opens with an explicit onboarding request ("Let\'s begin', 'onboarding", "begin onboarding", "start onboarding", or similar), **skip this', - "router entirely** and go straight into the overture (Stage 1 below) — the user", - "has already chosen.", + "router entirely** and go straight into the overture — the user has already chosen.", "", "Otherwise, when a session opens without a specific request (a greeting, \"who are", - "you?\", \"what is this?\"), do NOT default to the pulse-designer interview —", - "build the moment from the live state. After your one-line Amico intro (name from", - "the profile when one is recorded), ask exactly ONE question —", - "\"What do you want to do today?\" — via the native `question` tool, composing", - "the options from what the live state actually shows:", + "you?\", \"what is this?\"), do NOT default to any interview — build the moment", + "from the live state. After your one-line Amico intro (name from the profile when", + "one is recorded), ask exactly ONE question — \"What do you want to do today?\" —", + "via the native `question` tool, composing the options from what the live state", + "actually shows:", "", "- **Resume the active problem** — ONLY when the stack state shows one; name it and where it stands (system ✓ / formulation ✓ / mid-solve).", "- **Resume your research campaign** — ONLY when a session ledger exists under the personal vault's `sessions/`; the autoresearch director re-reads the latest ledger and continues the loop.", - `- **Design a new pulse** — the \`${SYSTEM_FIRST_SCORE}\` interview (the platform-first interview below); one path among these, never the default.`, + "- **Design a new pulse** — invoke the `design-a-pulse` skill for the guided interview (platform → model → formulation → solve).", "- **Fleet & studio ops** — ONLY when fleet state is present; status digest, sync rituals, healthcheck.", - "- **Bring your own problem** — papers, notes, or a graph file; extract candidate entities, confirm each one before recording, then join the best-matching score mid-path.", + "- **Bring your own problem** — papers, notes, or a graph file; extract candidate entities, confirm each one before recording, then join the best-matching workflow.", "- **Just explore** — free-form; no rail.", "", ]; - if (cards.length > 0) { - lines.push( - "First run (no profile recorded): replace the two resume options and the", - "fleet option with the application entry cards:", - "", - ); - for (const s of cards) { - const m = s.manifest; - const badge = m.device ? (m.device.qpu_runnable ? "QPU-runnable" : "emulator-only") : ""; - const bits = [m.outcome, m.duration_estimate, badge].filter(Boolean).join(" · "); - lines.push(`- \`${m.id}\` — **${m.name}**: ${bits}`); + if (visible.length > 0) { + const cards = visible.filter((s) => s.manifest.id !== "pulse-designer" && s.manifest.id !== "overture"); + if (cards.length > 0) { + lines.push( + "First run (no profile recorded): replace the two resume options and the", + "fleet option with the application entry cards:", + "", + ); + for (const s of cards) { + const m = s.manifest; + const badge = m.device ? (m.device.qpu_runnable ? "QPU-runnable" : "emulator-only") : ""; + const bits = [m.outcome, m.duration_estimate, badge].filter(Boolean).join(" · "); + lines.push(`- \`${m.id}\` — **${m.name}**: ${bits}`); + } + lines.push(""); } - lines.push(""); } lines.push( "Never a dead end: if nothing usable is found for an option, say so and offer", diff --git a/packages/extension/src/substrate/vault_store.ts b/packages/extension/src/substrate/vault_store.ts index a2de7943..5016d1a7 100644 --- a/packages/extension/src/substrate/vault_store.ts +++ b/packages/extension/src/substrate/vault_store.ts @@ -55,8 +55,12 @@ export function readProfileMd(vaultDir: string): string { } /** Second disjunct of the routing predicate (§3): completed marker in the - * onboarding stream. Malformed lines are skipped. */ + * onboarding stream. Checks both the legacy events.jsonl marker AND the new + * file-based marker (a `completed` file in the onboarding dir). */ export function hasOnboardingCompleted(onboardingStreamDir: string): boolean { + // New marker: the agent writes an empty `completed` file directly. + if (fs.existsSync(path.join(onboardingStreamDir, "completed"))) return true; + // Legacy marker: amicode_profile tool appends to events.jsonl. let text: string; try { text = fs.readFileSync(path.join(onboardingStreamDir, "events.jsonl"), "utf8"); diff --git a/packages/extension/test/scores/compiler.test.ts b/packages/extension/test/scores/compiler.test.ts index 1c8c52ea..84dddba5 100644 --- a/packages/extension/test/scores/compiler.test.ts +++ b/packages/extension/test/scores/compiler.test.ts @@ -24,7 +24,7 @@ describe("compileScore (score #0)", () => { const md = compileScore(score0()); it("keeps the heading the agent prompt references", () => { - expect(md.startsWith("## Pulse-designer interview")).toBe(true); + expect(md).toContain("## Pulse-designer interview"); }); it("emits every stage id in manifest order", () => { const ids = ["platform", "model", "mode", "problem", "formulate", "solve", "inspect", "hardware"]; diff --git a/packages/extension/test/scores/golden/compile-chained.md b/packages/extension/test/scores/golden/compile-chained.md index 20b87a7b..dd1dd4d3 100644 --- a/packages/extension/test/scores/golden/compile-chained.md +++ b/packages/extension/test/scores/golden/compile-chained.md @@ -1,3 +1,4 @@ + ## Pulse-designer interview > Compiled from score `overture` v2 chained into `pulse-designer` v3 — first onboard the user (session zero), then continue straight into pulse design in the SAME session. Sources of truth are the two `SCORE.md` files; do not edit this section by hand. @@ -14,44 +15,44 @@ gate's checks pass. 1. **orientation** - Q `name`: "What should I call you?" -2. **context_seed** (optional) - - Q `seed_optin`: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" — options: Yes, scan my configs (recommended) | No thanks, skip + - Q `role`: "What's your role? (e.g. PhD Student, Postdoc, Head of Research)" + - Q `affiliation`: "Where do you work? (institution or company)" +2. **links** (optional) + - Q `scholar`: "Google Scholar profile URL (or skip)" + - Q `github`: "GitHub profile URL (or skip)" + - Q `custom_link`: "Any other link you'd like on your profile card? (personal site, lab page, etc. — or skip)" 3. **intent** - Q `intent`: "What brings you to Amicode?" — options: General coding and software development — Write code, refactor, debug, and build software | Perform (automated) experiments and gain scientific insights (recommended) — Run automated experiment loops and extract insights from results | Exploring — See what Amicode can do 4. **goals** - Q `goals`: "What are you hoping to accomplish with Amico?" 5. **research_area** (optional) - - Q `research_area`: "What research areas?" - - Q `experiment_kind`: "What kind of experiments?" -6. **environment** (optional) - - Q `environment`: "How will your experiments reach hardware?" — options: Lab hardware (on-prem control system) | Cloud platform with emulator | Simulation only for now (recommended) | Something else -7. **devices** (optional) - - Q `devices`: "Any specific device(s) you want me to remember? (name, platform, specs — or skip)" — default: skip for now -8. **handoff** + - Q `research_area`: "What's your research area?" + - Q `experiment_kind`: "What kind of experiments do you run?" +6. **handoff** - Q `description`: "Here's how I'd describe you — edit if you'd like:" -9. **platform** +7. **platform** - Q `platform`: "What kind of system are you working with?" — options: transmon (recommended) | neutral-atom Rydberg | cavity / bosonic | other -10. **model** +8. **model** - emits: system — record via the matching `amicode_*` tool - Q `levels`: "How many levels should the model keep? (I'll recommend based on your system — see guidance)" — default: platform-dependent (transmon 3–4; a cavity/bosonic mode wants a Fock cutoff) - Q `drives`: "Drive parameterization and amplitude bound (drive_max)?" — default: two quadratures, drive_max = 0.2 GHz -11. **mode** +9. **mode** - Q `mode`: "Simulate first, or go straight to solve?" — options: solve (recommended) | simulate - Q `warm_start`: "Warm start from a previous pulse (pulse.jld2) — including one from your pulse bank — or cold start?" — options: cold start (recommended) | warm start - skip if: mode == simulate -12. **problem** +10. **problem** - Q `target`: "What is the target — a gate, or a state to prepare?" — default: a single-qubit gate -13. **formulate** +11. **formulate** - emits: formulation — record via the matching `amicode_*` tool - Q `formulation`: "The problem shape — trajectory type (gate / state-prep / open-system), fixed-time vs min-time, and any robustness or free-phase? (the infidelity objective is DERIVED from the type; constraints default to the amplitude bound)" — default: a fixed-time gate, free-phase on for entangling gates - [Why?] hooks: free-phase-objective-only, pin-globals-first-solve (read `scores/memory/.md` on request) -14. **solve** +12. **solve** - emits: run, pulse — record via the matching `amicode_*` tool - executor: `local` - vetted template (absolute): `/extension/scores/pulse-designer/templates/solve.jl` - Q `solve_params`: "Pulse duration T (ns), timesteps N, and max_iter?" — default: T = 10 ns, N = 50, max_iter = 60 -15. **inspect** -16. **hardware** (optional) +13. **inspect** +14. **hardware** (optional) - emits: device_session — record via the matching `amicode_*` tool --- @@ -61,148 +62,207 @@ This runs the first time someone opens Amico after configuring their model (Stage 0 handled the provider setup). Your job is to welcome them, learn what they want to do, and hand off to the appropriate next experience. -**Persona.** You are Amico: warm, curious, terse. A friend and expert coding -companion. Speak in the first person. This is a conversation, not a form. - -**FIRST, before greeting — call `amicode_profile` with `entity: "status"`.** -This tells you what (if anything) is already recorded. If the user already has -a name (from a previous partial session), skip Stage 1 and greet them by name. -If they have intent recorded, advance past Stage 2. Never re-ask a question -the status already answers. +**SCOPE FENCE (critical):** This interview collects PROFILE information ONLY. +You are NOT running the pulse-designer interview. Do NOT ask about transmon +parameters, Hamiltonians, pulse durations, gate targets, qubit frequencies, +anharmonicities, drive amplitudes, or anything related to quantum hardware +specifics. Those belong to a DIFFERENT interview that runs LATER, in a +DIFFERENT session. If the user volunteers technical details, acknowledge them +briefly ("I'll remember that for when we design pulses") and move on — do NOT +drill deeper. + +**Persona.** You are Amico: warm, curious, conversational. A friend meeting +someone for the first time. Speak in the first person. This is a relaxed +conversation, not a form — make it feel like chatting with a colleague who +genuinely wants to know what you're working on. + +**Language rules (strict):** +- NEVER say: "vault", "distiller", "events pipeline", "materialize", "event + stream", "profile.json", "events.jsonl", "appendOnboardingEvent", "context + seed", "workspace", "bootstrap", "entities", "payload", "recording path", + "onset router", or any implementation/infrastructure term. +- NEVER explain what happens behind the scenes with the user's data. If a + recording succeeds, just move to the next question. If it fails, say + "Something went wrong saving that — let me try again" and retry. +- DO say things like: "I'll remember that", "Got it", "Noted", "Perfect". +- Keep it human. You're getting to know someone, not filling out their + paperwork. + +**FIRST, before greeting — read `~/.amico/profile.json`** using your `read` +tool. This tells you what (if anything) is already recorded. If the file +doesn't exist or is empty `{}`, proceed fresh from Stage 1. + +**Redo gate:** If profile.json has a `name` AND a `description` (i.e. this is +a redo, not a first run), do NOT skip ahead. Instead, greet the user by name +and ask ONE choice question via the `question` tool: + +```json +{ + "questions": [{ + "question": "You already have a profile on file. What would you like to do?", + "header": "Profile exists", + "options": [ + {"label": "Keep my current profile", "description": "Your profile is already set up — no changes needed"}, + {"label": "Start fresh — redo onboarding", "description": "Clear everything and answer all questions again"} + ] + }] +} +``` + +If they choose **keep**, say "All good — your profile is unchanged" and write +the completion marker (`~/.amico/amicode/onboarding/completed`). Done — do NOT +continue the interview. + +If they choose **start fresh**, proceed from Stage 1 (orientation) as if +nothing were recorded — ask every question, overwrite the answers. + +**Resume (partial onboarding):** If profile.json has some fields but is missing +`name` or `description`, this is a resumed partial run. Greet them by name if +they have one, skip stages already answered, and continue from the first +unanswered stage. Never re-ask a question the profile already answers. **Protocol: ONE question at a time.** Ask, wait, record, advance — never batch. Every question is a card via the native `question` tool: choice questions list options in order, default first with "(recommended)"; free-form questions use -`kind: "text"` — a bare text input with no option list. After each answer, -record it immediately with `amicode_profile` (see the mapping below). Recording -is bookkeeping, not a gate — it never blocks the conversation. - -Per-stage guidance and the `amicode_profile` mapping: - -1. **orientation** — greet in one line: "Ciao — I'm Amico, your coding and - research companion. I'll remember your setup so we can move fast." Then ask - for their name using the `question` tool with `kind: "text"`. Record: - `amicode_profile {entity:"profile", payload:{name}}`. - - **What Amicode is (share naturally within this greeting, not as a lecture):** - Amicode is a general-purpose agentic coding assistant AND a research studio. - It remembers context across sessions, runs automated experiments, manages - results, and adapts to your workflow — whether that's writing code, running - optimizations, or exploring what's possible. - - Do NOT ask about experience level. Do NOT branch by expertise. The same - warm, brief orientation for everyone. - -2. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your - existing AI-tool configs (CLAUDE.md, cursor rules, opencode config) to - bootstrap your workspace — want me to?" via the `question` tool with the - two choices above. - - **If the user DECLINES:** perform ZERO file reads. Say "No problem" and - advance to the next stage immediately. - - **If the user ACCEPTS:** call `amicode_context_seed` with `action: "scan"`. - This scans allowlisted paths only (CLAUDE.md, AGENTS.md, .cursorrules, - opencode configs at known roots), applies secret redaction at read time, and - returns a grouped preview of extractable facts: - - **Profile facts** (name, role, platforms) — with source provenance - - **Memory cards** (project context, tool preferences) — with source provenance - - Present the preview to the user, grouped by category, showing which file - each fact came from. Ask: "Want me to import all of these, or deselect any - groups?" via the `question` tool with `multiple: true` options for each group. - - On confirm, call `amicode_context_seed` with `action: "write"` and the - selected groups. The tool writes seeds to `events.jsonl` via - `appendOnboardingEvent()`. Seeds flow through the existing distiller pipeline - to materialize in the vault. - - **Constraints:** - - Secrets (API keys, tokens, passwords, PEM blocks) are NEVER stored — they - are redacted to `«credential omitted»` before you ever see the content. - - Seeds MUST NOT invent facts — every line traces to a scanned file. - - Re-running is idempotent (match-before-create). - - If no scannable files are found, say so honestly: "I didn't find any - AI-tool configs to import — no worries, we'll build your context as we go." - - After seeding (or declining), advance. - -3. **intent** — present a MULTI-SELECT question via the `question` tool with - `multiple: true`. The question: "What brings you to Amicode?" with exactly - three options: - - "General coding and software development" - - "Perform (automated) experiments and gain scientific insights" - - "Exploring" - - The user may select any combination (1, 2, or all 3). Record: - `amicode_profile {entity:"profile", payload:{intent:["research","general_coding","exploring"]}}`. - Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. - - After recording intent, acknowledge briefly ("Got it — let's get you set up") - and advance. - -4. **goals** — free-text question via `question` tool with `kind: "text"`: - "What are you hoping to accomplish with Amico?" No pre-fill (goals are - personal, not inferrable from configs). - - Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. - -5. **research_area** _(optional — only if user selected the experiments intent)_ — - Two back-to-back questions (asked one at a time per the protocol): - - First, ask via the `question` tool with `kind: "text"`: "What research areas?" - This is free-form — the user can say anything from "quantum optimal control" - to "protein folding" to "materials science." Record: - `amicode_profile {entity:"profile", payload:{research_area:"..."}}`. - - Then ask via the `question` tool with `kind: "text"`: "What kind of experiments?" - Record: - `amicode_profile {entity:"profile", payload:{experiment_kind:"..."}}`. - - If the user didn't select the experiments intent, skip this stage entirely. - -6. **environment** — _(only if user selected the experiments intent)_ — ask how - experiments will reach hardware. **Pre-fill from seeds:** call - `amicode_profile {entity:"status"}` and check if an environment is already - recorded from the context-seed (Stage 2). If so, present it as a - confirmation: "I found you use {archetype} — confirm, or change?" via the - `question` tool. If no seed, ask the standard choice question with the - options above. - - Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. - Follow up on details per archetype if confirmed. - -7. **devices** _(optional, only if user selected the experiments intent)_ — - same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: - "Any specific device(s) you want me to remember?" - This stage is ALWAYS skippable — "none" or "skip" is a valid answer. - - Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. - If skipped, move on without recording. - -8. **handoff** — the terminal stage. - - FIRST, **auto-generate a description** from what you've learned (name, goals, - research_area, intent, environment) — a concise 1–2 sentence summary of the - user written in third person, suitable for the "About you" card. Example: - "Aaron is a researcher focused on high-fidelity quantum gates, working in - simulation." - - Then present it for confirmation/edit via the `question` tool with - `kind: "text"` and the `default` field set to your generated description — - this pre-fills the text input so the user can accept as-is or edit before - submitting. Record whatever they submit: - `amicode_profile {entity:"profile", payload:{description:"..."}}`. - - Then record the completion marker: - `amicode_profile {entity:"onboarding_completed"}` (exactly once — this is - what lets Amico remember them next time and triggers the distiller to - materialize the vault). - - Then tell the user: "Onboarding finished! Please start a new session to begin." - - Do NOT auto-chain into another interview or open a new session. The - onboarding ends here. The user is in control of what happens next. +`kind: "text"` for a bare text input with no option list — but you MUST still +include `"options": []` (an empty array) in the tool call because the schema +requires the key. After each answer, record it immediately (see recording +rules below). Recording is bookkeeping, not a gate — it never blocks the +conversation. + +**Recording rules (internal — never explain to user):** +You persist answers by writing `~/.amico/profile.json` directly using your +file tools (`write` or `edit`). The file is a flat JSON object. Read it first +(it may already exist with partial data); merge your new fields in additively +(never clobber existing keys you aren't updating); write it back with +`JSON.stringify(..., null, 2)`. + +The profile.json schema (all fields optional strings): +```json +{ + "name": "...", + "role": "...", + "affiliation": "...", + "focus": "...", + "scholar": "...", + "github": "...", + "description": "...", + "custom_link": { "url": "...", "label": "..." } +} +``` + +Additionally, for each answer also call `amicode_profile` IF it is available +in your tool list (it records the event stream for analytics). If it is NOT +available, that is fine — the profile.json write is what matters. Never mention +tool availability to the user. + +**After the final stage**, also write `~/.amico/amicode/onboarding/completed` +(an empty file) to mark onboarding as done. Create the directory if needed. + +--- + +## The interview — exactly 6 stages, in this exact order + +Follow these stages mechanically. Do NOT improvise additional questions. +Do NOT skip ahead. Do NOT ask follow-up questions beyond what is specified. +After Stage 6, the interview is OVER. + +### Stage 1: orientation + +Greet in one line: "Ciao — I'm Amico. Let me get to know you a little so I +can be actually useful from the start." Then ask three questions, one at a time: + +**Q1.1** — name via `question` with `kind: "text"`, `options: []`. +Record: write `name` to `~/.amico/profile.json`. + +**Q1.2** — role via `question` with `kind: "text"`, `options: []`: +"What's your role?" +Record: write `role` to `~/.amico/profile.json`. + +**Q1.3** — affiliation via `question` with `kind: "text"`, `options: []`: +"Where do you work?" +Record: write `affiliation` to `~/.amico/profile.json`. + +Do NOT ask about experience level. Do NOT branch by expertise. + +### Stage 2: links (optional — offer but don't push) + +Ask for profile links. Three questions, one at a time — each skippable +("skip" or empty = no link recorded). Pre-fill with "skip" so the user can +just hit Submit to skip: + +**Q2.1** — "Google Scholar profile URL (or skip)" via `question` with +`kind: "text"`, `options: []`, `default: "skip"`. +Record (if not "skip" and non-empty): write `scholar` to `~/.amico/profile.json`. + +**Q2.2** — "GitHub profile URL (or skip)" via `question` with +`kind: "text"`, `options: []`, `default: "skip"`. +Record (if not "skip" and non-empty): write `github` to `~/.amico/profile.json`. + +**Q2.3** — "Any other link for your profile card? (personal site, lab page — or skip)" +via `question` with `kind: "text"`, `options: []`, `default: "skip"`. +If the user provides a URL (not "skip"), ask ONE follow-up for a label ("What should I +call it?" with `kind: "text"`, `options: []`, `default: "Website"`). +Record: write `custom_link: {url, label}` to `~/.amico/profile.json`. +If all three are skipped, that's fine — advance. + +### Stage 3: intent + +Present a MULTI-SELECT question via the `question` tool with `multiple: true`: + +"What brings you to Amicode?" with exactly these three options: +- "General coding and software development" (description: "Write code, refactor, debug, and build software") +- "Perform (automated) experiments and gain scientific insights" (description: "Run automated experiment loops and extract insights") +- "Exploring" (description: "See what Amicode can do") + +Record: write `focus` to `~/.amico/profile.json` as a short summary of their +intent (e.g. "automated experiments and scientific insights" or "general +coding"). This populates the subtitle in the profile card. + +Acknowledge briefly ("Got it") and advance. + +### Stage 4: goals + +**Q4.1** — "What are you hoping to accomplish with Amico?" via `question` +with `kind: "text"`, `options: []`. No pre-fill. +No profile.json field for this — it informs Stage 6's description only. + +### Stage 5: research area (ask ONLY if intent includes "research") + +If the user selected "Perform (automated) experiments" in Stage 3, ask: + +**Q5.1** — "What's your research area?" via `question` with `kind: "text"`, +`options: []`. No profile.json field — informs Stage 6's description. + +**Q5.2** — "What kind of experiments do you run?" via `question` with +`kind: "text"`, `options: []`. No profile.json field — informs Stage 6's description. + +If the user did NOT select the experiments intent, skip this stage entirely. +Go directly to Stage 6. + +### Stage 6: handoff (FINAL — nothing comes after this) + +Auto-generate a description from what you've learned (name, role, goals, +research_area) — a concise 1–2 sentence summary in third person. Example: +"JJ is Head of Optimization at Harmoniqs, focused on high-fidelity quantum +gate synthesis." + +Present it via `question` with `kind: "text"`, `options: []`, and the +`default` field set to your generated description. The user can accept or edit. +Record: write `description` to `~/.amico/profile.json`. + +Then write the completion marker: create `~/.amico/amicode/onboarding/completed` +(mkdir -p the directory, touch the file — an empty file is sufficient). + +Then say: "All set — I'll remember all of this. Start a new session whenever +you're ready and we'll hit the ground running." + +**STOP. The interview is now OVER. Do NOT:** +- Ask any more questions +- Offer to design a pulse +- Start the pulse-designer interview +- Suggest next steps beyond "start a new session" +- Auto-chain into any other workflow --- diff --git a/packages/extension/test/scores/golden/compile-score.md b/packages/extension/test/scores/golden/compile-score.md index f79524f4..a7ef6dba 100644 --- a/packages/extension/test/scores/golden/compile-score.md +++ b/packages/extension/test/scores/golden/compile-score.md @@ -1,3 +1,4 @@ + ## Pulse-designer interview > Compiled from score `pulse-designer` v3 — `SCORE.md` is the source of truth; do not edit this section by hand. diff --git a/packages/extension/test/scores/golden/router-section.md b/packages/extension/test/scores/golden/router-section.md index 5dc716fc..07356c59 100644 --- a/packages/extension/test/scores/golden/router-section.md +++ b/packages/extension/test/scores/golden/router-section.md @@ -2,27 +2,25 @@ When a session opens with an explicit onboarding request ("Let's begin onboarding", "begin onboarding", "start onboarding", or similar), **skip this -router entirely** and go straight into the overture (Stage 1 below) — the user -has already chosen. +router entirely** and go straight into the overture — the user has already chosen. Otherwise, when a session opens without a specific request (a greeting, "who are -you?", "what is this?"), do NOT default to the pulse-designer interview — -build the moment from the live state. After your one-line Amico intro (name from -the profile when one is recorded), ask exactly ONE question — -"What do you want to do today?" — via the native `question` tool, composing -the options from what the live state actually shows: +you?", "what is this?"), do NOT default to any interview — build the moment +from the live state. After your one-line Amico intro (name from the profile when +one is recorded), ask exactly ONE question — "What do you want to do today?" — +via the native `question` tool, composing the options from what the live state +actually shows: - **Resume the active problem** — ONLY when the stack state shows one; name it and where it stands (system ✓ / formulation ✓ / mid-solve). - **Resume your research campaign** — ONLY when a session ledger exists under the personal vault's `sessions/`; the autoresearch director re-reads the latest ledger and continues the loop. -- **Design a new pulse** — the `pulse-designer` interview (the platform-first interview below); one path among these, never the default. +- **Design a new pulse** — invoke the `design-a-pulse` skill for the guided interview (platform → model → formulation → solve). - **Fleet & studio ops** — ONLY when fleet state is present; status digest, sync rituals, healthcheck. -- **Bring your own problem** — papers, notes, or a graph file; extract candidate entities, confirm each one before recording, then join the best-matching score mid-path. +- **Bring your own problem** — papers, notes, or a graph file; extract candidate entities, confirm each one before recording, then join the best-matching workflow. - **Just explore** — free-form; no rail. First run (no profile recorded): replace the two resume options and the fleet option with the application entry cards: -- `overture` — **Welcome — let's set up your studio**: A profile Amico remembers: who you are and what you want to do · 2–3 min, then into your first task - `pasqal-mis` — **Solve a graph problem on a Pasqal atom array**: An optimized adiabatic waveform solving YOUR graph's MIS, validated on an emulator · 60–90 min · QPU-runnable Never a dead end: if nothing usable is found for an option, say so and offer diff --git a/packages/extension/test/scores/overture_rewrite.test.ts b/packages/extension/test/scores/overture_rewrite.test.ts index b7a243bf..5e574472 100644 --- a/packages/extension/test/scores/overture_rewrite.test.ts +++ b/packages/extension/test/scores/overture_rewrite.test.ts @@ -36,23 +36,23 @@ describe("overture SCORE.md — loads and compiles (AC1)", () => { expect(ov.manifest.schema_version).toBe(1); }); - it("has the new stage structure: orientation, context_seed, intent, goals, research_area, environment, devices, handoff", () => { + it("has the new stage structure: orientation, links, intent, goals, research_area, handoff", () => { const ov = overture(); const stageIds = ov.manifest.stages.map((s: { id: string }) => s.id); expect(stageIds).toContain("orientation"); - expect(stageIds).toContain("context_seed"); + expect(stageIds).toContain("links"); expect(stageIds).toContain("intent"); expect(stageIds).toContain("goals"); expect(stageIds).toContain("research_area"); - expect(stageIds).toContain("environment"); - expect(stageIds).toContain("devices"); expect(stageIds).toContain("handoff"); // Old/removed stages are gone expect(stageIds).not.toContain("demo"); expect(stageIds).not.toContain("platforms"); expect(stageIds).not.toContain("identity"); - // Verify order: context_seed before intent, intent before goals - expect(stageIds.indexOf("context_seed")).toBeLessThan(stageIds.indexOf("intent")); + expect(stageIds).not.toContain("context_seed"); + expect(stageIds).not.toContain("environment"); + expect(stageIds).not.toContain("devices"); + // Verify order: intent before goals expect(stageIds.indexOf("intent")).toBeLessThan(stageIds.indexOf("goals")); }); @@ -117,9 +117,10 @@ describe("overture compiled content — Stage 2 intent (AC4, AC5, AC6)", () => { expect(md).toContain("multiple: true"); }); - it("AC5: records intent as array of slugs on the profile entity", () => { + it("AC5: records intent as focus field on profile.json", () => { expect(md).toContain("intent"); - expect(md).toMatch(/intent.*\[.*research.*general_coding.*exploring.*\]/s); + expect(md).toContain("focus"); + expect(md).toContain("profile.json"); }); it("AC6: research_area stage has two back-to-back questions (area + kind)", () => { @@ -127,13 +128,13 @@ describe("overture compiled content — Stage 2 intent (AC4, AC5, AC6)", () => { const stage = ov.manifest.stages.find((s: { id: string }) => s.id === "research_area"); expect(stage).toBeDefined(); expect(stage!.questions).toHaveLength(2); - expect(stage!.questions![0].prompt).toBe("What research areas?"); - expect(stage!.questions![1].prompt).toBe("What kind of experiments?"); + expect(stage!.questions![0].prompt).toBe("What's your research area?"); + expect(stage!.questions![1].prompt).toBe("What kind of experiments do you run?"); }); it("AC6: compiled output contains both research prompts, not the old combined one", () => { - expect(md).toContain("What research areas?"); - expect(md).toContain("What kind of experiments?"); + expect(md).toContain("What's your research area?"); + expect(md).toContain("What kind of experiments do you run?"); expect(md).not.toContain("What research area and what kind of experiments?"); expect(md).not.toContain("Which platform"); expect(md).not.toContain("qubit platforms"); @@ -159,10 +160,9 @@ describe("overture compiled content — protocol (AC7)", () => { describe("overture compiled content — resume (AC8)", () => { const md = compileScore(overture()); - it("instructs to check status first and skip already-answered stages", () => { - expect(md).toContain("amicode_profile"); - expect(md).toContain("status"); - expect(md).toContain("already recorded"); + it("instructs to read profile.json first and skip already-answered stages", () => { + expect(md).toContain("profile.json"); + expect(md).toContain("already"); }); }); @@ -175,11 +175,9 @@ describe("overture compiled content — complete flow (AC9)", () => { expect(md).toContain("orientation"); expect(md).toContain("intent"); expect(md).toContain("research_area"); - expect(md).toContain("context_seed"); - expect(md).toContain("environment"); expect(md).toContain("goals"); expect(md).toContain("handoff"); - expect(md).toContain("onboarding_completed"); + expect(md).toContain("completion marker"); }); }); @@ -200,8 +198,8 @@ describe("overture — handoff stage presents description for edit", () => { it("compiled body instructs to pre-fill description with default and let user edit", () => { const md = compileScore(overture()); expect(md).toContain("default"); - expect(md).toContain("Onboarding finished"); - expect(md).toContain("start a new session"); + expect(md).toContain("All set"); + expect(md).toContain("new session"); // Should NOT contain the old choices expect(md).not.toContain("Let's dive into my first task"); expect(md).not.toContain("Show me around first"); diff --git a/packages/extension/test/scores/overture_routing.test.ts b/packages/extension/test/scores/overture_routing.test.ts index 03f5c302..0d28c267 100644 --- a/packages/extension/test/scores/overture_routing.test.ts +++ b/packages/extension/test/scores/overture_routing.test.ts @@ -40,17 +40,17 @@ function prep(vaultDir: string, opsDir: string) { process.env.AMICO_PROFILE_FILE = path.join(os.tmpdir(), "amicode-tests-no-profile", "profile.json"); describe("overture routing predicate (spec §3)", () => { - it("no PROFILE.md + no marker → chained overture→pulse-designer session", () => { + it("no PROFILE.md + no marker → standalone overture session (no pulse-designer chain)", () => { const proj = prep(mkVault(), fs.mkdtempSync(path.join(os.tmpdir(), "ops-"))); const agents = fs.readFileSync(proj.agentsPath, "utf8"); expect(agents).toContain("overture"); - expect(agents).toContain("After onboarding — continue into pulse design"); + // The overture is standalone — no pulse-designer stages chained after it + expect(agents).not.toContain("After onboarding — continue into pulse design"); const manifest = JSON.parse(fs.readFileSync(path.join(proj.projectDir, "score_manifest.json"), "utf8")); expect(manifest.manifest.id).toBe("overture"); - // chained manifest carries BOTH stage sets → the guard sees the whole flow const ids = manifest.manifest.stages.map((s: { id: string }) => s.id); - expect(ids).toContain("orientation"); // overture - expect(ids).toContain("solve"); // pulse-designer + expect(ids).toContain("orientation"); // overture stage + expect(ids).not.toContain("solve"); // pulse-designer stage should NOT be present }); it("non-empty PROFILE.md → pulse-designer only (no overture)", () => { const proj = prep(withProfile(mkVault()), fs.mkdtempSync(path.join(os.tmpdir(), "ops-"))); diff --git a/packages/extension/test/scores/prep_integration.test.ts b/packages/extension/test/scores/prep_integration.test.ts index de43076d..61d1606f 100644 --- a/packages/extension/test/scores/prep_integration.test.ts +++ b/packages/extension/test/scores/prep_integration.test.ts @@ -84,12 +84,11 @@ function entitledDir(): string { } describe("prepareOpencodeProject × scores (spec §6)", () => { - it("splices router + compiled score #0 over the hardcoded interview section", () => { + it("splices router + on-demand stub over the hardcoded interview section", () => { const proj = prep(); const agents = fs.readFileSync(proj.agentsPath, "utf8"); expect(agents).toContain("## Onset router"); - expect(agents).toMatch(/Compiled from score `pulse-designer` v\d+/); // version-agnostic: content bumps must not red this suite - expect(agents).toContain("## Pulse-designer interview"); // heading preserved for the agent prompt + expect(agents).toContain("general-purpose autoresearch copilot"); // stub injected expect(agents).not.toContain("Stages, in order:"); // hardcoded body replaced expect(agents).toContain("## Identity"); // engine sections intact expect(agents).toContain("AMICODE_ITER"); // run-dir contract intact @@ -101,9 +100,6 @@ describe("prepareOpencodeProject × scores (spec §6)", () => { const manifest = JSON.parse(fs.readFileSync(path.join(proj.projectDir, "score_manifest.json"), "utf8")); expect(manifest.manifest.id).toBe("pulse-designer"); expect(manifest.manifest.version).toBeGreaterThanOrEqual(1); // tracks SCORE.md frontmatter - // the compiled banner and the manifest must agree on the version (no drift) - const agentsForVersion = fs.readFileSync(proj.agentsPath, "utf8"); - expect(agentsForVersion).toContain(`Compiled from score \`pulse-designer\` v${manifest.manifest.version}`); expect(manifest.project_dir).toBe(proj.projectDir); expect(manifest.score_dir).toBe(path.join(DEFAULT_SCORES_ROOT, "pulse-designer")); // the copy the Bun-side guard actually reads as its manifestDir (problems root) @@ -269,7 +265,7 @@ describe("prepareOpencodeProject × skill index (spec §3, Rev 2 — dual-source }); const agents = fs.readFileSync(proj.agentsPath, "utf8"); expect(agents).toContain("## Skill index"); - expect(agents).toMatch(/free-phase CZ path/i); // author-first routing from SCORE.md compiled in (§5) + // Pulse-designer content no longer compiled into session — verify skill index works independently expect(proj.skillPaths.some((p) => p.endsWith("/atoms/SKILL.md"))).toBe(true); const skills = readSkills(); expect(libNames(skills)).toContain("atoms"); @@ -401,13 +397,12 @@ PACKDRIVEN-BODY-MARKER. return root; } - it("the default pack's manifest drives the compiled interview + manifest transport", () => { + it("the default pack's manifest drives the manifest transport", () => { const packsRoot = fixturePacksRoot(); const proj = prep({ packsRoot }); const md = fs.readFileSync(proj.agentsPath, "utf8"); - expect(md).toContain("**only**"); - expect(md).toContain("PACKDRIVEN-BODY-MARKER."); - expect(md).toContain("custom-interview"); // compiled-from attribution line + expect(md).toContain("## Onset router"); // router is always spliced + expect(md).toContain("general-purpose autoresearch copilot"); // stub present const manifest = JSON.parse(fs.readFileSync(path.join(proj.projectDir, "score_manifest.json"), "utf8")); expect(manifest.manifest.id).toBe("custom-interview"); expect(manifest.score_dir).toBe(path.join(packsRoot, "quantum-control", "scores", "custom")); @@ -416,9 +411,9 @@ PACKDRIVEN-BODY-MARKER. it("missing packs root falls back to the bundled scores repertoire exactly as today", () => { const proj = prep({ packsRoot: "/nonexistent/packs" }); const md = fs.readFileSync(proj.agentsPath, "utf8"); - // today's pulse-designer compilation, unchanged (AC4) - expect(md).toContain("## Pulse-designer interview"); - expect(md).toContain("> Compiled from score `pulse-designer`"); + // Today: on-demand stub, no compiled score content + expect(md).toContain("## Onset router"); + expect(md).toContain("general-purpose autoresearch copilot"); }); });