Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
0604c10
feat(onboarding): add links stage + bridge onboarding to profile.json
jeonghun-jj-lee Aug 23, 2026
8a08b43
fix(onboarding): decouple overture from pulse-designer chain
jeonghun-jj-lee Aug 23, 2026
6a1ec5a
feat(onboarding): add redo gate — ask keep/reset when profile already…
jeonghun-jj-lee Aug 23, 2026
23ef0cf
fix(onboarding): fix redo gate question format for question tool schema
jeonghun-jj-lee Aug 23, 2026
7c481a4
fix(onboarding): materializeProfileJson accepts inline profile fallback
jeonghun-jj-lee Aug 23, 2026
f826a28
fix(onboarding): provide exact JSON example for redo gate question to…
jeonghun-jj-lee Aug 23, 2026
3b258a5
feat(onboarding): ask role + affiliation after name, move links befor…
jeonghun-jj-lee Aug 23, 2026
d96c16d
fix(onboarding): add hard rule — amicode_profile is the ONLY recordin…
jeonghun-jj-lee Aug 23, 2026
a1db249
fix(onboarding): instruct agent to include options:[] for text-kind q…
jeonghun-jj-lee Aug 23, 2026
cf607fb
fix(onboarding): make agent dialogue personable, ban jargon and files…
jeonghun-jj-lee Aug 23, 2026
533f499
fix(onboarding): simplify SCORE to 6 mechanical stages, add scope fence
jeonghun-jj-lee Aug 23, 2026
c4e2686
fix(scores): overture compiles with '## Onboarding interview' heading…
jeonghun-jj-lee Aug 23, 2026
ebd4d26
feat(scores): remove pulse-designer from default session prompt — on-…
jeonghun-jj-lee Aug 23, 2026
3d3c657
feat(skills): add design-a-pulse skill — the pulse interview as on-de…
jeonghun-jj-lee Aug 23, 2026
16a8afe
fix(onboarding): write profile.json directly + pre-fill skip links
jeonghun-jj-lee Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/app-bundle/overlay/packages/schema/src/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
6 changes: 4 additions & 2 deletions packages/extension/opencode-plugin/amicode_tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
59 changes: 58 additions & 1 deletion packages/extension/opencode-plugin/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OnboardingEntity, string[]> = {
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: [],
Expand Down Expand Up @@ -144,5 +144,62 @@ export function triggerOnboardingDistill(ops: string = opsDir()): boolean {
const cfg = readDistillerConfig(ops);
const defaults = (cfg && (cfg as { job_defaults?: Record<string, unknown> }).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<string, unknown>): 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<string, unknown> = {};
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<string, string> = {
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");
}
Loading
Loading