From 0529e8beee6c0753814a3362c6c432788051dc9c Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 6 Sep 2026 21:34:19 -0400 Subject: [PATCH 1/4] =?UTF-8?q?feat(schema):=20the=20mode-id=20read-resolv?= =?UTF-8?q?e=20alias=20table=20(autodev=E2=86=92develop,=20autoresearch?= =?UTF-8?q?=E2=86=92research)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spec-20260907-011500 D1, #858. MODE_ID_ALIASES + resolveModeId: old ids resolve at READ time, never migrate-on-write — append-only artifacts keep old ids forever. build is NOT aliased (it exits the picker, not the vocabulary). The alias window's exit rides the next CONTRACT-VERSION bump (removal is non-additive) — pinned by the mode_aliases.test.ts lint. Refs #858 --- .../agents/{autodev.md => develop.md} | 0 .../agents/{autoresearch.md => research.md} | 0 .../modes/{autodev => develop}/card.md | 0 .../modes/{autodev => develop}/mode.toml | 0 .../modes/{autodev => develop}/pack.toml | 0 .../modes/{autoresearch => research}/card.md | 0 .../{autoresearch => research}/mode.toml | 0 .../{autoresearch => research}/pack.toml | 0 .../{autoresearch => research}/SKILL.md | 0 packages/schema/src/index.ts | 2 + packages/schema/src/mode_registry.ts | 28 ++++++++ packages/schema/test/mode_aliases.test.ts | 67 +++++++++++++++++++ 12 files changed, 97 insertions(+) rename packages/extension/agents/{autodev.md => develop.md} (100%) rename packages/extension/agents/{autoresearch.md => research.md} (100%) rename packages/extension/modes/{autodev => develop}/card.md (100%) rename packages/extension/modes/{autodev => develop}/mode.toml (100%) rename packages/extension/modes/{autodev => develop}/pack.toml (100%) rename packages/extension/modes/{autoresearch => research}/card.md (100%) rename packages/extension/modes/{autoresearch => research}/mode.toml (100%) rename packages/extension/modes/{autoresearch => research}/pack.toml (100%) rename packages/extension/skills/{autoresearch => research}/SKILL.md (100%) create mode 100644 packages/schema/test/mode_aliases.test.ts diff --git a/packages/extension/agents/autodev.md b/packages/extension/agents/develop.md similarity index 100% rename from packages/extension/agents/autodev.md rename to packages/extension/agents/develop.md diff --git a/packages/extension/agents/autoresearch.md b/packages/extension/agents/research.md similarity index 100% rename from packages/extension/agents/autoresearch.md rename to packages/extension/agents/research.md diff --git a/packages/extension/modes/autodev/card.md b/packages/extension/modes/develop/card.md similarity index 100% rename from packages/extension/modes/autodev/card.md rename to packages/extension/modes/develop/card.md diff --git a/packages/extension/modes/autodev/mode.toml b/packages/extension/modes/develop/mode.toml similarity index 100% rename from packages/extension/modes/autodev/mode.toml rename to packages/extension/modes/develop/mode.toml diff --git a/packages/extension/modes/autodev/pack.toml b/packages/extension/modes/develop/pack.toml similarity index 100% rename from packages/extension/modes/autodev/pack.toml rename to packages/extension/modes/develop/pack.toml diff --git a/packages/extension/modes/autoresearch/card.md b/packages/extension/modes/research/card.md similarity index 100% rename from packages/extension/modes/autoresearch/card.md rename to packages/extension/modes/research/card.md diff --git a/packages/extension/modes/autoresearch/mode.toml b/packages/extension/modes/research/mode.toml similarity index 100% rename from packages/extension/modes/autoresearch/mode.toml rename to packages/extension/modes/research/mode.toml diff --git a/packages/extension/modes/autoresearch/pack.toml b/packages/extension/modes/research/pack.toml similarity index 100% rename from packages/extension/modes/autoresearch/pack.toml rename to packages/extension/modes/research/pack.toml diff --git a/packages/extension/skills/autoresearch/SKILL.md b/packages/extension/skills/research/SKILL.md similarity index 100% rename from packages/extension/skills/autoresearch/SKILL.md rename to packages/extension/skills/research/SKILL.md diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 6eee3ecb..e81a55dc 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -101,6 +101,8 @@ export { type ModeBundleOpts, checkConsumerFloor, SUPPORTED_MODE_BUNDLE_VERSION, + MODE_ID_ALIASES, + resolveModeId, type ConsumerFloorOk, type ConsumerFloorGap, parseReleaseIndex, diff --git a/packages/schema/src/mode_registry.ts b/packages/schema/src/mode_registry.ts index 449e074b..ab631a74 100644 --- a/packages/schema/src/mode_registry.ts +++ b/packages/schema/src/mode_registry.ts @@ -480,6 +480,34 @@ export function checkConsumerFloor( * what the floor map catches). */ export const SUPPORTED_MODE_BUNDLE_VERSION = "1"; +// ── the mode-id read-resolve alias table (spec-20260907-011500 D1, #858) ───── +// +// The three-mode surface renamed the director modes: autodev → develop, +// autoresearch → research. The aliases are READ-RESOLVE, never +// migrate-on-write: append-only artifacts (session ledgers, spec +// frontmatter, campaign fixtures) legitimately keep old ids forever and are +// resolved at read time; tooling that joins on a mode id supports both ids +// permanently. `build` is deliberately NOT aliased — it exits the PICKER, +// not the vocabulary (it remains a valid explicit id everywhere: +// default_agent, spawn params, CLI args). +// +// THE ALIAS WINDOW'S EXIT is contract-version-gated, not calendar-gated: +// removing an alias is NON-ADDITIVE (the freeze validator's rule), so it +// rides the next CONTRACT-VERSION bump (SUPPORTED_MODE_BUNDLE_VERSION), not +// a release date. The lint in test/mode_aliases.test.ts pins this gate. +export const MODE_ID_ALIASES: Record = { + autodev: "develop", + autoresearch: "research", +}; + +/** Resolve a mode id through the read-resolve alias table: an old id + * resolves to its renamed mode; every other id (renamed ids, `build`, + * plan, role agents, custom agents) passes through identity. One hop by + * construction — no alias target is itself an alias key. */ +export function resolveModeId(id: string): string { + return MODE_ID_ALIASES[id] ?? id; +} + // ── the release index (AC6) ────────────────────────────────────────────────── export interface ReleaseIndexEntry { diff --git a/packages/schema/test/mode_aliases.test.ts b/packages/schema/test/mode_aliases.test.ts new file mode 100644 index 00000000..968248ea --- /dev/null +++ b/packages/schema/test/mode_aliases.test.ts @@ -0,0 +1,67 @@ +// mode_aliases.test.ts — the read-resolve alias table (spec-20260907-011500 +// D1, issue #858): autodev → develop, autoresearch → research. The aliases +// are READ-RESOLVE, never migrate-on-write: append-only artifacts (session +// ledgers, spec frontmatter, campaign fixtures) legitimately keep old ids +// forever and resolve at read time; tooling that joins on a mode id supports +// both ids permanently. `build` is NOT aliased — it exits the PICKER, not the +// vocabulary (it remains a valid explicit id everywhere). +// +// The alias window's END is contract-version-gated, not calendar-gated: +// removal is NON-ADDITIVE, so it rides the next mode-bundle CONTRACT-VERSION +// bump (the freeze-validator-legal exit). The lint below pins that gate. +import { describe, it, expect } from "vitest"; +import { + MODE_ID_ALIASES, + resolveModeId, + SUPPORTED_MODE_BUNDLE_VERSION, +} from "../src/mode_registry.js"; + +describe("the mode-id read-resolve alias table (spec-20260907-011500 D1, #858)", () => { + it("maps exactly the two renamed director modes", () => { + expect(MODE_ID_ALIASES).toEqual({ autodev: "develop", autoresearch: "research" }); + }); + + it("resolves the old ids and passes everything else through untouched", () => { + expect(resolveModeId("autodev")).toBe("develop"); + expect(resolveModeId("autoresearch")).toBe("research"); + // the renamed ids are identity + expect(resolveModeId("develop")).toBe("develop"); + expect(resolveModeId("research")).toBe("research"); + // `build` remains a valid explicit id — it exits the picker, not the vocabulary + expect(resolveModeId("build")).toBe("build"); + // plan, role agents, custom agents, empty — all identity + expect(resolveModeId("plan")).toBe("plan"); + expect(resolveModeId("implementer")).toBe("implementer"); + expect(resolveModeId("my-custom-agent")).toBe("my-custom-agent"); + expect(resolveModeId("")).toBe(""); + }); + + it("is idempotent (an already-resolved id never double-resolves)", () => { + for (const id of ["autodev", "autoresearch", "develop", "research", "build", "plan"]) { + expect(resolveModeId(resolveModeId(id))).toBe(resolveModeId(id)); + } + }); + + it("no alias target collides with another alias key (the chain is one hop by construction)", () => { + for (const target of Object.values(MODE_ID_ALIASES)) { + expect(MODE_ID_ALIASES, `alias target ${target} must not itself be an alias key`).not.toHaveProperty(target); + } + }); +}); + +describe("the alias window's exit is contract-version-gated (the freeze-validator-legal exit)", () => { + it("while the contract version is 1 the alias window is OPEN — removing the aliases without the bump fails this lint", () => { + // Removal is NON-ADDITIVE (the freeze validator's rule): it must ride the + // next CONTRACT-VERSION bump, never a calendar date. If you are here + // because you deleted the aliases: bump SUPPORTED_MODE_BUNDLE_VERSION and + // this lint passes — that IS the legal exit. + if (SUPPORTED_MODE_BUNDLE_VERSION === "1") { + expect( + Object.keys(MODE_ID_ALIASES).length, + "contract v1 ships the read-resolve alias window; removing the aliases is " + + "non-additive and must ride the next CONTRACT-VERSION bump " + + "(spec-20260907-011500 D1)", + ).toBeGreaterThan(0); + } + }); +}); From f0bb3e28fc2632a78886253bfd70fb73c4b5ee67 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 6 Sep 2026 21:34:23 -0400 Subject: [PATCH 2/4] =?UTF-8?q?feat(extension):=20the=20three-mode=20surfa?= =?UTF-8?q?ce=20=E2=80=94=20modes=20renamed=20develop/research,=20vocabula?= =?UTF-8?q?ry=20swept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #858 (spec-20260907-011500 D1): director cards, mode bundles (manifests' mode/agent ids, pack handoff targets, cards), the research skill rename, and the skill/AGENTS/CONTEXT/README vocabulary all carry the renamed ids. The mode-protocol skill keeps its id 'autodev' (the workflow skill owns 'develop') — the alias covers it; the seeded role cards keep their old prose (seed-gated on a signed amendment, #806). The mode registry passes the shared validator with the renamed set; agent_order: [plan, develop, research] rides the session config (#305's field, honored app-side). build remains a valid explicit id everywhere. Refs #858 --- AGENTS.md | 12 +- CONTEXT.md | 15 +- README.md | 2 +- packages/extension/AGENTS.md | 8 +- packages/extension/agents/develop.md | 10 +- packages/extension/agents/research.md | 8 +- packages/extension/modes/develop/card.md | 10 +- packages/extension/modes/develop/mode.toml | 17 +- packages/extension/modes/develop/pack.toml | 2 +- packages/extension/modes/research/card.md | 8 +- packages/extension/modes/research/mode.toml | 14 +- packages/extension/modes/research/pack.toml | 2 +- .../extension/opencode-plugin/mode_block.ts | 25 ++- .../opencode-plugin/session_spawn.ts | 33 +++- packages/extension/skills/autodev/SKILL.md | 28 +-- packages/extension/skills/develop/SKILL.md | 4 +- .../extension/skills/director-core/SKILL.md | 12 +- .../skills/migrate-research-project/SKILL.md | 4 +- packages/extension/skills/research/SKILL.md | 22 +-- packages/extension/src/mode_cards.ts | 4 +- packages/extension/src/opencode_config.ts | 36 ++-- packages/extension/src/scores/router.ts | 2 +- .../test/fixtures/doctor/doctor-current.json | 16 +- packages/extension/test/gate_packs.test.ts | 20 +- packages/extension/test/mode_block.test.ts | 179 ++++++++++++------ packages/extension/test/mode_cards.test.ts | 8 +- .../extension/test/mode_cards_staging.test.ts | 106 ++++++++++- packages/extension/test/mode_registry.test.ts | 72 +++---- .../test/mode_registry_staging.test.ts | 24 +-- .../extension/test/naming_records.test.ts | 26 +-- .../extension/test/opencode_config.test.ts | 12 +- packages/extension/test/packaging.test.ts | 24 +-- packages/extension/test/role_cards.test.ts | 8 +- .../test/scores/golden/router-section.md | 2 +- .../test/scores/package_skills.test.ts | 6 +- .../test/scores/prep_integration.test.ts | 6 +- packages/extension/test/session_spawn.test.ts | 23 +++ .../test/workflow_skills_public.test.ts | 39 ++-- scripts/deploy-agents.mjs | 4 +- 39 files changed, 557 insertions(+), 296 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0896990a..a741569b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,11 +93,13 @@ macOS note: the vendored binary is unsigned — if Gatekeeper blocks it: lines, `iter_*.png`, `result.toml`, `pulse.jld2`, `FINISHED`). Validate files with `packages/schema/launcher/amico-validate `. - **New sessions open on opencode's `plan` agent** (plan-first posture for all - users): `buildOpencodeConfigContent` injects `default_agent: "plan"`; the - ordered picker is `plan → build → autodev → autoresearch` (default first, - then the director modes). The pulse-designer agent shell is retired (#389); - the interview content lives in the compiled AGENTS.md score section, visible - to every agent. + users): `buildOpencodeConfigContent` injects `default_agent: "plan"` and + `agent_order: ["plan", "develop", "research"]` — the three-mode surface + (spec-20260907-011500 D1, #858: autodev → develop, autoresearch → research; + old ids read-resolve for one release cycle; stock `build` is the implied + auto — a valid explicit id, out of the named set). The pulse-designer agent + shell is retired (#389); the interview content lives in the compiled + AGENTS.md score section, visible to every agent. - Never commit to `main`; branch + PR. ## Changing opencode (the vendored fork) diff --git a/CONTEXT.md b/CONTEXT.md index 35b9e051..327a8681 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,6 +1,6 @@ # Amicode -The VSCode extension + CLI autoresearch studio — researchers propose, run, verify, and record experiments through a structured loop. Quantum optimal control is the primary Domain Pack; the product is the loop, not the domain. +The VSCode extension + CLI research studio — researchers propose, run, verify, and record experiments through a structured loop. Quantum optimal control is the primary Domain Pack; the product is the loop, not the domain. ## Language @@ -71,12 +71,13 @@ _Avoid_: cron, scheduler (as concept names), night shift The role that leads any autonomous loop — one canonical protocol (ledger discipline, dispatch through gates, analyze, record) that every campaign runs under, whichever mode it is bound to. Research and development differ in their gate packs, never in their director. _Avoid_: conductor (standing decision) -**Autoresearch**: -The research mode: hypothesis queue → deliberate spec → experiment → gates → analyzer — the shipped, name-frozen autonomous mode, binding the research gate pack over the director core. +**Research**: +The research mode (renamed from `autoresearch` — the three-mode surface, spec-20260907-011500; old ids read-resolve for one release cycle, never migrated in place): hypothesis queue → deliberate spec → experiment → gates → analyzer — the shipped, name-frozen autonomous mode, binding the research gate pack over the director core. +_Avoid_: autoresearch (the pre-rename id — an alias at read time, not a name) -**Autodev**: -The development mode: issue DAG → TDD slices → CI/review → landed delta — the second autonomous mode, binding the dev gate pack. The loop is issue → PR → merge; automating the walk never weakens the dev gate or the never-merge-non-green rule. -_Avoid_: autobuild ("build" already means CI to everyone) +**Develop**: +The development mode (renamed from `autodev` — the three-mode surface, spec-20260907-011500; old ids read-resolve for one release cycle, never migrated in place): issue DAG → TDD slices → CI/review → landed delta — the second autonomous mode, binding the dev gate pack. The loop is issue → PR → merge; automating the walk never weakens the dev gate or the never-merge-non-green rule. +_Avoid_: autobuild ("build" already means CI to everyone), autodev (the pre-rename id — an alias at read time, not a name) **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. @@ -86,7 +87,7 @@ _Avoid_: session (a copilot session is never a campaign; a campaign ledger is ne 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. **Mode**: -One of the three director postures — copilot (the zeroth: default, interactive, packless), autoresearch, autodev. A mode binds a gate pack iff it is autonomous; the copilot mode binds none. +One of the three director postures — copilot (the zeroth: default, interactive, packless), research, develop. A mode binds a gate pack iff it is autonomous; the copilot mode binds none. _Avoid_: surface, rail (they render and switch modes; a mode is a posture, not a surface) ### Fleet & serving diff --git a/README.md b/README.md index ed056e47..be06d00e 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Skills are not configuration — they are the capability surface. **26 skills** | Surface | What it covers | |---|---| -| Research loop | `autoresearch`, `analyze`, `hypothesis-review`, `dream-reflect`, `open-threads`, `paper-writer`, `create-research-project`, `migrate-research-project` | +| Research loop | `research`, `analyze`, `hypothesis-review`, `dream-reflect`, `open-threads`, `paper-writer`, `create-research-project`, `migrate-research-project` | | Lab + catalog + vault | `amico-lab`, `amico-catalog`, `amico-vault`, `amico-strategy`, `amico-schema-check`, `amico-slack` | | Engineering | `debugging`, `tdd`, `verification`, `brainstorming`, `deliberate`, `grill-me`, `grill-with-docs`, `improve-codebase-architecture`, `teach`, `report-a-bug` | | Entitled surfaces | `piccolissimo`, `intonatissimo` — usage guidance for the `-issimo` performance tiers; ship in the vsix, stage only for entitled sessions | diff --git a/packages/extension/AGENTS.md b/packages/extension/AGENTS.md index b9ad9eb0..32f48cd8 100644 --- a/packages/extension/AGENTS.md +++ b/packages/extension/AGENTS.md @@ -2,10 +2,10 @@ ## Identity -You are **Amico** — Amicode's autoresearch copilot. You are NOT "opencode": +You are **Amico** — Amicode's research copilot. You are NOT "opencode": opencode is the engine underneath, **Amicode** is the product, **Amico** is you. If asked who or what you are, answer in one line — "I'm Amico — Amicode's -autoresearch copilot" — and never describe yourself as an interactive CLI tool. +research copilot" — and never describe yourself as an interactive CLI tool. You run the research loop first — campaigns, hypotheses, spec gates, experiments, mechanical verdicts — plus the dev work that loop needs (issues, @@ -67,7 +67,7 @@ loop is the spine they serve. ## Workflow -The autoresearch loop is domain-agnostic. When a user wants to run an +The research loop is domain-agnostic. When a user wants to run an experiment, invoke the relevant domain skill (e.g. `/solve` for quantum control) which carries the full lifecycle: tier resolution, script authoring, gate launch, verification. The skill is the authoritative reference for domain @@ -116,7 +116,7 @@ Your material is already in this prompt: `About this user`, `Your recent problems`, `Reference demos`, `Memory index`, the `## Skill index`, and the `Mount stack`. Build the pitch in this order: -1. **Open with the autoresearch loop** — propose → trusted gate → independent +1. **Open with the research loop** — propose → trusted gate → independent corrector → stage → human promote. Every experiment feeds the next one; results become reusable knowledge. This is what Amicode IS. 2. **Then THEIR results.** If problem cards or completed runs exist, lead with diff --git a/packages/extension/agents/develop.md b/packages/extension/agents/develop.md index 91fcb949..07ca43be 100644 --- a/packages/extension/agents/develop.md +++ b/packages/extension/agents/develop.md @@ -1,5 +1,5 @@ --- -description: Amico in development mode — the autodev director. Leads the autonomous development loop over the dev gate pack (decompose → implement → integrate), dispatching one implementer per issue slice through the dev gate, TDD red-green, draft-PR lifecycle, and review, with verdicts derived from commands and merges of green work only. Switch into dev mode for issue-DAG campaigns. +description: Amico in development mode — the develop director. Leads the autonomous development loop over the dev gate pack (decompose → implement → integrate), dispatching one implementer per issue slice through the dev gate, TDD red-green, draft-PR lifecycle, and review, with verdicts derived from commands and merges of green work only. Switch into dev mode for issue-DAG campaigns. mode: primary color: accent permission: @@ -7,7 +7,7 @@ permission: bash: allow --- -You are the DIRECTOR of an autodev loop — Amico in development mode. This card is +You are the DIRECTOR of an develop loop — Amico in development mode. This card is the opencode binding of the director role for development campaigns; the engine-neutral protocol lives in the `director-core` skill (canonical copy: the shipped skill library). You automate the *walk*, never the *gate*: @@ -17,7 +17,7 @@ condition, and promotions stay human-only. **First action (kickoff or resume): invoke the `director-core` skill and follow it.** It is the canonical loop protocol; the spine below is its summary, never a replacement. Your mode's specifics — the phase graph, gates, and roles — are the -**dev gate pack** (`modes/autodev/pack.toml` in the amicode repo, schema'd and +**dev gate pack** (`modes/develop/pack.toml` in the amicode repo, schema'd and fixture-tested): phases decompose → implement → integrate. ## The spine @@ -66,7 +66,7 @@ ledger serves every posture the campaign runs. ## The dev gate pack (your mode's binding) The loop above wears the dev gate pack in this mode. Its phases, gates, and -roles are typed data — the committed `modes/autodev/pack.toml` fixture is the contract of +roles are typed data — the committed `modes/develop/pack.toml` fixture is the contract of record; this prose is the binding, never a second spec. - **Decompose** — break the issue DAG into TDD-ready slices (tracer bullets), @@ -98,7 +98,7 @@ retry cycles is a `failed` return, not a negotiation. You are the development posture of the one director. The user can hand you a research-shaped ask (a hypothesis worth an experiment, a question about a -result); answer it as autoresearch or copilot would, file a hypothesis seed +result); answer it as research or copilot would, file a hypothesis seed when it deserves one, then return to the loop. Out-of-posture asks are answered, never silently absorbed into a dev campaign that should not exist. diff --git a/packages/extension/agents/research.md b/packages/extension/agents/research.md index 824c23fa..ba15338e 100644 --- a/packages/extension/agents/research.md +++ b/packages/extension/agents/research.md @@ -1,5 +1,5 @@ --- -description: Amico in research mode — the autoresearch director. Leads the autonomous research loop with session-ledger discipline, the hypothesizer/experimenter/analyzer trio, deliberate spec gates, and mechanical verdicts. Switch into research mode for hypothesis-driven campaigns; interim until the studio rail lands. +description: Amico in research mode — the research director. Leads the autonomous research loop with session-ledger discipline, the hypothesizer/experimenter/analyzer trio, deliberate spec gates, and mechanical verdicts. Switch into research mode for hypothesis-driven campaigns; interim until the studio rail lands. mode: primary color: accent permission: @@ -7,7 +7,7 @@ permission: bash: allow --- -You are the DIRECTOR of an autoresearch loop — Amico in research mode. This card is +You are the DIRECTOR of an research loop — Amico in research mode. This card is the opencode binding of the director role; the engine-neutral protocol lives in the `director-core` skill (canonical copy: the shipped skill library). The operating principle is fixed: **the context window is a cache; the vault is the database.** Every @@ -17,7 +17,7 @@ any compaction costs a cache refill, never state. **First action (kickoff or resume): invoke the `director-core` skill and follow it.** It is the canonical loop protocol; the spine below is its summary, never a replacement. Your mode's specifics — the phase graph, gates, and roles — are the **research gate -pack** (`modes/autoresearch/pack.toml` in the amicode repo, schema'd and fixture-tested): +pack** (`modes/research/pack.toml` in the amicode repo, schema'd and fixture-tested): phases hypothesize → deliberate → experiment → gate → analyze. ## The spine @@ -86,7 +86,7 @@ compaction). ## Posture honesty You are the research posture of the one director — the studio's copilot ↔ -autoresearch ↔ autodev rail supersedes the interim Tab-switch when it lands. +research ↔ develop rail supersedes the interim Tab-switch when it lands. Until then, you carry the research posture: the copilot content (pulse design, solves, the interview) remains available in you, but the loop is the spine. The user can hand you a pulse-design ask; answer it as copilot would, diff --git a/packages/extension/modes/develop/card.md b/packages/extension/modes/develop/card.md index 91fcb949..07ca43be 100644 --- a/packages/extension/modes/develop/card.md +++ b/packages/extension/modes/develop/card.md @@ -1,5 +1,5 @@ --- -description: Amico in development mode — the autodev director. Leads the autonomous development loop over the dev gate pack (decompose → implement → integrate), dispatching one implementer per issue slice through the dev gate, TDD red-green, draft-PR lifecycle, and review, with verdicts derived from commands and merges of green work only. Switch into dev mode for issue-DAG campaigns. +description: Amico in development mode — the develop director. Leads the autonomous development loop over the dev gate pack (decompose → implement → integrate), dispatching one implementer per issue slice through the dev gate, TDD red-green, draft-PR lifecycle, and review, with verdicts derived from commands and merges of green work only. Switch into dev mode for issue-DAG campaigns. mode: primary color: accent permission: @@ -7,7 +7,7 @@ permission: bash: allow --- -You are the DIRECTOR of an autodev loop — Amico in development mode. This card is +You are the DIRECTOR of an develop loop — Amico in development mode. This card is the opencode binding of the director role for development campaigns; the engine-neutral protocol lives in the `director-core` skill (canonical copy: the shipped skill library). You automate the *walk*, never the *gate*: @@ -17,7 +17,7 @@ condition, and promotions stay human-only. **First action (kickoff or resume): invoke the `director-core` skill and follow it.** It is the canonical loop protocol; the spine below is its summary, never a replacement. Your mode's specifics — the phase graph, gates, and roles — are the -**dev gate pack** (`modes/autodev/pack.toml` in the amicode repo, schema'd and +**dev gate pack** (`modes/develop/pack.toml` in the amicode repo, schema'd and fixture-tested): phases decompose → implement → integrate. ## The spine @@ -66,7 +66,7 @@ ledger serves every posture the campaign runs. ## The dev gate pack (your mode's binding) The loop above wears the dev gate pack in this mode. Its phases, gates, and -roles are typed data — the committed `modes/autodev/pack.toml` fixture is the contract of +roles are typed data — the committed `modes/develop/pack.toml` fixture is the contract of record; this prose is the binding, never a second spec. - **Decompose** — break the issue DAG into TDD-ready slices (tracer bullets), @@ -98,7 +98,7 @@ retry cycles is a `failed` return, not a negotiation. You are the development posture of the one director. The user can hand you a research-shaped ask (a hypothesis worth an experiment, a question about a -result); answer it as autoresearch or copilot would, file a hypothesis seed +result); answer it as research or copilot would, file a hypothesis seed when it deserves one, then return to the loop. Out-of-posture asks are answered, never silently absorbed into a dev campaign that should not exist. diff --git a/packages/extension/modes/develop/mode.toml b/packages/extension/modes/develop/mode.toml index 515a9507..60db7db9 100644 --- a/packages/extension/modes/develop/mode.toml +++ b/packages/extension/modes/develop/mode.toml @@ -1,4 +1,4 @@ -# The autodev mode bundle manifest — ONE unit (spec-20260905-063000 D1): the +# The develop mode bundle manifest — ONE unit (spec-20260905-063000 D1): the # card, the gate pack, the role cards the mode dispatches, the protocol skills # it binds, the handoff seed it receives, the schema version, and the # per-consumer minimum-version map. ONE shared validator (packages/schema, @@ -14,11 +14,11 @@ # Content parity against the engine-neutral vault definition is seed-gated # on Aaron's signature — see docs/seed-gate/role-cards-seed-diff.md. schema_version = "1" -mode = "autodev" +mode = "develop" # The agent id that BINDS this mode (D4 posture-binding map, #808): a session -# resolved to this agent runs the autodev director posture; every other agent +# resolved to this agent runs the develop director posture; every other agent # id (plan, build, role agents, custom) binds copilot and is silent. -agent = "autodev" +agent = "develop" card = "card.md" pack = "pack.toml" @@ -40,12 +40,13 @@ name = "implementer" path = "../../agents/implementer.md" # The mode's protocol skills (the binding surface). The five dev-workflow -# skills + the `autodev` mode-protocol skill are PUBLIC with in-repo canonical -# copies under packages/extension/skills/ (#807, D2, ADR-0011 amendment: -# workflow public, package-proprietary gated). +# skills + the `autodev` mode-protocol skill (its id keeps the pre-rename +# mode name; the mode id is `develop` per spec-20260907-011500 D1) are PUBLIC +# with in-repo canonical copies under packages/extension/skills/ (#807, D2, +# ADR-0011 amendment: workflow public, package-proprietary gated). # The handoff seed this mode RECEIVES: research closes with an issue seed and -# hands it to autodev (D6). kind → receiving mode is registry data, checked +# hands it to develop (D6). kind → receiving mode is registry data, checked # structurally by the registry-level validator: every pack handoff targeting # this mode must emit the kind this manifest declares. [[handoff_seeds]] diff --git a/packages/extension/modes/develop/pack.toml b/packages/extension/modes/develop/pack.toml index 54774438..c5fd65fc 100644 --- a/packages/extension/modes/develop/pack.toml +++ b/packages/extension/modes/develop/pack.toml @@ -46,4 +46,4 @@ name = "integrate" [[handoffs]] kind = "hypothesis_seed" -target = "autoresearch" +target = "research" diff --git a/packages/extension/modes/research/card.md b/packages/extension/modes/research/card.md index 824c23fa..ba15338e 100644 --- a/packages/extension/modes/research/card.md +++ b/packages/extension/modes/research/card.md @@ -1,5 +1,5 @@ --- -description: Amico in research mode — the autoresearch director. Leads the autonomous research loop with session-ledger discipline, the hypothesizer/experimenter/analyzer trio, deliberate spec gates, and mechanical verdicts. Switch into research mode for hypothesis-driven campaigns; interim until the studio rail lands. +description: Amico in research mode — the research director. Leads the autonomous research loop with session-ledger discipline, the hypothesizer/experimenter/analyzer trio, deliberate spec gates, and mechanical verdicts. Switch into research mode for hypothesis-driven campaigns; interim until the studio rail lands. mode: primary color: accent permission: @@ -7,7 +7,7 @@ permission: bash: allow --- -You are the DIRECTOR of an autoresearch loop — Amico in research mode. This card is +You are the DIRECTOR of an research loop — Amico in research mode. This card is the opencode binding of the director role; the engine-neutral protocol lives in the `director-core` skill (canonical copy: the shipped skill library). The operating principle is fixed: **the context window is a cache; the vault is the database.** Every @@ -17,7 +17,7 @@ any compaction costs a cache refill, never state. **First action (kickoff or resume): invoke the `director-core` skill and follow it.** It is the canonical loop protocol; the spine below is its summary, never a replacement. Your mode's specifics — the phase graph, gates, and roles — are the **research gate -pack** (`modes/autoresearch/pack.toml` in the amicode repo, schema'd and fixture-tested): +pack** (`modes/research/pack.toml` in the amicode repo, schema'd and fixture-tested): phases hypothesize → deliberate → experiment → gate → analyze. ## The spine @@ -86,7 +86,7 @@ compaction). ## Posture honesty You are the research posture of the one director — the studio's copilot ↔ -autoresearch ↔ autodev rail supersedes the interim Tab-switch when it lands. +research ↔ develop rail supersedes the interim Tab-switch when it lands. Until then, you carry the research posture: the copilot content (pulse design, solves, the interview) remains available in you, but the loop is the spine. The user can hand you a pulse-design ask; answer it as copilot would, diff --git a/packages/extension/modes/research/mode.toml b/packages/extension/modes/research/mode.toml index 5b0899c2..3f13fe27 100644 --- a/packages/extension/modes/research/mode.toml +++ b/packages/extension/modes/research/mode.toml @@ -1,5 +1,5 @@ -# The autoresearch mode bundle manifest — ONE unit (spec-20260905-063000 D1). -# Same contract as modes/autodev/mode.toml: ONE shared validator enforces the +# The research mode bundle manifest — ONE unit (spec-20260905-063000 D1). +# Same contract as modes/develop/mode.toml: ONE shared validator enforces the # pack schema and the declared set. Since D3 (#806) the three role cards this # pack casts are versioned repo sources seeded from the live deployed # artifacts (packages/extension/agents/, provenance in .seed-provenance.json @@ -7,11 +7,11 @@ # director card. The registry is the source of record for the mode surfaces; # the flat agents/ staging (mode_cards.ts) remains opencode's discovery path. schema_version = "1" -mode = "autoresearch" +mode = "research" # The agent id that BINDS this mode (D4 posture-binding map, #808): a session -# resolved to this agent runs the autoresearch director posture; every other +# resolved to this agent runs the research director posture; every other # agent id (plan, build, role agents, custom) binds copilot and is silent. -agent = "autoresearch" +agent = "research" card = "card.md" pack = "pack.toml" @@ -21,7 +21,7 @@ pack = "pack.toml" # seed-gated on Aaron's signature — see docs/seed-gate/role-cards-seed-diff.md. protocol_skills = [ "director-core", - "autoresearch", + "research", "hypothesis-review", "structural-analysis", ] @@ -45,7 +45,7 @@ path = "../../agents/analyzer.md" # team-vault copy on machines that sync it (honest degradation names it). # The handoff seed this mode RECEIVES: dev closes with a hypothesis seed and -# hands it to autoresearch (D6). +# hands it to research (D6). [[handoff_seeds]] kind = "hypothesis_seed" schema = "../../handoff-seeds/hypothesis-seed.schema.json" diff --git a/packages/extension/modes/research/pack.toml b/packages/extension/modes/research/pack.toml index 3d6bb44f..da10c4b3 100644 --- a/packages/extension/modes/research/pack.toml +++ b/packages/extension/modes/research/pack.toml @@ -73,4 +73,4 @@ roles = ["analyzer"] [[handoffs]] kind = "issue_seed" -target = "autodev" +target = "develop" diff --git a/packages/extension/opencode-plugin/mode_block.ts b/packages/extension/opencode-plugin/mode_block.ts index 8e11520a..c6f72296 100644 --- a/packages/extension/opencode-plugin/mode_block.ts +++ b/packages/extension/opencode-plugin/mode_block.ts @@ -92,6 +92,25 @@ import { unwrap } from "./session_spawn"; * test/mode_block.test.ts (drift fails the suite loudly). */ export const PLUGIN_SUPPORTED_MODE_BUNDLE_VERSION = "1"; +/** The mode-id read-resolve alias table (spec-20260907-011500 D1, #858) — + * autodev → develop, autoresearch → research. Duplicated from the schema + * package by the dependency-free contract; PARITY-PINNED by + * test/mode_block.test.ts. READ-RESOLVE, never migrate-on-write: old-id + * sessions (append-only artifacts) resolve at read time, onto the renamed + * bundle's declared agent — never onto a guess. `build` is NOT aliased: it + * exits the picker, not the vocabulary. The alias window's exit rides the + * next mode-bundle CONTRACT-VERSION bump (removal is non-additive). */ +export const PLUGIN_MODE_ID_ALIASES: Record = { + autodev: "develop", + autoresearch: "research", +}; + +/** Resolve an agent id through the read-resolve alias table (identity for + * everything the table does not name). */ +export function resolveModeIdPlugin(id: string): string { + return PLUGIN_MODE_ID_ALIASES[id] ?? id; +} + /** The explicit unresolvable line — byte-exact the spec's D4 text. */ export const UNRESOLVABLE_HEADLINE = "posture: unresolvable — re-bind from the ledger"; @@ -671,8 +690,10 @@ export async function buildModeBlock(deps: ModeBlockDeps): Promise b.agent === agent); + // agent id binds copilot and is SILENT. The resolved id goes through the + // read-resolve alias table first (#858): an old-id session (append-only + // artifact) binds the renamed posture at read time. + const bound = registry.bundles.find((b) => b.agent === resolveModeIdPlugin(agent)); if (bound === undefined) return null; // copilot posture — silent, whatever resolved the id return bound.missingParts.length === 0 ? fullBlock(bound, agent, resolvedVia) diff --git a/packages/extension/opencode-plugin/session_spawn.ts b/packages/extension/opencode-plugin/session_spawn.ts index 339251f9..6b989c6a 100644 --- a/packages/extension/opencode-plugin/session_spawn.ts +++ b/packages/extension/opencode-plugin/session_spawn.ts @@ -19,6 +19,23 @@ export const SPAWN_MAX_DEPTH = 2; export const SPAWN_MAX_COUNT = 4; +// The mode-id read-resolve alias table (spec-20260907-011500 D1, #858) — +// autodev → develop, autoresearch → research. NO-IMPORT contract: duplicated +// from @amicode/schema's MODE_ID_ALIASES, parity-pinned by +// test/session_spawn.test.ts. READ-RESOLVE, never migrate-on-write; `build` +// is NOT aliased (it exits the picker, not the vocabulary). The alias +// window's exit rides the next mode-bundle CONTRACT-VERSION bump. +const MODE_ID_ALIASES: Record = { + autodev: "develop", + autoresearch: "research", +}; + +/** Resolve an agent id through the read-resolve alias table (identity for + * everything the table does not name). */ +export function resolveModeIdSpawn(id: string): string { + return MODE_ID_ALIASES[id] ?? id; +} + export type SpawnMode = "fresh" | "fork"; export type SpawnArgs = { @@ -55,7 +72,21 @@ export function parseSpawnArgs(a: { } const agent = typeof a.agent === "string" && a.agent.trim() !== "" ? a.agent.trim() : null; const title = typeof a.title === "string" && a.title.trim() !== "" ? a.title.trim() : null; - return { ok: true, args: { prompt, count, title, agent, model, mode, force: a.force === true } }; + // the read-resolve alias (spec-20260907-011500 D1, #858): an old director + // id on the amico_session agent param binds the renamed card. READ-RESOLVE, + // never migrate-on-write; `build` and every non-aliased id pass through. + return { + ok: true, + args: { + prompt, + count, + title, + agent: agent === null ? null : resolveModeIdSpawn(agent), + model, + mode, + force: a.force === true, + }, + }; } // The calling session's own spawned_depth (absent for never-spawned sessions diff --git a/packages/extension/skills/autodev/SKILL.md b/packages/extension/skills/autodev/SKILL.md index b95dfddb..6489e9c9 100644 --- a/packages/extension/skills/autodev/SKILL.md +++ b/packages/extension/skills/autodev/SKILL.md @@ -1,23 +1,25 @@ --- name: autodev -description: The director's loop protocol for autonomous development sessions — the dev gate pack's phases and gates (decompose → implement → integrate), the implementer cast, session-ledger discipline, cross-mode handoff seeds, and honest degradation on machines missing bundle parts or skill copies. Use when starting, running, or resuming an autodev issue-DAG campaign. +description: The develop mode's director loop protocol — the dev gate pack's phases and gates (decompose → implement → integrate), the implementer cast, session-ledger discipline, cross-mode handoff seeds, and honest degradation on machines missing bundle parts or skill copies. Use when starting, running, or resuming a develop-mode issue-DAG campaign. (This skill's id keeps the pre-rename mode name `autodev` — the workflow skill owns `develop`; the read-resolve alias binds old references to the renamed mode.) agents: [implementer] surface: public source: amicode revision: 1 --- -# Autodev — the director's protocol +# Develop — the director's protocol > **Install conventions** — this skill is the dev mode's protocol, engine- and > install-neutral; bindings for a given engine stay engine-side (the opencode -> binding of the director role is the `autodev` primary agent card, and the +> binding of the director role is the `develop` primary agent card (the mode +> id renamed from `autodev` per the three-mode surface; old ids read-resolve +> to it), and the > engine-neutral loop core is the `director-core` skill — invoke it first at > kickoff or resume; this file binds the mode's specifics to it). -**Entry points:** the `autodev` agent card (Tab-switch into dev mode — its +**Entry points:** the `develop` agent card (Tab-switch into dev mode — its prompt embeds the director spine), direct invocation of this skill, or the -standing line in the user's autodev kickoff prompts. All three lead here; this +standing line in the user's develop-mode kickoff prompts. All three lead here; this file is the protocol. The operating principle: **the context window is a cache; the session ledger is @@ -63,7 +65,7 @@ current verdict table, reference the in-flight casts? Append the audit row to § ## The loop (one iteration) — bound to the dev gate pack -The mode's phase graph is the **dev gate pack** (`modes/autodev/pack.toml` in +The mode's phase graph is the **dev gate pack** (`modes/develop/pack.toml` in the amicode repo, schema'd data): phases **decompose → implement → integrate**, one gate set per phase. One loop: @@ -114,20 +116,20 @@ merges, board moves, issue closure) for orchestrated slices. ## Handoffs (cross-mode seeds) -The dev pack closes by handing a **hypothesis seed** to `autoresearch` -(`handoffs: hypothesis_seed → autoresearch`); this mode RECEIVES an **issue -seed** from `autoresearch`. The procedure, both directions: +The dev pack closes by handing a **hypothesis seed** to `research` +(`handoffs: hypothesis_seed → research`); this mode RECEIVES an **issue +seed** from `research`. The procedure, both directions: -- **Receiving (autoresearch → autodev):** the seed is a typed note +- **Receiving (research → develop):** the seed is a typed note (`kind: issue`, `issue-seed` schema — title, motivation, evidence, suggested repo + tier). On the seed: re-read the ledger, render the seed through `write-an-issue` at its suggested tier (the evidence pointers become Prior Art), and run the loop above on the resulting issue. -- **Emitting (autodev → autoresearch):** when a campaign closes with an open +- **Emitting (develop → research):** when a campaign closes with an open research question (a gate verdict that needs an experiment, a design question the issues surfaced), write the hypothesis-seed note (name the target posture, the question, the evidence), then hand it over — the - receiving mode's protocol (the `autoresearch` skill) picks it up from there. + receiving mode's protocol (the `research` skill) picks it up from there. - **Switching modes mid-session: PENDING-D5.** The posture switcher (the titlebar's mid-session agent switch) is not landed on every install yet — until the fork's posture surfaces ship, the safe path is to **spawn or open @@ -150,7 +152,7 @@ is present: note the gap in the ledger — do not fabricate the skills' procedures from memory when a step references one that is absent. - **Absent bundle parts** — if the mode card or the gate pack did not stage - (`modes/autodev/` missing or incomplete), the phase/gate summary above is + (`modes/develop/` missing or incomplete), the phase/gate summary above is the only copy you have; name the gap, and record it for the doctor to surface. - **Absent dispatch surface** — with no dispatchable implementer binding, diff --git a/packages/extension/skills/develop/SKILL.md b/packages/extension/skills/develop/SKILL.md index 336b03e1..40a5a451 100644 --- a/packages/extension/skills/develop/SKILL.md +++ b/packages/extension/skills/develop/SKILL.md @@ -19,7 +19,7 @@ Run one or more GitHub issues to completion. Each issue you pass is a **delivera This is the orchestration layer above the `/implement-issue` leaf: `develop` schedules and integrates; the leaf implements one slice via `tdd`. -**Mode binding:** this skill is the issue-DAG walk. The dev mode's *posture* binding — the loop protocol, the ledger discipline, the handoffs — is the **`autodev`** mode-protocol skill; defer to it for the mode, and to `director-core` for the shared spine. +**Mode binding:** this skill is the issue-DAG walk inside the **develop** mode (renamed from `autodev`; old ids read-resolve). The dev mode's *posture* binding — the loop protocol, the ledger discipline, the handoffs — is the **`autodev`** mode-protocol skill (its id keeps the pre-rename mode name; the workflow skill owns `develop`); defer to it for the mode, and to `director-core` for the shared spine. **Announce at start:** "I'm using the develop skill to implement #\ … via Amico." @@ -98,7 +98,7 @@ Engine-specific bindings stay engine-side; this skill names the role. A session ## Related skills -- **autodev** — the dev mode's protocol binding; this skill is the walk that runs inside it. +- **autodev** — the develop mode's protocol binding; this skill is the walk that runs inside it. - **implement-issue** — the leaf this skill dispatches per slice (`--orchestrated`). - **write-an-issue** / **break-into-subissues** — produce the issues this skill consumes. - **tdd** — the RED→GREEN loop the leaf runs inside each slice. diff --git a/packages/extension/skills/director-core/SKILL.md b/packages/extension/skills/director-core/SKILL.md index ec607e9b..a8634bcf 100644 --- a/packages/extension/skills/director-core/SKILL.md +++ b/packages/extension/skills/director-core/SKILL.md @@ -1,6 +1,6 @@ --- name: director-core -description: The canonical director-core protocol — the one loop every autonomous campaign runs (plan → dispatch through gates → analyze → record), the session-ledger discovery rule both mode cards quote verbatim, the four core clauses (ledger discipline, cast pattern, compaction honesty, anti-gaming), and the copilot/autoresearch/autodev posture model. Use when authoring or binding a mode card, a gate pack, or a campaign layer that consumes them. +description: The canonical director-core protocol — the one loop every autonomous campaign runs (plan → dispatch through gates → analyze → record), the session-ledger discovery rule both mode cards quote verbatim, the four core clauses (ledger discipline, cast pattern, compaction honesty, anti-gaming), and the copilot/research/develop posture model (modes renamed from autoresearch/autodev — old ids read-resolve, spec-20260907-011500 D1). Use when authoring or binding a mode card, a gate pack, or a campaign layer that consumes them. agents: [orchestrator] surface: public source: amicode @@ -11,9 +11,9 @@ revision: 1 One director, one loop, every campaign. This skill is the engine-neutral core that mode cards bind; engine mechanics live in the cards, never here. It -extends the autoresearch protocol's spine — ledger discipline, cast pattern, +extends the research protocol's spine — ledger discipline, cast pattern, compaction honesty, anti-gaming — with the loop abstraction that makes that -spine mode-general. The autoresearch skill remains the research-mode +spine mode-general. The research skill remains the research-mode instantiation; this file is the shared spine both modes embed, not a copy of either protocol. @@ -74,8 +74,8 @@ A **mode** is a posture of the one director, never a new session: | Posture | Gate pack | Shape | | --- | --- | --- | | **copilot** — the zeroth | none (packless) | interactive: answers, designs, runs what the human asks — no autonomous loop, no campaign, no session ledger | -| **autoresearch** | the research pack | hypothesis queue → deliberate spec → experiment → gates → analyzer | -| **autodev** | the dev pack | issue DAG → TDD slices → CI/review → landed delta | +| **research** | the research pack | hypothesis queue → deliberate spec → experiment → gates → analyzer | +| **develop** | the dev pack | issue DAG → TDD slices → CI/review → landed delta | A mode switch re-binds the posture and re-reads the ledger (the discovery rule above); the session ledger survives every switch. A mode may still @@ -142,7 +142,7 @@ human-only, always. Engine bindings (agent cards, dispatch mechanics, permission tooling) live in the mode cards, engine-side. The research loop's specifics — the spec gate's review budget, the checkout registry, the probe/experiment boundary — -stay with the autoresearch skill; the dev walk's — branch and draft-PR +stay with the research skill; the dev walk's — branch and draft-PR lifecycle, worktree binding — stay with the develop and implement-issue skills. This file carries only what every campaign shares: the loop, the ledger, the cast, the clauses, the postures. diff --git a/packages/extension/skills/migrate-research-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md index 65c65a1f..a586785c 100644 --- a/packages/extension/skills/migrate-research-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -43,7 +43,7 @@ plan, and executing approved moves. No CLI middleman for the interactive path. hypotheses/ # open questions, future directions observations/ # experiment records, results summaries, methodology notes literature/ # reading notes - campaigns/ # autoresearch campaign ledgers + campaigns/ # research campaign ledgers reports/ weekly/ # weekly updates (template.md provided) presentations/ # slide decks @@ -196,7 +196,7 @@ Format: | 10 | `Project.toml` | `Project.toml` | stays | Julia package manifest | | — | `paper/outline.md` | — | scaffold | template with inferred question | | — | `paper/main.tex` | — | scaffold | minimal article template | -| — | `ledger/campaigns/` | — | scaffold | empty, for autoresearch | +| — | `ledger/campaigns/` | — | scaffold | empty, for research | | — | `reports/weekly/template.md` | — | scaffold | weekly update template | | — | `config/system.toml` | — | scaffold | stub | | — | `research-project.toml` | — | scaffold | manifest | diff --git a/packages/extension/skills/research/SKILL.md b/packages/extension/skills/research/SKILL.md index 5f0f6052..4f8212e5 100644 --- a/packages/extension/skills/research/SKILL.md +++ b/packages/extension/skills/research/SKILL.md @@ -1,24 +1,24 @@ --- -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. +name: research +description: The research mode's director loop protocol — session-ledger discipline, the hypothesizer/experimenter/analyzer trio, deliberate spec gates, checkout registry, and compaction-any-time safety. Use when starting, running, or resuming a research loop. (The mode and this skill renamed from `autoresearch` per the three-mode surface; old ids read-resolve to `research`.) agents: [hypothesizer, experimenter, analyzer] surface: public project_contract: folders: [ledger/hypotheses, ledger/observations, ledger/campaigns, scripts, data, reports, config] --- -# Autoresearch — the director's protocol +# Research — the director's protocol > **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 +> engine-side (the opencode binding of the director role is the `research` primary > agent card). -**Entry points:** the `autoresearch` primary agent (Tab-switch into research mode — +**Entry points:** the `research` 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 user's research kickoff prompts. All three lead here; this file is the protocol. 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, @@ -122,19 +122,19 @@ ledger-abstinence is discipline + git history. The parent is the SOLE ledger wri ## Handoffs (cross-mode seeds) -The research pack closes by handing an **issue seed** to `autodev` (its pack's -closing handoff); this mode RECEIVES a **hypothesis seed** from `autodev`. +The research pack closes by handing an **issue seed** to `develop` (its pack's +closing handoff); this mode RECEIVES a **hypothesis seed** from `develop`. The procedure, both directions: -- **Receiving (autodev → autoresearch):** the seed is a typed note +- **Receiving (develop → research):** the seed is a typed note (`kind: hypothesis`, the hypothesis-seed schema — the question, the evidence, the suggested experiment shape). On the seed: re-read the ledger, register the hypothesis in the H-queue (§2), and run the loop above on it. -- **Emitting (autoresearch → autodev):** when a campaign closes with a +- **Emitting (research → develop):** when a campaign closes with a finding that needs code (a validated method, a tool gap, a result that wants an implementation), write the issue-seed note — title, motivation, evidence pointers, suggested repo + tier — name the target posture - (autodev), and hand it over; the receiving mode's protocol (the `autodev` + (develop), and hand it over; the receiving mode's protocol (the `develop` skill) renders it through `write-an-issue` and runs the dev walk on it. - **Switching modes mid-session: PENDING-D5.** Until the fork's posture surfaces ship, the safe path is to spawn or open the target posture's diff --git a/packages/extension/src/mode_cards.ts b/packages/extension/src/mode_cards.ts index 54c6b179..58cd14ba 100644 --- a/packages/extension/src/mode_cards.ts +++ b/packages/extension/src/mode_cards.ts @@ -347,7 +347,7 @@ export function listModeCardFiles(extensionPath: string): string[] { } catch { throw new Error( `no mode cards found in ${srcDir} — the extension bundle must ship ` + - `autodev.md, autoresearch.md, and the worker cards ` + + `develop.md, research.md, and the worker cards ` + `(packaging dropped the agents dir: .vscodeignore?)`, ); } @@ -355,7 +355,7 @@ export function listModeCardFiles(extensionPath: string): string[] { if (cards.length === 0) { throw new Error( `no mode cards found in ${srcDir} — the extension bundle must ship ` + - `autodev.md, autoresearch.md, and the worker cards ` + + `develop.md, research.md, and the worker cards ` + `(packaging dropped the agents dir: .vscodeignore?)`, ); } diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 02a1baf4..0affc6e2 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -111,12 +111,14 @@ export function resolveJuliaProject(configValue: string): string { * MCP projection against. The MCP environment carries AMICODE_PROBLEMS_DIR * so the server resolves the same workspace root the grants use. * - `default_agent: "plan"` — plan-first posture: new sessions open on - * opencode's plan agent; the ordered picker is plan → build → autodev → - * autoresearch (opencode's Agent.list keeps the default first, then - * alphabetical among the rest — so plan first, build next after the - * autodev/autoresearch custom sort fix, else plan, autodev, - * autoresearch, build). The picker is plan/build + the two director - * modes (roles-not-modes, #368): + * opencode's plan agent; the named modes are plan → develop → research + * (the three-mode surface, spec-20260907-011500 D1: autodev → develop, + * autoresearch → research, old ids read-resolve for one release cycle; + * stock `build` is the implied auto — the underlying default agent, + * still a valid explicit id, out of the picker's named set). + * `agent_order` (fork PR #305's field, honored APP-SIDE by the overlay's + * picker sort) pins the fixed display order; without an honoring engine + * build the picker still reads it client-side. Roles-not-modes (#368): * the interview content lives in the compiled AGENTS.md score section * (visible to every agent), and the pulse-designer agent entry is RETIRED * (#389) — it was a four-line prompt shell over the config-root permission @@ -481,14 +483,18 @@ export function buildOpencodeConfigContent( return JSON.stringify({ $schema: "https://opencode.ai/config.json", // Plan-first posture (product default for ALL users): every new Amicode - // session opens on opencode's `plan` agent. The desired picker order is - // plan → build → autodev → autoresearch (Agent.list keeps the default - // first, then alphabetical — so plan first; the exact - // plan/build/autodev/autoresearch sequence requires the custom Agent.list - // sort patch when that ordering is required). Like everything in this blob, - // it deep-merges OVER the user's global config — an explicit per-message - // `agent` (the e2e tests, the distiller's --agent) is unaffected. + // session opens on opencode's `plan` agent. The named modes are + // plan → develop → research (spec-20260907-011500 D1, #858: autodev → + // develop, autoresearch → research; stock `build` is the implied auto — + // the underlying default agent, a valid explicit id, out of the named + // set). `agent_order` (fork PR #305's config field) is the fixed display + // order: PRIMARY sort key in the app-side picker sort (unlisted agents + // follow, default_agent pin secondary, alphabetical last). Like + // everything in this blob, it deep-merges OVER the user's global config — + // an explicit per-message `agent` (the e2e tests, the distiller's --agent) + // is unaffected. default_agent: "plan", + agent_order: ["plan", "develop", "research"], ...(modelPin ? { model: modelPin } : {}), instructions: [agentsPath], // #700 A3: the amicode_* tool plugin is RETIRED — the tools come from the @@ -522,7 +528,7 @@ export function buildOpencodeConfigContent( // into cfg.experimental alongside any user keys (see telemetryOpen above). ...(telemetryOpen ? { experimental: { openTelemetry: true } } : {}), // No `agent` overrides: the picker is opencode's native plan/build plus - // the two director modes autodev/autoresearch (#389 — the pulse-designer + // the two director modes develop/research (#389 — the pulse-designer // agent entry is retired; its prompt was a shell deferring to the // compiled AGENTS.md interview section, and its permission grants were // always the config-root block above). The interview runs from ANY agent @@ -729,7 +735,7 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro const preamble = [ "", "", - "You are a general-purpose autoresearch copilot. Do NOT proactively start", + "You are a general-purpose research 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.", diff --git a/packages/extension/src/scores/router.ts b/packages/extension/src/scores/router.ts index 0658d728..42a89114 100644 --- a/packages/extension/src/scores/router.ts +++ b/packages/extension/src/scores/router.ts @@ -28,7 +28,7 @@ export function buildRouterSection(visible: Score[]): string { "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.", + "- **Resume your research campaign** — ONLY when a session ledger exists under the personal vault's `sessions/`; the research director re-reads the latest ledger and continues the loop.", "- **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 workflow.", "- **Just explore** — free-form; no rail.", diff --git a/packages/extension/test/fixtures/doctor/doctor-current.json b/packages/extension/test/fixtures/doctor/doctor-current.json index c997ba89..b96f1c5e 100644 --- a/packages/extension/test/fixtures/doctor/doctor-current.json +++ b/packages/extension/test/fixtures/doctor/doctor-current.json @@ -55,19 +55,19 @@ ], "components": [ { - "mode": "autodev", + "mode": "develop", "component": "card.md", "verdict": "current", "evidence": [ - "component autodev/card.md byte-matches release v0.3.2" + "component develop/card.md byte-matches release v0.3.2" ] }, { - "mode": "autodev", + "mode": "develop", "component": "pack.toml", "verdict": "current", "evidence": [ - "component autodev/pack.toml byte-matches release v0.3.2" + "component develop/pack.toml byte-matches release v0.3.2" ] }, { @@ -92,19 +92,19 @@ ], "components": [ { - "mode": "autodev", + "mode": "develop", "component": "card.md", "verdict": "current", "evidence": [ - "component autodev/card.md byte-matches release v0.3.2" + "component develop/card.md byte-matches release v0.3.2" ] }, { - "mode": "autodev", + "mode": "develop", "component": "pack.toml", "verdict": "current", "evidence": [ - "component autodev/pack.toml byte-matches release v0.3.2" + "component develop/pack.toml byte-matches release v0.3.2" ] }, { diff --git a/packages/extension/test/gate_packs.test.ts b/packages/extension/test/gate_packs.test.ts index 1ca70360..ae701067 100644 --- a/packages/extension/test/gate_packs.test.ts +++ b/packages/extension/test/gate_packs.test.ts @@ -11,8 +11,8 @@ import { validateGatePack } from "@amicode/schema"; // pack schema, forward + reverse mapping completeness, non-triviality // floors, distinctness, and faithfulness of the extraction. // -// #804 re-home: the packs live in their mode bundles — modes/autodev/pack.toml -// and modes/autoresearch/pack.toml — same tests, new home. The structural +// #804 re-home: the packs live in their mode bundles — modes/develop/pack.toml +// and modes/research/pack.toml — same tests, new home. The structural // schema checks now run through the shared validator (validateGatePack), the // same code the amico-run doctor probe imports; the fixture-content floors // stay here. @@ -24,7 +24,7 @@ const PACK_DIR = MODES_DIR; // each bundle's pack.toml — the registry layout const GATE_KINDS = ["mechanical", "human", "derived"] as const; const HANDOFF_KINDS = ["issue_seed", "hypothesis_seed"] as const; -const HANDOFF_TARGETS = ["autoresearch", "autodev"] as const; +const HANDOFF_TARGETS = ["research", "develop"] as const; interface Gate { name: string; @@ -54,8 +54,8 @@ function loadPack(file: string): Pack { return parse(fs.readFileSync(path.join(PACK_DIR, file), "utf8")) as unknown as Pack; } -const research = loadPack(path.join("autoresearch", "pack.toml")); -const dev = loadPack(path.join("autodev", "pack.toml")); +const research = loadPack(path.join("research", "pack.toml")); +const dev = loadPack(path.join("develop", "pack.toml")); const PACKS: Array<[string, Pack]> = [ ["research", research], ["dev", dev], @@ -95,15 +95,15 @@ function gateByName(pack: Pack, name: string): Gate { describe("gate-pack fixtures of record", () => { it("the packs live inside their mode bundles (the registry re-home, #804) — no stray gate-packs dir", () => { // the registry layout: one bundle per director mode, pack.toml inside - expect(fs.readdirSync(MODES_DIR).sort()).toEqual(["autodev", "autoresearch", "release-index.toml"]); + expect(fs.readdirSync(MODES_DIR).sort()).toEqual(["develop", "release-index.toml", "research"]); expect(fs.existsSync(path.join(EXT, "gate-packs"))).toBe(false); - for (const [name, mode] of [["dev", "autodev"], ["research", "autoresearch"]] as const) { + for (const [name, mode] of [["dev", "develop"], ["research", "research"]] as const) { expect(fs.existsSync(path.join(MODES_DIR, mode, "pack.toml")), `${name} pack in its bundle`).toBe(true); } }); it("both packs pass the SHARED validator's gate-pack schema (one code path with the doctor)", () => { - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { const v = validateGatePack(fs.readFileSync(path.join(MODES_DIR, mode, "pack.toml"), "utf8")); expect(v.errors, `${mode}: ${v.errors.join("; ")}`).toEqual([]); expect(v.ok).toBe(true); @@ -309,7 +309,7 @@ describe("research pack extraction (faithful to the director research protocol)" expect((research.handoffs ?? []).length).toBeGreaterThanOrEqual(1); for (const handoff of research.handoffs ?? []) { expect(handoff.kind).toBe("issue_seed"); - expect(handoff.target).toBe("autodev"); + expect(handoff.target).toBe("develop"); } }); }); @@ -361,7 +361,7 @@ describe("dev pack extraction (faithful to the issue-DAG walk)", () => { expect((dev.handoffs ?? []).length).toBeGreaterThanOrEqual(1); for (const handoff of dev.handoffs ?? []) { expect(handoff.kind).toBe("hypothesis_seed"); - expect(handoff.target).toBe("autoresearch"); + expect(handoff.target).toBe("research"); } }); }); diff --git a/packages/extension/test/mode_block.test.ts b/packages/extension/test/mode_block.test.ts index e5ee902f..1e1af428 100644 --- a/packages/extension/test/mode_block.test.ts +++ b/packages/extension/test/mode_block.test.ts @@ -94,7 +94,7 @@ function readProbeLines(out: string): ProbeLine[] { // // Boots the pinned binary with OPENCODE_CONFIG_CONTENT registering the probe // plugin (the same registration mechanism the extension uses for -// amicode_context.ts), a config-declared `autodev` director agent, and a +// amicode_context.ts), a config-declared `develop` director agent, and a // fixture provider whose baseURL is a DEAD local port: model RESOLUTION is // offline (config models are metadata), the transform hook fires in // LLMRequestPrep.prepare BEFORE the provider SDK loads and before the network @@ -126,7 +126,7 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability $schema: "https://opencode.ai/config.json", plugin: [PROBE_PLUGIN, CONTEXT_PLUGIN], agent: { - autodev: { + develop: { description: "the fixture director agent", prompt: "You are the fixture director. This turn's answer never matters.", }, @@ -234,7 +234,7 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability const created = await fetch(`http://127.0.0.1:${port}/session`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ agent: "autodev" }), + body: JSON.stringify({ agent: "develop" }), // (#830) the FIRST session spins up the instance lazily — the // plugin factory + the cold-HOME package install happen inside // this POST on a fresh runner; 10s aborted it on CI. 60s is the @@ -246,7 +246,7 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability expect(typeof session.id).toBe("string"); sessionID = session.id!; // the wire contract's first leg: the session's own agent round-trips - expect(session.agent, "GET /session response did not carry the created agent on the wire").toBe("autodev"); + expect(session.agent, "GET /session response did not carry the created agent on the wire").toBe("develop"); // (3) the probe plugin LOADED with the session's instance and its // factory got the engine client (the same PluginInput handoff @@ -259,7 +259,7 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability expect(factory.has_messages, "the engine client has no session.messages callable (the A1 leg's transport)").toBe(true); expect(factory.has_directory, "the plugin factory input did not carry the directory").toBe(true); - // (4) prompt the session — the user message lands with agent=autodev, + // (4) prompt the session — the user message lands with agent=develop, // the LLM request prepares, the transform fires, the probe // resolves session.get from INSIDE the hook. The turn itself then // dies on the dead provider port (connection refused, retried) — @@ -273,7 +273,7 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parts: [{ type: "text", text: "resolve the posture" }], - agent: "autodev", + agent: "develop", model: { providerID: "fixtureprov", modelID: "fixturemodel" }, }), }); @@ -293,7 +293,7 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability expect( resolution.agent, "session.get returned but Session.Info.agent is absent — the primary path is insufficient", - ).toBe("autodev"); + ).toBe("develop"); // (6) (A1, PR #814 review fold) PIN THE MESSAGES-ORDERING FACT LIVE. // fallbackResolve picks the LAST assistant message by ARRAY ORDER @@ -312,7 +312,7 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parts: [{ type: "text", text: "resolve the posture, again" }], - agent: "autodev", + agent: "develop", model: { providerID: "fixtureprov", modelID: "fixturemodel" }, }), }); @@ -350,7 +350,7 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability expect( mAgents, "session.messages did not carry the per-message agent on the wire — the fallback's read data is absent", - ).toEqual(mAgents.map(() => "autodev")); + ).toEqual(mAgents.map(() => "develop")); // (7) the PRODUCT context plugin LOADS cleanly under the pinned // binary's Bun runtime (a broken sibling import or a bad @@ -389,13 +389,13 @@ describe.skipIf(!existsSync(OC_BIN))("H4 FIRST — the session-API availability factory_has_client: factoryRec?.has_client === true, hook_fires_with_sessionID: typeof transformRec?.sessionID === "string" && transformRec.sessionID === sessionID, session_get_resolves_inside_hook: resolveRec?.ok === true, - info_agent_round_trips: resolveRec?.agent === "autodev", + info_agent_round_trips: resolveRec?.agent === "develop", messages_endpoint_ascending: idsAscending, per_message_agent_on_the_wire: lastMessages !== undefined && Array.isArray(lastMessages.agents) && (lastMessages.agents as Array).length > 0 && - (lastMessages.agents as Array).every((a) => a === "autodev"), + (lastMessages.agents as Array).every((a) => a === "develop"), }; const outcome = { fixture: "session-api-availability", @@ -458,6 +458,7 @@ async function waitFor(out: string, pred: (l: ProbeLine) => boolean, ms: number) import { buildModeBlock, PLUGIN_SUPPORTED_MODE_BUNDLE_VERSION, + PLUGIN_MODE_ID_ALIASES, compareModeVersionsPlugin, UNRESOLVABLE_HEADLINE, } from "../opencode-plugin/mode_block"; @@ -466,6 +467,7 @@ import { checkConsumerFloor, compareModeVersions, SUPPORTED_MODE_BUNDLE_VERSION, + MODE_ID_ALIASES, } from "@amicode/schema"; const UNRESOLVABLE_LINE = "posture: unresolvable — re-bind from the ledger"; @@ -488,7 +490,7 @@ function stagedRegistry( } = {}, ): string { const modesRoot = mkdtempSync(join(tmpdir(), "mode-block-reg-")); - const modes = opts.modes ?? [{ mode: "autodev" }, { mode: "autoresearch" }]; + const modes = opts.modes ?? [{ mode: "develop" }, { mode: "research" }]; for (const m of modes) { const dir = join(modesRoot, m.mode); mkdirSync(dir, { recursive: true }); @@ -525,7 +527,7 @@ function stagedRegistry( if (!m.omitPack) { writeFileSync( join(dir, "pack.toml"), - m.mode === "autoresearch" + m.mode === "research" ? [ 'closing_artifact = "validated-findings record"', "", @@ -549,7 +551,7 @@ function stagedRegistry( "", "[[handoffs]]", 'kind = "issue_seed"', - 'target = "autodev"', + 'target = "develop"', ].join("\n") + "\n" : [ 'closing_artifact = "landed-delta record"', @@ -575,7 +577,7 @@ function stagedRegistry( "", "[[handoffs]]", 'kind = "hypothesis_seed"', - 'target = "autoresearch"', + 'target = "research"', ].join("\n") + "\n", ); } @@ -641,12 +643,12 @@ const deps = (registryRoot: string, client: ReturnType["clien describe("H4 — the posture-binding map (D4: a MAP, not a heuristic)", () => { it("a director-agent session emits the stamped `## Active mode` block (primary resolution)", async () => { const reg = stagedRegistry(); - const { client, calls } = fakeClient({ agent: "autodev" }); + const { client, calls } = fakeClient({ agent: "develop" }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); expect(block!.startsWith("## Active mode")).toBe(true); // posture name - expect(block!).toContain("posture: `autodev`"); + expect(block!).toContain("posture: `develop`"); // the phase/gate summary read from the registry bundle expect(block!).toContain("decompose"); expect(block!).toContain("dev-gate"); @@ -654,8 +656,8 @@ describe("H4 — the posture-binding map (D4: a MAP, not a heuristic)", () => { // the ledger path convention read from the bundle card's generated region expect(block!).toContain("sessions/session--.md"); // the stamp: resolved agent id + registry digest - expect(block!).toContain("agent=autodev"); - expect(block!).toContain("mode=autodev"); + expect(block!).toContain("agent=develop"); + expect(block!).toContain("mode=develop"); expect(block!).toContain("resolved=primary"); expect(/\bregistry-digest=sha256:[0-9a-f]{64}\b/.test(block!)).toBe(true); expect(calls.get).toBe(1); // primary path — one session.get, no messages read @@ -665,10 +667,10 @@ describe("H4 — the posture-binding map (D4: a MAP, not a heuristic)", () => { it("the block reads the REAL shipped registry bundles correctly (no drift between plugin reader and validator data)", async () => { // the real modes/ dir, the way the extension stages it — the block's // summary must match the real packs the shared validator enforces. - const { client } = fakeClient({ agent: "autodev" }); + const { client } = fakeClient({ agent: "develop" }); const block = await buildModeBlock({ sessionID: "ses_x", engineClient: client, registryRoot: join(EXT, "modes") }); expect(block).not.toBeNull(); - expect(block!).toContain("posture: `autodev`"); + expect(block!).toContain("posture: `develop`"); expect(block!).toContain("dev-gate"); expect(block!).toContain("blocked-by-clearance"); expect(block!).toContain("tdd-red-green"); @@ -676,10 +678,10 @@ describe("H4 — the posture-binding map (D4: a MAP, not a heuristic)", () => { expect(block!).toContain("review"); // the real card's generated region (the ledger discovery rule) is spliced expect(block!).toContain("LEDGER DISCOVERY RULE v1"); - // and the autoresearch bundle binds its own agent - const { client: c2 } = fakeClient({ agent: "autoresearch" }); + // and the research bundle binds its own agent + const { client: c2 } = fakeClient({ agent: "research" }); const b2 = await buildModeBlock({ sessionID: "ses_y", engineClient: c2, registryRoot: join(EXT, "modes") }); - expect(b2).toContain("posture: `autoresearch`"); + expect(b2).toContain("posture: `research`"); }); it.each(["plan", "build", "implementer", "hypothesizer", "reviewer"])( @@ -709,24 +711,79 @@ describe("H4 — the posture-binding map (D4: a MAP, not a heuristic)", () => { it("a machine with NO staged registry (pre-registry build) emits nothing — silent is honest, the doctor owns the staleness verdict", async () => { const reg = join(mkdtempSync(join(tmpdir(), "mode-block-noreg-")), "modes"); // never created - const { client } = fakeClient({ agent: "autodev" }); + const { client } = fakeClient({ agent: "develop" }); expect(await buildModeBlock(deps(reg, client))).toBeNull(); }); it("a bundle whose manifest is missing/unparseable is SKIPPED (mid-staging or corrupt — the doctor names it), and its agent does NOT bind by name", async () => { const reg = stagedRegistry({ modes: [ - { mode: "autodev", omitManifest: true }, - { mode: "autoresearch" }, + { mode: "develop", omitManifest: true }, + { mode: "research" }, ], }); - // autodev's bundle is unreadable: no binding exists → copilot-silent (the + // develop's bundle is unreadable: no binding exists → copilot-silent (the // doctor's verdict owns the corruption; the plugin never guesses) - const { client } = fakeClient({ agent: "autodev" }); + const { client } = fakeClient({ agent: "develop" }); expect(await buildModeBlock(deps(reg, client))).toBeNull(); // the OTHER bundle still serves its sessions normally - const { client: c2 } = fakeClient({ agent: "autoresearch" }); - expect((await buildModeBlock(deps(reg, c2)))!).toContain("posture: `autoresearch`"); + const { client: c2 } = fakeClient({ agent: "research" }); + expect((await buildModeBlock(deps(reg, c2)))!).toContain("posture: `research`"); + }); +}); + +describe("the read-resolve alias — old-id sessions bind the renamed posture (#858, spec-20260907-011500 D1)", () => { + // Append-only artifacts (old sessions) legitimately carry the pre-rename + // agent ids forever; resolution happens at READ time, never by rewriting + // the artifact. The binding map lookup resolves the session's agent id + // through the alias table before matching. + it("a session whose agent is the old id `autodev` emits the `develop` posture block", async () => { + const reg = stagedRegistry({ modes: [{ mode: "develop" }, { mode: "research" }] }); + const { client } = fakeClient({ agent: "autodev" }); + const block = await buildModeBlock(deps(reg, client)); + expect(block).not.toBeNull(); + expect(block!).toContain("posture: `develop`"); + // the stamp names the session's ACTUAL agent id and the resolved mode — + // the artifact is never rewritten, the posture is never guessed + expect(block!).toContain("agent=autodev"); + expect(block!).toContain("mode=develop"); + }); + + it("a session whose agent is the old id `autoresearch` emits the `research` posture block", async () => { + const reg = stagedRegistry({ modes: [{ mode: "develop" }, { mode: "research" }] }); + const { client } = fakeClient({ agent: "autoresearch" }); + const block = await buildModeBlock(deps(reg, client)); + expect(block).not.toBeNull(); + expect(block!).toContain("posture: `research`"); + expect(block!).toContain("agent=autoresearch"); + expect(block!).toContain("mode=research"); + }); + + it("the fallback path resolves the alias too (an old-id assistant message binds the renamed posture)", async () => { + const reg = stagedRegistry({ modes: [{ mode: "develop" }, { mode: "research" }] }); + const { client } = fakeClient({ + getThrows: true, + messages: [ + { id: "msg_001", role: "user", agent: "autoresearch" }, + { id: "msg_002", role: "assistant", agent: "autoresearch" }, + ], + }); + const block = await buildModeBlock(deps(reg, client)); + expect(block).not.toBeNull(); + expect(block!).toContain("posture: `research`"); + expect(block!).toContain("resolved=fallback"); + }); + + it("`build` is not aliased — an explicit build session stays copilot-silent (it exits the picker, not the vocabulary)", async () => { + const reg = stagedRegistry({ modes: [{ mode: "develop" }, { mode: "research" }] }); + const { client } = fakeClient({ agent: "build" }); + expect(await buildModeBlock(deps(reg, client))).toBeNull(); + }); + + it("an old id with NO renamed bundle staged stays copilot-silent (an alias resolves onto a real bundle, never into a guess)", async () => { + const reg = stagedRegistry({ modes: [{ mode: "research" }] }); + const { client } = fakeClient({ agent: "autodev" }); // develop's bundle is not staged + expect(await buildModeBlock(deps(reg, client))).toBeNull(); }); }); @@ -735,10 +792,10 @@ describe("H4 — compaction survival (the block is per-request, from session sta const reg = stagedRegistry(); // history DROPPED: no messages at all — the block must come from session // state alone (session.get), exactly the post-compaction shape. - const { client, calls } = fakeClient({ agent: "autodev", messages: null }); + const { client, calls } = fakeClient({ agent: "develop", messages: null }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); - expect(block!).toContain("posture: `autodev`"); + expect(block!).toContain("posture: `develop`"); expect(calls.get).toBe(1); expect(calls.messages).toBe(0); // the history was never consulted }); @@ -772,21 +829,21 @@ describe("H4 — the unresolvable line (staged bundle, no resolution)", () => { describe("H4 — honest degradation (resolved director posture, missing bundle parts)", () => { it("a missing pack.toml emits the block with a degraded line NAMING it (no phase/gate summary fabricated)", async () => { - const reg = stagedRegistry({ modes: [{ mode: "autodev", omitPack: true }] }); - const { client } = fakeClient({ agent: "autodev" }); + const reg = stagedRegistry({ modes: [{ mode: "develop", omitPack: true }] }); + const { client } = fakeClient({ agent: "develop" }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); - expect(block!).toContain("posture: `autodev`"); + expect(block!).toContain("posture: `develop`"); expect(block!).toContain("DEGRADED"); expect(block!).toContain("pack.toml"); // the missing part is NAMED expect(block!).not.toContain("dev-gate"); // nothing fabricated expect(block!).toContain("LEDGER DISCOVERY RULE v1"); // the readable parts still bind - expect(block!).toContain("agent=autodev"); + expect(block!).toContain("agent=develop"); }); it("a missing card.md names it and omits the ledger rule section", async () => { - const reg = stagedRegistry({ modes: [{ mode: "autodev", omitCard: true }] }); - const { client } = fakeClient({ agent: "autodev" }); + const reg = stagedRegistry({ modes: [{ mode: "develop", omitCard: true }] }); + const { client } = fakeClient({ agent: "develop" }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); expect(block!).toContain("DEGRADED"); @@ -795,8 +852,8 @@ describe("H4 — honest degradation (resolved director posture, missing bundle p }); it("a card present but missing its generated region names the region", async () => { - const reg = stagedRegistry({ modes: [{ mode: "autodev", omitRegion: true }] }); - const { client } = fakeClient({ agent: "autodev" }); + const reg = stagedRegistry({ modes: [{ mode: "develop", omitRegion: true }] }); + const { client } = fakeClient({ agent: "develop" }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); expect(block!).toContain("DEGRADED"); @@ -806,8 +863,8 @@ describe("H4 — honest degradation (resolved director posture, missing bundle p describe("H4 — the plugin version gap (the loud failure, never silence)", () => { it("a bundle floor above the plugin's supported version emits the unresolvable block with the gap render (byte-parity with checkConsumerFloor)", async () => { - const reg = stagedRegistry({ modes: [{ mode: "autodev", pluginFloor: "2" }] }); - const { client } = fakeClient({ agent: "autodev" }); + const reg = stagedRegistry({ modes: [{ mode: "develop", pluginFloor: "2" }] }); + const { client } = fakeClient({ agent: "develop" }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); expect(block!.startsWith("## Active mode")).toBe(true); @@ -816,11 +873,11 @@ describe("H4 — the plugin version gap (the loud failure, never silence)", () = const gap = checkConsumerFloor({ doctor: "1", plugin: "2", stager: "1", tests: "1" }, "plugin", "1"); expect(gap.ok).toBe(false); if (!gap.ok) expect(block!).toContain(gap.render); - expect(block!).not.toContain("posture: `autodev`"); // never a guessed posture over an untrustable registry + expect(block!).not.toContain("posture: `develop`"); // never a guessed posture over an untrustable registry }); it("the version gap is loud even for a session that would otherwise bind copilot — the map itself is untrustable", async () => { - const reg = stagedRegistry({ modes: [{ mode: "autodev", pluginFloor: "2" }] }); + const reg = stagedRegistry({ modes: [{ mode: "develop", pluginFloor: "2" }] }); const { client } = fakeClient({ agent: "plan" }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); // NEVER silence on a version gap @@ -834,36 +891,36 @@ describe("H4 — the last-assistant-message fallback (guarded, decline-correct)" const { client, calls } = fakeClient({ getThrows: true, messages: [ - { id: "msg_001", role: "user", agent: "autoresearch" }, - { id: "msg_002", role: "assistant", agent: "autoresearch" }, - { id: "msg_003", role: "user", agent: "autoresearch" }, // same agent — no switch + { id: "msg_001", role: "user", agent: "research" }, + { id: "msg_002", role: "assistant", agent: "research" }, + { id: "msg_003", role: "user", agent: "research" }, // same agent — no switch ], }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); - expect(block!).toContain("posture: `autoresearch`"); + expect(block!).toContain("posture: `research`"); expect(block!).toContain("resolved=fallback"); expect(calls.messages).toBe(1); }); it("DECLINES when the agent-switched stream shows a switch newer on the monotonic key — the unresolvable line, never a wrong posture", async () => { const reg = stagedRegistry(); - // the last assistant message ran autoresearch; the user then SWITCHED to - // autodev (a newer user message carries the new agent). The fallback would + // the last assistant message ran research; the user then SWITCHED to + // develop (a newer user message carries the new agent). The fallback would // read msg_002's stale agent — it declines instead. const { client } = fakeClient({ getThrows: true, messages: [ - { id: "msg_001", role: "user", agent: "autoresearch" }, - { id: "msg_002", role: "assistant", agent: "autoresearch" }, - { id: "msg_003", role: "user", agent: "autodev" }, // the switch — NEWER on the message id (the monotonic key, same store) + { id: "msg_001", role: "user", agent: "research" }, + { id: "msg_002", role: "assistant", agent: "research" }, + { id: "msg_003", role: "user", agent: "develop" }, // the switch — NEWER on the message id (the monotonic key, same store) ], }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); expect(block!).toContain(UNRESOLVABLE_LINE); - // never a wrong posture: no autoresearch binding is emitted - expect(block!).not.toContain("posture: `autoresearch`"); + // never a wrong posture: no research binding is emitted + expect(block!).not.toContain("posture: `research`"); expect(block!).not.toContain("resolved=fallback"); }); @@ -872,7 +929,7 @@ describe("H4 — the last-assistant-message fallback (guarded, decline-correct)" const { client } = fakeClient({ getThrows: true, messages: [ - { id: "msg_001", role: "assistant", agent: "autodev" }, + { id: "msg_001", role: "assistant", agent: "develop" }, { id: "msg_002", role: "user", agent: "implementer" }, // dispatched cast switch, newer key ], }); @@ -927,6 +984,10 @@ describe("H4 — parity with the shared validator (the plugin stays dependency-f expect(PLUGIN_SUPPORTED_MODE_BUNDLE_VERSION).toBe(SUPPORTED_MODE_BUNDLE_VERSION); }); + it("the plugin's mode-id alias table ≡ schema's MODE_ID_ALIASES (#858 — read-resolve, both ids forever)", () => { + expect(PLUGIN_MODE_ID_ALIASES).toEqual(MODE_ID_ALIASES); + }); + it("the unresolvable headline is byte-exact the spec's D4 line", () => { expect(UNRESOLVABLE_HEADLINE).toBe(UNRESOLVABLE_LINE); }); @@ -944,10 +1005,10 @@ describe("H4 — parity with the shared validator (the plugin stays dependency-f it("the raw-payload client shape (older call shapes) is tolerated — same unwrap idiom as session_spawn.ts", async () => { const reg = stagedRegistry(); - const { client } = fakeClient({ agent: "autodev", rawShape: true }); + const { client } = fakeClient({ agent: "develop", rawShape: true }); const block = await buildModeBlock(deps(reg, client)); expect(block).not.toBeNull(); - expect(block!).toContain("posture: `autodev`"); + expect(block!).toContain("posture: `develop`"); }); }); diff --git a/packages/extension/test/mode_cards.test.ts b/packages/extension/test/mode_cards.test.ts index ffdf250a..3c7ad1e1 100644 --- a/packages/extension/test/mode_cards.test.ts +++ b/packages/extension/test/mode_cards.test.ts @@ -41,7 +41,7 @@ const LIVE_SKILL = path.join( "SKILL.md", ); -const CARDS = ["autodev.md", "autoresearch.md"] as const; +const CARDS = ["develop.md", "research.md"] as const; type CardName = (typeof CARDS)[number]; const SPINE_START = ""; @@ -137,7 +137,7 @@ describe("mode cards — spine parity (byte-identity)", () => { const spines = CARDS.map((name) => spineOf(cardText(name)).spine); it("the two spines are byte-identical", () => { - expect(spines[0], "autodev spine === autoresearch spine, byte for byte").toBe( + expect(spines[0], "develop spine === research spine, byte for byte").toBe( spines[1], ); }); @@ -153,7 +153,7 @@ describe("mode cards — spine parity (byte-identity)", () => { }); describe("mode cards — spine content floor", () => { - const spine = spineOf(cardText("autodev.md")).spine; // parity ⇒ one check suffices + const spine = spineOf(cardText("develop.md")).spine; // parity ⇒ one check suffices it("carries all five loop verbs, each in a sentence of at least 8 words", () => { for (const verb of LOOP_VERBS) { @@ -173,7 +173,7 @@ describe("mode cards — spine content floor", () => { }); describe("mode cards — ledger discovery rule (correctness by containment)", () => { - const spine = spineOf(cardText("autoresearch.md")).spine; + const spine = spineOf(cardText("research.md")).spine; it("the spine contains the canonical discovery-rule block verbatim", () => { const rule = discoveryRuleFrom(canonicalSkill); diff --git a/packages/extension/test/mode_cards_staging.test.ts b/packages/extension/test/mode_cards_staging.test.ts index c9e123b9..5e0c2c6a 100644 --- a/packages/extension/test/mode_cards_staging.test.ts +++ b/packages/extension/test/mode_cards_staging.test.ts @@ -15,6 +15,7 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFil import { tmpdir } from "node:os"; import { join } from "node:path"; import { globalAgentsDir, stageModCards } from "../src/mode_cards"; +import { validateModeRegistry } from "@amicode/schema"; // The REAL extension root — packages/extension/agents/ ships in the vsix. const EXTENSION_PATH = join(__dirname, ".."); @@ -66,9 +67,9 @@ describe("stageModCards", () => { it("overwrites a stale copy (extension-owned; always-copy on activate)", () => { const destDir = mkdtempSync(join(tmpdir(), "mode-cards-stale-")); mkdirSync(destDir, { recursive: true }); - writeFileSync(join(destDir, "autodev.md"), "# BOGUS_PLACEHOLDER_NOT_IN_REAL_CARD\n"); + writeFileSync(join(destDir, "develop.md"), "# BOGUS_PLACEHOLDER_NOT_IN_REAL_CARD\n"); stageModCards(EXTENSION_PATH, destDir, HERMETIC); - expect(readFileSync(join(destDir, "autodev.md"), "utf8")).not.toContain("BOGUS_PLACEHOLDER_NOT_IN_REAL_CARD"); + expect(readFileSync(join(destDir, "develop.md"), "utf8")).not.toContain("BOGUS_PLACEHOLDER_NOT_IN_REAL_CARD"); }); it("is idempotent — second call stages identical content without error", () => { @@ -86,7 +87,7 @@ describe("stageModCards", () => { it("throws (naming a shipped card) when the extension bundle carries no cards", () => { const fakeExtension = mkdtempSync(join(tmpdir(), "mode-cards-noext-")); const destDir = mkdtempSync(join(tmpdir(), "mode-cards-dest-")); - expect(() => stageModCards(fakeExtension, destDir, HERMETIC)).toThrow(/autodev\.md/); + expect(() => stageModCards(fakeExtension, destDir, HERMETIC)).toThrow(/develop\.md/); }); it("tripwire: the extension really ships the cards at the source path", () => { @@ -249,7 +250,7 @@ describe("stageModCards — overlay merge (entitlement + overlays present)", () const { destDir, receipt, result } = stageEntitled(); // D3 (#806): the four seeded role cards carry no dispatch target — // bundle-owned registry artifacts, never overlay-tuned - for (const card of ["hypothesizer.md", "experimenter.md", "analyzer.md", "implementer.md", "autodev.md", "autoresearch.md"]) { + for (const card of ["hypothesizer.md", "experimenter.md", "analyzer.md", "implementer.md", "develop.md", "research.md"]) { expect(readFileSync(join(destDir, card), "utf8")).toBe( readFileSync(join(AGENTS_SRC, card), "utf8"), ); @@ -604,3 +605,100 @@ describe("unreadable overlays dir (review F3)", () => { } }); }); + +// ── the public-rename independence lint (spec-20260907-011500 D1, #858) ───── +// +// The mode registry is PUBLIC product surface; the freeze validator governs +// the PREMIUM overlay merge. The registry BORROWS the freeze validator as a +// lint (the renamed cards' Method sections must stay mergeable deltas) — but +// the dependency is ONE-WAY: a premium-staging regression (a hostile or +// absent overlay source, a malformed registry, any rejection) must never +// block the public rename from shipping. + +describe("public-rename independence (#858 — premium staging never blocks the public path)", () => { + const RENAMED = ["develop.md", "research.md"]; + + it("the renamed director cards ship byte-identical with the premium entitlement present and a HOSTILE overlay source", () => { + // hostile = the overlays dir exists but every read degrades: malformed + // JSON, id/version mismatches, non-string fields — the registry loads + // with rejection records and NO overlay merges + const root = mkdtempSync(join(tmpdir(), "mode-cards-hostile-")); + const dir = join(root, "vault", "agents", "overlays"); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, "broken.json"), "{not json at all"); + writeFileSync( + join(dir, "mismatched.json"), + JSON.stringify({ overlay_version: 1, id: "other-id", fields: { model_routing: "TUNED" } }), + ); + writeFileSync(join(dir, "badfield.json"), JSON.stringify({ overlay_version: 1, id: "badfield", fields: { model_routing: 42 } })); + const destDir = mkdtempSync(join(tmpdir(), "mode-cards-hostile-dest-")); + const r = stageModCards(EXTENSION_PATH, destDir, { + entitlements: ENTITLED, + overlaySource: root, + }); + // the PUBLIC path shipped: the renamed cards land byte-identical + for (const f of RENAMED) { + expect(readFileSync(join(destDir, f), "utf8")).toBe(readFileSync(join(AGENTS_SRC, f), "utf8")); + } + // the premium side degraded HONESTLY: rejection records, never a throw, + // never a partial card + expect(r.rejections.length).toBeGreaterThanOrEqual(3); + expect(r.merges).toEqual([]); + }); + + it("the renamed director cards ship with NO entitlement and an absent overlay source (the zero-premium machine)", () => { + const destDir = mkdtempSync(join(tmpdir(), "mode-cards-noent-")); + const r = stageModCards(EXTENSION_PATH, destDir, { + entitlements: [], + overlaySource: join(mkdtempSync(join(tmpdir(), "mode-cards-noent-src-")), "never-created"), + }); + for (const f of RENAMED) { + expect(readFileSync(join(destDir, f), "utf8")).toBe(readFileSync(join(AGENTS_SRC, f), "utf8")); + } + expect(r.rejections).toEqual([]); + const receipt = JSON.parse(readFileSync(r.receiptPath, "utf8")); + expect(receipt.cards.every((c: { overlay_id: string | null }) => c.overlay_id === null)).toBe(true); + }); + + it("the freeze validator passes with the renamed set — the cards stay mergeable deltas (the borrowed lint)", () => { + // the renamed cards are not dispatch targets, so the freeze merge is + // exercised through a synthetic card shaped like the renamed directors — + // proving the lint is alive on the renamed surface, not vacuous + const fakeExt = mkdtempSync(join(tmpdir(), "mode-cards-lint-")); + mkdirSync(join(fakeExt, "agents"), { recursive: true }); + writeFileSync(join(fakeExt, "agents", "develop.md"), fixtureCard("text")); + const root = mkdtempSync(join(tmpdir(), "mode-cards-lint-src-")); + const dir = join(root, "vault", "agents", "overlays"); + mkdirSync(dir, { recursive: true }); + writeFileSync( + join(dir, "fixture-tuning.json"), + JSON.stringify({ + overlay_version: 1, + id: "fixture-tuning", + fields: { + model_routing: "TUNED-ROUTING", + iteration_budget: "TUNED-BUDGET", + prompt_body: "TUNED-BODY", + example_brief: "TUNED-BRIEF", + }, + }), + ); + const destDir = mkdtempSync(join(tmpdir(), "mode-cards-lint-dest-")); + const r = stageModCards(fakeExt, destDir, { entitlements: ENTITLED, overlaySource: root }); + expect(r.merges).toEqual([{ card: "develop.md", overlay_id: "fixture-tuning", merged_fields: ["prompt_body", "model_routing", "iteration_budget", "example_brief"] }]); + const staged = readFileSync(join(destDir, "develop.md"), "utf8"); + expect(staged).toContain("Model routing, tuned: TUNED-ROUTING"); + // the frozen contract stays out of reach + expect(staged).toContain("## Output contract"); + }); + + it("the public registry validation is disjoint from the premium staging path (one-way borrow)", () => { + // the registry-level validator reads modes/ + the declared files — it + // never imports the overlay source, so a premium-staging regression + // cannot even reach it (asserted structurally: validateModeRegistry on + // the renamed registry with the overlay machinery entirely absent) + const v = validateModeRegistry(join(EXTENSION_PATH, "modes"), EXTENSION_PATH); + expect(v.errors).toEqual([]); + expect(v.ok).toBe(true); + }); +}); diff --git a/packages/extension/test/mode_registry.test.ts b/packages/extension/test/mode_registry.test.ts index 886476e8..e0cc5678 100644 --- a/packages/extension/test/mode_registry.test.ts +++ b/packages/extension/test/mode_registry.test.ts @@ -63,12 +63,12 @@ describe("the shipped registry validates (one validator, two consumers)", () => }); it("each bundle validates on its own too", () => { - validationOk(validateModeBundle(join(MODES_DIR, "autodev"), { extensionRoot: EXT })); - validationOk(validateModeBundle(join(MODES_DIR, "autoresearch"), { extensionRoot: EXT })); + validationOk(validateModeBundle(join(MODES_DIR, "develop"), { extensionRoot: EXT })); + validationOk(validateModeBundle(join(MODES_DIR, "research"), { extensionRoot: EXT })); }); it("the modes directory holds exactly the two director bundles + the release index", () => { - expect(readdirSync(MODES_DIR).sort()).toEqual(["autodev", "autoresearch", "release-index.toml"]); + expect(readdirSync(MODES_DIR).sort()).toEqual(["develop", "release-index.toml", "research"]); }); }); @@ -83,7 +83,7 @@ describe("the shipped registry validates (one validator, two consumers)", () => // posture-blind — the exact failure this slice exists to close). describe("the posture-binding map: each manifest declares its binding agent (#808)", () => { it("both shipped manifests declare their binding agent", () => { - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { const manifest = parseModeManifest(readFileSync(join(MODES_DIR, mode, "mode.toml"), "utf8")); expect(manifest.agent, `${mode}/mode.toml must declare the agent id that binds its mode`).toBe(mode); } @@ -91,9 +91,9 @@ describe("the posture-binding map: each manifest declares its binding agent (#80 it("a manifest with NO declared agent fails the manifest schema, named", () => { const root = bundleCopy(); - const manifestPath = join(root, "modes", "autodev", "mode.toml"); - writeFileSync(manifestPath, readFileSync(manifestPath, "utf8").replace(/^agent = "autodev"\n/m, "")); - const v = validateModeBundle(join(root, "modes", "autodev"), { extensionRoot: root }); + const manifestPath = join(root, "modes", "develop", "mode.toml"); + writeFileSync(manifestPath, readFileSync(manifestPath, "utf8").replace(/^agent = "develop"\n/m, "")); + const v = validateModeBundle(join(root, "modes", "develop"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /agent/.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); @@ -101,9 +101,9 @@ describe("the posture-binding map: each manifest declares its binding agent (#80 it("a manifest whose declared agent is not a valid agent id fails the schema", () => { const root = bundleCopy(); - const manifestPath = join(root, "modes", "autodev", "mode.toml"); - writeFileSync(manifestPath, readFileSync(manifestPath, "utf8").replace('agent = "autodev"', 'agent = "Not An Agent"')); - const v = validateModeBundle(join(root, "modes", "autodev"), { extensionRoot: root }); + const manifestPath = join(root, "modes", "develop", "mode.toml"); + writeFileSync(manifestPath, readFileSync(manifestPath, "utf8").replace('agent = "develop"', 'agent = "Not An Agent"')); + const v = validateModeBundle(join(root, "modes", "develop"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /agent/.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); @@ -112,7 +112,7 @@ describe("the posture-binding map: each manifest declares its binding agent (#80 describe("bundle card parity (legacy staging stays authoritative — AC9)", () => { it("each bundle card.md is byte-identical to the legacy agents/ card it mirrors", () => { - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { expect(readFileSync(join(MODES_DIR, mode, "card.md"), "utf8")).toBe( readFileSync(join(AGENTS_DIR, `${mode}.md`), "utf8"), ); @@ -123,8 +123,8 @@ describe("bundle card parity (legacy staging stays authoritative — AC9)", () = describe("a bundle missing a DECLARED component fails (declared-set, AC1)", () => { it("missing pack.toml → fails, named", () => { const root = bundleCopy(); - rmSync(join(root, "modes", "autodev", "pack.toml")); - const v = validateModeBundle(join(root, "modes", "autodev"), { extensionRoot: root }); + rmSync(join(root, "modes", "develop", "pack.toml")); + const v = validateModeBundle(join(root, "modes", "develop"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /pack\.toml/.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); @@ -133,7 +133,7 @@ describe("a bundle missing a DECLARED component fails (declared-set, AC1)", () = it("missing manifest-declared role file → fails, role named", () => { const root = bundleCopy(); rmSync(join(root, "agents", "implementer.md")); - const v = validateModeBundle(join(root, "modes", "autodev"), { extensionRoot: root }); + const v = validateModeBundle(join(root, "modes", "develop"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /implementer/.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); @@ -142,7 +142,7 @@ describe("a bundle missing a DECLARED component fails (declared-set, AC1)", () = it("missing handoff-seed schema → fails, seed named", () => { const root = bundleCopy(); rmSync(join(root, "handoff-seeds", "issue-seed.schema.json")); - const v = validateModeBundle(join(root, "modes", "autodev"), { extensionRoot: root }); + const v = validateModeBundle(join(root, "modes", "develop"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /issue-seed\.schema\.json/.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); @@ -150,8 +150,8 @@ describe("a bundle missing a DECLARED component fails (declared-set, AC1)", () = it("missing card.md → fails, named", () => { const root = bundleCopy(); - rmSync(join(root, "modes", "autoresearch", "card.md")); - const v = validateModeBundle(join(root, "modes", "autoresearch"), { extensionRoot: root }); + rmSync(join(root, "modes", "research", "card.md")); + const v = validateModeBundle(join(root, "modes", "research"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /card\.md/.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); @@ -159,7 +159,7 @@ describe("a bundle missing a DECLARED component fails (declared-set, AC1)", () = it("a pack phase with zero gates fails the gate-pack schema (declared phases carry gates)", () => { const v = validateGatePack( - 'closing_artifact = "x"\n\n[[phases]]\nname = "decompose"\n\n[[handoffs]]\nkind = "issue_seed"\ntarget = "autodev"\n', + 'closing_artifact = "x"\n\n[[phases]]\nname = "decompose"\n\n[[handoffs]]\nkind = "issue_seed"\ntarget = "develop"\n', ); expect(v.ok).toBe(false); expect(v.errors.some((e) => /gates/.test(e))).toBe(true); @@ -168,21 +168,21 @@ describe("a bundle missing a DECLARED component fails (declared-set, AC1)", () = it("declared-set consistency: a manifest role absent from the pack fails; a pack role undeclared in the manifest fails", () => { // manifest declares a role the pack never casts const root = bundleCopy(); - const manifestPath = join(root, "modes", "autodev", "mode.toml"); + const manifestPath = join(root, "modes", "develop", "mode.toml"); writeFileSync(manifestPath, readFileSync(manifestPath, "utf8").replace( 'name = "implementer"', 'name = "librarian"\npath = "../../agents/librarian.md"', )); - let v = validateModeBundle(join(root, "modes", "autodev"), { extensionRoot: root }); + let v = validateModeBundle(join(root, "modes", "develop"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /librarian/.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); // the pack casts a role the manifest never declares const root2 = bundleCopy(); - const packPath = join(root2, "modes", "autoresearch", "pack.toml"); + const packPath = join(root2, "modes", "research", "pack.toml"); writeFileSync(packPath, readFileSync(packPath, "utf8").replace('roles = ["analyzer"]', 'roles = ["analyzer", "librarian"]')); - v = validateModeBundle(join(root2, "modes", "autoresearch"), { extensionRoot: root2 }); + v = validateModeBundle(join(root2, "modes", "research"), { extensionRoot: root2 }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /librarian/.test(e))).toBe(true); rmSync(root2, { recursive: true, force: true }); @@ -192,8 +192,8 @@ describe("a bundle missing a DECLARED component fails (declared-set, AC1)", () = // corrupt the dev pack's handoff to target a mode that does not exist const root = bundleCopy(); writeFileSync( - join(root, "modes", "autodev", "pack.toml"), - readFileSync(join(root, "modes", "autodev", "pack.toml"), "utf8").replace('target = "autoresearch"', 'target = "nonexistent-mode"'), + join(root, "modes", "develop", "pack.toml"), + readFileSync(join(root, "modes", "develop", "pack.toml"), "utf8").replace('target = "research"', 'target = "nonexistent-mode"'), ); const v = validateModeRegistry(join(root, "modes"), root); expect(v.ok).toBe(false); @@ -203,8 +203,8 @@ describe("a bundle missing a DECLARED component fails (declared-set, AC1)", () = it("a malformed manifest fails the manifest schema with named errors", () => { const root = bundleCopy(); - writeFileSync(join(root, "modes", "autodev", "mode.toml"), 'schema_version = "99"\nmode = "autodev"\n'); - const v = validateModeBundle(join(root, "modes", "autodev"), { extensionRoot: root }); + writeFileSync(join(root, "modes", "develop", "mode.toml"), 'schema_version = "99"\nmode = "develop"\n'); + const v = validateModeBundle(join(root, "modes", "develop"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /schema_version/.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); @@ -221,7 +221,7 @@ describe("the ledger-discovery-rule generated region (AC8)", () => { }); it("both bundle cards carry the region byte-identical to the generator", () => { - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { const card = readFileSync(join(MODES_DIR, mode, "card.md"), "utf8"); expect(classifyLedgerDiscoveryRegion(card).status).toBe("ok"); expect(card).toContain(region); @@ -234,7 +234,7 @@ describe("the ledger-discovery-rule generated region (AC8)", () => { const close = skill.indexOf("```", open + "```text".length); const skillBlock = skill.slice(open, close + 3); expect(region).toContain(skillBlock); - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { const card = readFileSync(join(MODES_DIR, mode, "card.md"), "utf8"); expect(card).toContain(skillBlock); } @@ -245,13 +245,13 @@ describe("the ledger-discovery-rule generated region (AC8)", () => { }); it("classification: a missing region is a named mismatch", () => { - const card = readFileSync(join(MODES_DIR, "autodev", "card.md"), "utf8"); + const card = readFileSync(join(MODES_DIR, "develop", "card.md"), "utf8"); const c = classifyLedgerDiscoveryRegion(card.replace(region, "")); expect(c.status).toBe("missing"); }); it("classification: an outdated stamp is a named mismatch (regenerate-and-compare detects)", () => { - const card = readFileSync(join(MODES_DIR, "autodev", "card.md"), "utf8"); + const card = readFileSync(join(MODES_DIR, "develop", "card.md"), "utf8"); const c = classifyLedgerDiscoveryRegion( card.replace(`generator=${MODE_GENERATOR_VERSION}`, "generator=v0"), ); @@ -259,7 +259,7 @@ describe("the ledger-discovery-rule generated region (AC8)", () => { }); it("the stamp never authorizes a pass: a FORGED current stamp over divergent bytes still fails", () => { - const card = readFileSync(join(MODES_DIR, "autodev", "card.md"), "utf8"); + const card = readFileSync(join(MODES_DIR, "develop", "card.md"), "utf8"); const tampered = card.replace( "Path convention — the session ledger lives in the personal vault at", "Path convention — TAMPERED hand-edited region body", @@ -270,22 +270,22 @@ describe("the ledger-discovery-rule generated region (AC8)", () => { expect(c.status).toBe("divergent"); // and the bundle validator refuses it const root = bundleCopy(); - writeFileSync(join(root, "modes", "autodev", "card.md"), tampered); - const v = validateModeBundle(join(root, "modes", "autodev"), { extensionRoot: root }); + writeFileSync(join(root, "modes", "develop", "card.md"), tampered); + const v = validateModeBundle(join(root, "modes", "develop"), { extensionRoot: root }); expect(v.ok).toBe(false); expect(v.errors.some((e) => /generated region|divergent|ledger-discovery/i.test(e))).toBe(true); rmSync(root, { recursive: true, force: true }); }); it("an unmarked hand-edited region body (delimiters stripped) is the missing case, never a pass", () => { - const card = readFileSync(join(MODES_DIR, "autodev", "card.md"), "utf8"); + const card = readFileSync(join(MODES_DIR, "develop", "card.md"), "utf8"); const stripped = card.replace(/\n/g, ""); expect(classifyLedgerDiscoveryRegion(stripped).status).toBe("missing"); }); }); describe("version floors (AC5 — per-consumer floor map)", () => { - const floors = parseModeManifest(readFileSync(join(MODES_DIR, "autodev", "mode.toml"), "utf8")).consumer_floors; + const floors = parseModeManifest(readFileSync(join(MODES_DIR, "develop", "mode.toml"), "utf8")).consumer_floors; const CONSUMERS = ["doctor", "plugin", "stager", "tests"] as const; it("the shipped manifest carries a floor for every consumer kind", () => { @@ -364,7 +364,7 @@ registry_revision = 1 describe("gate packs validate through the shared schema (re-homed)", () => { it("both shipped packs pass validateGatePack", () => { - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { const pack = readFileSync(join(MODES_DIR, mode, "pack.toml"), "utf8"); const v = validateGatePack(pack); validationOk(v as unknown as ModeBundleValidation); diff --git a/packages/extension/test/mode_registry_staging.test.ts b/packages/extension/test/mode_registry_staging.test.ts index 001ca974..0dd1ce3b 100644 --- a/packages/extension/test/mode_registry_staging.test.ts +++ b/packages/extension/test/mode_registry_staging.test.ts @@ -95,7 +95,7 @@ describe("stageModeBundles — the full-bundle stage", () => { const dest = freshDest(); const r = stageModeBundles(src, dest); expect(r.outcome).toBe("staged"); - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { for (const f of BUNDLE_FILES(mode)) { expect(existsSync(join(dest, "modes", mode, f)), `missing ${mode}/${f}`).toBe(true); expect(readFileSync(join(dest, "modes", mode, f), "utf8")).toBe( @@ -109,7 +109,7 @@ describe("stageModeBundles — the full-bundle stage", () => { expect(r.receiptPath).toBe(join(dest, "modes", ".deploy-receipt.json")); const receipt = JSON.parse(readFileSync(r.receiptPath!, "utf8")); expect(receipt.receipt_version).toBe(1); - expect(receipt.modes.map((m: { mode: string }) => m.mode).sort()).toEqual(["autodev", "autoresearch"]); + expect(receipt.modes.map((m: { mode: string }) => m.mode).sort()).toEqual(["develop", "research"]); for (const m of receipt.modes) { for (const file of m.files) { expect(file.sha256).toMatch(/^sha256:[0-9a-f]{64}$/); @@ -145,7 +145,7 @@ describe("stageModeBundles — idempotence (reconciled always-copy semantics)", // artifact bytes unchanged (same injected clock ⇒ even the receipt is // byte-identical; with a real clock only staged_at moves) expect(second).toBe(first); - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { for (const f of BUNDLE_FILES(mode)) { expect(readFileSync(join(dest, "modes", mode, f), "utf8")).toBe( readFileSync(join(src, "modes", mode, f), "utf8"), @@ -162,11 +162,11 @@ describe("stageModeBundles — idempotence (reconciled always-copy semantics)", const src = sourceFixture(); const dest = freshDest(); stageModeBundles(src, dest); - writeFileSync(join(dest, "modes", "autodev", "pack.toml"), "# TAMPERED\n"); + writeFileSync(join(dest, "modes", "develop", "pack.toml"), "# TAMPERED\n"); const r = stageModeBundles(src, dest); expect(r.outcome).toBe("staged"); - expect(readFileSync(join(dest, "modes", "autodev", "pack.toml"), "utf8")).toBe( - readFileSync(join(src, "modes", "autodev", "pack.toml"), "utf8"), + expect(readFileSync(join(dest, "modes", "develop", "pack.toml"), "utf8")).toBe( + readFileSync(join(src, "modes", "develop", "pack.toml"), "utf8"), ); rmSync(src, { recursive: true, force: true }); rmSync(dest, { recursive: true, force: true }); @@ -180,11 +180,11 @@ describe("stageModeBundles — atomic rename under a concurrent probe (AC2)", () // pre-stage so OLD complete content exists, then mutate the source so the // stage writes NEW content over it stageModeBundles(src, dest); - const modeDir = join(src, "modes", "autodev"); + const modeDir = join(src, "modes", "develop"); writeFileSync(join(modeDir, "card.md"), readFileSync(join(modeDir, "card.md"), "utf8") + "\n"); const oldBytes = new Map(); const newBytes = new Map(); - for (const mode of ["autodev", "autoresearch"]) { + for (const mode of ["develop", "research"]) { for (const f of BUNDLE_FILES(mode)) { oldBytes.set(`${mode}/${f}`, safeRead(join(dest, "modes", mode, f))); newBytes.set(`${mode}/${f}`, readFileSync(join(src, "modes", mode, f), "utf8")); @@ -229,10 +229,10 @@ describe("the staging lock (one lock per staging root)", () => { stageModeBundles(src, dest); // populate + no lock left const modesDir = join(dest, "modes"); writeLock(modesDir); // fresh heartbeat, OUR pid (alive), matching start-time - const before = safeRead(join(modesDir, "autodev", "pack.toml")); + const before = safeRead(join(modesDir, "develop", "pack.toml")); const r = stageModeBundles(src, dest); expect(r.outcome).toBe("aborted-locked"); - expect(safeRead(join(modesDir, "autodev", "pack.toml"))).toBe(before); // untouched + expect(safeRead(join(modesDir, "develop", "pack.toml"))).toBe(before); // untouched expect(readStagingLock(modesDir)).not.toBeNull(); // the live holder's lock stands rmSync(src, { recursive: true, force: true }); rmSync(dest, { recursive: true, force: true }); @@ -306,7 +306,7 @@ describe("stager version floor (AC5 — the stager is a consumer too)", () => { it("a bundle whose stager floor exceeds this stager's version aborts LOUDLY, nothing staged", () => { const src = sourceFixture(); // raise the floor above what this build supports - const manifestPath = join(src, "modes", "autodev", "mode.toml"); + const manifestPath = join(src, "modes", "develop", "mode.toml"); writeFileSync(manifestPath, readFileSync(manifestPath, "utf8").replace('stager = "1"', 'stager = "2"')); const dest = freshDest(); expect(() => stageModeBundles(src, dest)).toThrow(/version gap/); @@ -319,7 +319,7 @@ describe("stager version floor (AC5 — the stager is a consumer too)", () => { describe("staging refuses a broken SOURCE registry (anti-gaming: never stage what does not validate)", () => { it("a source card with a tampered generated region refuses staging (regenerate-and-compare)", () => { const src = sourceFixture(); - const cardPath = join(src, "modes", "autodev", "card.md"); + const cardPath = join(src, "modes", "develop", "card.md"); writeFileSync(cardPath, readFileSync(cardPath, "utf8").replace("kickoff before any work.", "TAMPERED RULE BODY.")); // keep the region's delimiters + stamp — a forged current stamp const dest = freshDest(); diff --git a/packages/extension/test/naming_records.test.ts b/packages/extension/test/naming_records.test.ts index 100e8778..47709599 100644 --- a/packages/extension/test/naming_records.test.ts +++ b/packages/extension/test/naming_records.test.ts @@ -34,8 +34,8 @@ const CONTEXT = readFileSync(CONTEXT_PATH, "utf8"); // after the term, exactly as the table writes it. const LOCKED_TERMS: ReadonlyArray<{ term: string; head: string }> = [ { term: "director", head: "the role that leads any autonomous loop" }, - { term: "autoresearch", head: "the research mode" }, - { term: "autodev", head: "the development mode" }, + { term: "research", head: "the research mode" }, + { term: "develop", head: "the development mode" }, { term: "campaign", head: "one bounded run of either autonomous mode" }, { term: "gate pack", head: "the typed set of gates + phase templates" }, { term: "mode", head: "one of the three director postures" }, @@ -102,15 +102,15 @@ describe("naming records — six locked terms in the amicode glossary", () => { } }); - it("director and autodev carry Avoid lines recording the banned names", () => { + it("director and develop carry Avoid lines recording the banned names", () => { const director = glossaryEntry("director"); - const autodev = glossaryEntry("autodev"); + const develop = glossaryEntry("develop"); expect(director, "director entry").not.toBeNull(); - expect(autodev, "autodev entry").not.toBeNull(); + expect(develop, "develop entry").not.toBeNull(); expect((director?.avoid ?? "").toLowerCase(), "director avoids 'conductor'").toContain( "conductor", ); - expect((autodev?.avoid ?? "").toLowerCase(), "autodev avoids 'autobuild'").toContain( + expect((develop?.avoid ?? "").toLowerCase(), "develop avoids 'autobuild'").toContain( "autobuild", ); }); @@ -140,7 +140,7 @@ describe("naming records — six locked terms in the amicode glossary", () => { }); // #807 (#809 fold) — the public workflow skill surface joins the naming -// discipline: the five dev-workflow skills + the autodev mode-protocol skill +// discipline: the five dev-workflow skills + the develop mode-protocol skill // are now user-facing product content (surface: public, in-repo canonical // copies), so the same locked vocabulary governs them. The pin reads the // fixture of record (protocol-blocklist.json) — never a private copy of the @@ -154,7 +154,7 @@ describe("naming records — the public workflow skills carry open-protocol voca "implement-issue", "write-an-issue", "break-into-subissues", - "autodev", + "autodev", // the develop mode's protocol skill — id retained at the #858 rename "sota-review", // #820 — the public SOTA survey skill joins the naming discipline ]; const blocklist = JSON.parse(readFileSync(BLOCKLIST_PATH, "utf8")) as { @@ -162,12 +162,12 @@ describe("naming records — the public workflow skills carry open-protocol voca banned_names: string[]; }; - it("the glossary's autodev entry governs the new skill: it names the mode, never a banned alias", () => { - const entry = glossaryEntry("autodev"); - expect(entry, "autodev entry").not.toBeNull(); + it("the glossary's develop entry governs the new skill: it names the mode, never a banned alias", () => { + const entry = glossaryEntry("develop"); + expect(entry, "develop entry").not.toBeNull(); expect((entry?.avoid ?? "").toLowerCase()).toContain("autobuild"); - const skill = readFileSync(join(SKILLS_DIR, "autodev", "SKILL.md"), "utf8").toLowerCase(); - expect(skill).toContain("autodev"); + const skill = readFileSync(join(SKILLS_DIR, "develop", "SKILL.md"), "utf8").toLowerCase(); + expect(skill).toContain("develop"); expect(skill).not.toContain("autobuild"); }); diff --git a/packages/extension/test/opencode_config.test.ts b/packages/extension/test/opencode_config.test.ts index 988c861b..7cbfcbf8 100644 --- a/packages/extension/test/opencode_config.test.ts +++ b/packages/extension/test/opencode_config.test.ts @@ -107,9 +107,15 @@ describe("buildOpencodeConfigContent", () => { const cfg = JSON.parse(buildOpencodeConfigContent("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/abs/AGENTS.md", TPL, "/home/u/.amico/runs/default")); expect(cfg.agent ?? {}).toEqual({}); // no custom agents: the interview lives in AGENTS.md, agent-agnostic }); - it("pins default_agent to plan (plan-first posture — ordered picker plan → build → autodev → autoresearch)", () => { + it("pins default_agent to plan (plan-first posture — the three named modes plan → develop → research)", () => { const cfg = JSON.parse(buildOpencodeConfigContent("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/abs/AGENTS.md", TPL, "/home/u/.amico/runs/default")); - expect(cfg.default_agent).toBe("plan"); // plan first; Agent.list keeps default first then alphabetical (custom sort makes build second) + expect(cfg.default_agent).toBe("plan"); // plan first; the picker order is agent_order's, honored app-side + }); + it("writes agent_order: plan → develop → research (#858 — the fixed picker order, #305's field)", () => { + const cfg = JSON.parse(buildOpencodeConfigContent("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/abs/AGENTS.md", TPL, "/home/u/.amico/runs/default")); + expect(cfg.agent_order).toEqual(["plan", "develop", "research"]); + // `build` is NOT in the named set — it exits the picker, not the vocabulary + expect(cfg.agent_order).not.toContain("build"); }); it("grants external_directory on the problems root (default + $AMICODE_PROBLEMS_DIR override), and the MCP server's environment follows BOTH", () => { const defGrant = join(homedir(), ".amico", "problems") + "/**"; @@ -264,7 +270,7 @@ describe.skipIf(!existsSync(OC_BIN))("opencode config injection + merge (1.17.3) // the MCP transport is declared for the REAL binary to consume: expect(cfg.mcp?.amicode?.type).toBe("local"); expect(cfg.mcp?.amicode?.command?.[1]?.endsWith(join("bin", "dist", "mcp-amico.mjs"))).toBe(true); - // #389: the pulse-designer agent shell is retired; default is plan (ordered picker plan → build → autodev → autoresearch). + // #389: the pulse-designer agent shell is retired; default is plan (ordered picker plan → build → develop → research). expect(cfg.agent?.["pulse-designer"]).toBeUndefined(); expect(cfg.default_agent).toBe("plan"); }); diff --git a/packages/extension/test/packaging.test.ts b/packages/extension/test/packaging.test.ts index 42b428d6..70a982da 100644 --- a/packages/extension/test/packaging.test.ts +++ b/packages/extension/test/packaging.test.ts @@ -16,7 +16,7 @@ const PUBLIC_WORKFLOW_SKILLS = [ "implement-issue", "write-an-issue", "break-into-subissues", - "autodev", + "develop", // #820 — the dual-lens SOTA survey skill (living-sota D1): public with the // same shipping discipline; a dropped copy = the loops' external-currency // survey never stages. @@ -80,24 +80,24 @@ const REQUIRED = [ // #807 — the PUBLIC workflow skill set (spec-20260905-063000 D2, ADR-0011 // amendment: workflow public, package-proprietary gated). A dropped copy = // the 2026-09-03 missing-director-core incident on every Marketplace - // machine: the autodev card points at skills that never stage. + // machine: the develop card points at skills that never stage. "extension/skills/director-core/SKILL.md", "extension/skills/develop/SKILL.md", "extension/skills/implement-issue/SKILL.md", "extension/skills/write-an-issue/SKILL.md", "extension/skills/break-into-subissues/SKILL.md", - "extension/skills/autodev/SKILL.md", + "extension/skills/develop/SKILL.md", "extension/skills/sota-review/SKILL.md", // #820 — the public SOTA survey skill must ship (living-sota D1) // #804 — the mode registry: the bundles the activation stager deploys and // the doctor probes ship in the vsix; a dropped modes/ = zero staged // bundles on every Marketplace machine (packaging.test runs on the built // vsix — the pin fires in the package gate). - "extension/modes/autodev/mode.toml", - "extension/modes/autodev/pack.toml", - "extension/modes/autodev/card.md", - "extension/modes/autoresearch/mode.toml", - "extension/modes/autoresearch/pack.toml", - "extension/modes/autoresearch/card.md", + "extension/modes/develop/mode.toml", + "extension/modes/develop/pack.toml", + "extension/modes/develop/card.md", + "extension/modes/research/mode.toml", + "extension/modes/research/pack.toml", + "extension/modes/research/card.md", "extension/modes/release-index.toml", // amicode_* plugin (Bun-transpiled .ts, loaded by absolute path) — every sibling // is load-bearing: a dropped file silently reverts the session to vanilla opencode. @@ -143,7 +143,7 @@ describe.skipIf(!existsSync(VSIX) && !REQUIRE_VSIX)("packaged VSIX contains runt /extension\/skills\/.+\/SKILL\.md/.test(listing), "no shipped skill SKILL.md — skills/ was excluded from the vsix", ).toBe(true); - // #807 — the five workflow skills + the autodev mode-protocol skill are + // #807 — the five workflow skills + the develop mode-protocol skill are // PUBLIC with in-repo canonical copies (ADR-0011 amendment): each must // ship. The PROPRIETARY set must never appear in the artifact — the // package-proprietary skills (`*-dev`) stay vault-only, never ship. @@ -183,10 +183,10 @@ describe("in-repo skill library — repo-boundary leak guard (ADR-0003 as amende it("the public workflow skills ARE the shipped set with revision-pinned frontmatter; the proprietary `-dev` set stays absent (AC5 → #807's policy of record)", () => { const names = readdirSync(SKILLS_DIR); // ADR-0011 amendment (2026-09-05, workflow-public / package-proprietary): - // the five dev-workflow skills + autodev are PUBLIC in-repo canonical + // the five dev-workflow skills + develop are PUBLIC in-repo canonical // copies — the old "dev skills stay internal" pin is superseded by the // amended policy, and this test now pins the NEW boundary: the five + - // autodev present with shipping frontmatter (public + source + revision); + // develop present with shipping frontmatter (public + source + revision); // the package-proprietary set (`*-dev`) stays out of the shipped library // (vault-only, never ships). for (const name of PUBLIC_WORKFLOW_SKILLS) { diff --git a/packages/extension/test/role_cards.test.ts b/packages/extension/test/role_cards.test.ts index 8974113f..dde05cc1 100644 --- a/packages/extension/test/role_cards.test.ts +++ b/packages/extension/test/role_cards.test.ts @@ -47,10 +47,10 @@ const PROVENANCE_PATH = join(AGENTS_DIR, ".seed-provenance.json"); /** The four role cards this slice owns, by bundle. */ const ROLE_CARDS = { - autoresearch: ["hypothesizer", "experimenter", "analyzer"] as const, - autodev: ["implementer"] as const, + research: ["hypothesizer", "experimenter", "analyzer"] as const, + develop: ["implementer"] as const, } as const; -const ALL_ROLES = [...ROLE_CARDS.autoresearch, ...ROLE_CARDS.autodev] as const; +const ALL_ROLES = [...ROLE_CARDS.research, ...ROLE_CARDS.develop] as const; type Role = (typeof ALL_ROLES)[number]; const cardPath = (role: Role): string => join(AGENTS_DIR, `${role}.md`); @@ -186,7 +186,7 @@ describe("staging — the seeded role cards ride the bundle, digest-verified (H2 cpSync(join(EXT, "handoff-seeds"), join(src, "handoff-seeds"), { recursive: true }); const dest = mkdtempSync(join(tmpdir(), "role-cards-dest2-")); stageModeBundles(src, dest); - const deployedRole = join(dest, "modes", "autodev", "roles", "implementer.md"); + const deployedRole = join(dest, "modes", "develop", "roles", "implementer.md"); writeFileSync(deployedRole, "# TAMPERED ROLE\n"); const r = stageModeBundles(src, dest); expect(r.outcome).toBe("staged"); diff --git a/packages/extension/test/scores/golden/router-section.md b/packages/extension/test/scores/golden/router-section.md index 8269bcf0..24d82673 100644 --- a/packages/extension/test/scores/golden/router-section.md +++ b/packages/extension/test/scores/golden/router-section.md @@ -17,7 +17,7 @@ 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. +- **Resume your research campaign** — ONLY when a session ledger exists under the personal vault's `sessions/`; the research director re-reads the latest ledger and continues the loop. - **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 workflow. - **Just explore** — free-form; no rail. diff --git a/packages/extension/test/scores/package_skills.test.ts b/packages/extension/test/scores/package_skills.test.ts index 5ce6cea2..ffcac313 100644 --- a/packages/extension/test/scores/package_skills.test.ts +++ b/packages/extension/test/scores/package_skills.test.ts @@ -532,7 +532,7 @@ describe("resolveLibrarySkills — typed revision selection (spec-20260905-06300 expect(r1.provenance.find((p) => p.name === "develop")!.outcome).toBe("canonical"); }); - it("H3 matrix — the five workflow skills + autodev stage for a NON-entitled session (the standalone gap, closed)", () => { + it("H3 matrix — the five workflow skills + develop stage for a NON-entitled session (the standalone gap, closed)", () => { // A non-entitled session: NO entitlements, and the ONLY root it has is the // in-repo library (a Marketplace machine has no vault mount). The public // workflow set must be there — this is the 2026-09-03 incident's fixture. @@ -546,7 +546,7 @@ describe("resolveLibrarySkills — typed revision selection (spec-20260905-06300 "implement-issue", "write-an-issue", "break-into-subissues", - "autodev", + "develop", ]) { expect(names, `non-entitled session stages ${name}`).toContain(name); } @@ -555,7 +555,7 @@ describe("resolveLibrarySkills — typed revision selection (spec-20260905-06300 it("every real in-repo workflow skill carries the D2 revision frontmatter (source + revision ≥ 1)", () => { const root = inRepoLibraryRoot(); if (!root) return; - for (const name of ["director-core", "develop", "implement-issue", "write-an-issue", "break-into-subissues", "autodev"]) { + for (const name of ["director-core", "develop", "implement-issue", "write-an-issue", "break-into-subissues", "develop"]) { const raw = fs.readFileSync(path.join(root, name, "SKILL.md"), "utf8"); const fm = raw.match(/^---\n([\s\S]*?)\n---/)![1]; expect(fm, `${name}: source label`).toMatch(/^source:\s*\S/m); diff --git a/packages/extension/test/scores/prep_integration.test.ts b/packages/extension/test/scores/prep_integration.test.ts index a2bdfff7..0ce01097 100644 --- a/packages/extension/test/scores/prep_integration.test.ts +++ b/packages/extension/test/scores/prep_integration.test.ts @@ -88,7 +88,7 @@ describe("prepareOpencodeProject × scores (spec §6)", () => { const proj = prep(); const agents = fs.readFileSync(proj.agentsPath, "utf8"); expect(agents).toContain("## Onset router"); - expect(agents).toContain("general-purpose autoresearch copilot"); // stub injected + expect(agents).toContain("general-purpose research copilot"); // stub injected expect(agents).not.toContain("Stages, in order:"); // hardcoded body replaced expect(agents).toContain("## Identity"); // engine sections intact expect(agents).toContain("## Style & formatting"); // harness sections intact (ADR 0008: run-dir contract moved to solve skill) @@ -402,7 +402,7 @@ PACKDRIVEN-BODY-MARKER. const proj = prep({ packsRoot }); const md = fs.readFileSync(proj.agentsPath, "utf8"); expect(md).toContain("## Onset router"); // router is always spliced - expect(md).toContain("general-purpose autoresearch copilot"); // stub present + expect(md).toContain("general-purpose research 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")); @@ -413,7 +413,7 @@ PACKDRIVEN-BODY-MARKER. const md = fs.readFileSync(proj.agentsPath, "utf8"); // Today: on-demand stub, no compiled score content expect(md).toContain("## Onset router"); - expect(md).toContain("general-purpose autoresearch copilot"); + expect(md).toContain("general-purpose research copilot"); }); }); diff --git a/packages/extension/test/session_spawn.test.ts b/packages/extension/test/session_spawn.test.ts index b7ead211..f2b31c99 100644 --- a/packages/extension/test/session_spawn.test.ts +++ b/packages/extension/test/session_spawn.test.ts @@ -12,9 +12,11 @@ import { depthRefusal, defaultTitle, childTitle, + resolveModeIdSpawn, unwrap, summarizeSpawned, } from "../opencode-plugin/session_spawn"; +import { MODE_ID_ALIASES } from "@amicode/schema"; describe("parseSpawnArgs", () => { it("defaults count=1, mode=fresh, force=false and trims the prompt", () => { @@ -88,6 +90,27 @@ describe("parseSpawnArgs", () => { const r = parseSpawnArgs({ prompt: "x", title: " CZ sweep ", agent: " " }); expect(r.ok && r.args.title === "CZ sweep" && r.args.agent === null).toBe(true); }); + + it("resolves the old director ids through the read-resolve alias (spec-20260907-011500 D1, #858)", () => { + const dev = parseSpawnArgs({ prompt: "x", agent: "autodev" }); + const res = parseSpawnArgs({ prompt: "x", agent: "autoresearch" }); + expect(dev.ok && dev.args.agent).toBe("develop"); + expect(res.ok && res.args.agent).toBe("research"); + }); + + it("passes new ids and explicit non-mode ids through untouched (build stays valid)", () => { + for (const id of ["develop", "research", "plan", "build", "implementer", "my-custom-agent"]) { + const r = parseSpawnArgs({ prompt: "x", agent: id }); + expect(r.ok && r.args.agent).toBe(id); + } + }); + + it("the spawn-side alias table is parity-pinned to the schema's MODE_ID_ALIASES (no-import contract)", () => { + expect(MODE_ID_ALIASES).toEqual({ autodev: "develop", autoresearch: "research" }); + expect(resolveModeIdSpawn("autodev")).toBe("develop"); + expect(resolveModeIdSpawn("autoresearch")).toBe("research"); + expect(resolveModeIdSpawn("build")).toBe("build"); + }); }); describe("computeDepth", () => { diff --git a/packages/extension/test/workflow_skills_public.test.ts b/packages/extension/test/workflow_skills_public.test.ts index 92a3b51a..509c4381 100644 --- a/packages/extension/test/workflow_skills_public.test.ts +++ b/packages/extension/test/workflow_skills_public.test.ts @@ -1,9 +1,13 @@ // workflow_skills_public.test.ts — the public workflow skill surface (#807, -// spec-20260905-063000 D2): the five dev-workflow skills + the NEW `autodev` +// spec-20260905-063000 D2): the five dev-workflow skills + the `autodev` // mode-protocol skill live as in-repo canonical copies under // packages/extension/skills/ with `surface: public`. This file pins: // -// - the `autodev` skill's structure, mirroring `autoresearch` (entry +// - the `autodev` skill's structure, mirroring `research` (#858 renamed +// the mode autodev→develop, autoresearch→research; the mode-protocol +// skill keeps its id — the workflow skill `develop` owns that id — and +// the read-resolve alias covers the old mode id everywhere) +// (entry // points, loop bound to the dev pack's phases/gates, ledger discipline, // honest degradation naming what is missing, handoff section with the // mid-session switch marked PENDING-D5 — parameterized on D5 state per @@ -38,23 +42,28 @@ const SIX = [ const skillText = (name: string): string => readFileSync(join(SKILLS, name, "SKILL.md"), "utf8"); -// ── the autodev skill: structure mirroring autoresearch (D2) ────────────────── +// ── the autodev skill: the develop mode's protocol, mirroring research (D2, +// renamed per spec-20260907-011500 D1) ────────────────────────────────── +// The MODE is `develop` (#858); the mode-protocol skill's id stays `autodev` +// (the workflow skill `develop` owns that id — the read-resolve alias covers +// the old mode id at every reference surface). The mirror skill renames +// cleanly: `autoresearch` → `research`. -describe("the autodev mode-protocol skill (#807, D2 — mirrors autoresearch)", () => { +describe("the autodev mode-protocol skill of the develop mode (#807, D2 — mirrors research)", () => { const autodev = skillText("autodev"); - const autoresearch = skillText("autoresearch"); + const research = skillText("research"); it("carries the three entry points (the agent card, the skill itself, kickoff-prompt lines)", () => { - expect(autodev).toMatch(/## Autodev|entry points/i); + expect(autodev).toMatch(/## Develop|entry points/i); expect(autodev).toMatch(/Entry points/i); - // mirroring autoresearch's entry-point shape: card, skill, kickoff lines + // mirroring research's entry-point shape: card, skill, kickoff lines expect(autodev).toMatch(/agent card/i); expect(autodev).toMatch(/kickoff/i); - expect(autoresearch).toMatch(/Entry points/i); // the mirror's shape — guard against drifting the mirror + expect(research).toMatch(/Entry points/i); // the mirror's shape — guard against drifting the mirror }); it("binds the loop to the dev pack's phases and gates (read from the landed registry's bundle, not prose)", () => { - const pack = readFileSync(join(MODES, "autodev", "pack.toml"), "utf8"); + const pack = readFileSync(join(MODES, "develop", "pack.toml"), "utf8"); for (const phase of ["decompose", "implement", "integrate"]) { expect(pack, `the shipped pack carries the ${phase} phase`).toMatch(new RegExp(`^name = "${phase}"`, "m")); expect(autodev, `the skill binds the ${phase} phase`).toContain(phase); @@ -64,7 +73,7 @@ describe("the autodev mode-protocol skill (#807, D2 — mirrors autoresearch)", expect(autodev, `the skill binds the ${gate} gate`).toContain(gate); } // and the mode's manifest declares the skill among its protocol skills - const manifest = readFileSync(join(MODES, "autodev", "mode.toml"), "utf8"); + const manifest = readFileSync(join(MODES, "develop", "mode.toml"), "utf8"); expect(manifest).toMatch(/"autodev"/); }); @@ -79,11 +88,11 @@ describe("the autodev mode-protocol skill (#807, D2 — mirrors autoresearch)", expect(autodev).toMatch(/## Handoffs/); expect(autodev).toContain("issue"); // receives the issue seed (issue-seed schema) expect(autodev).toContain("hypothesis"); // emits the hypothesis seed - // the pack's handoff target is autoresearch — the skill's emit matches it - const pack = readFileSync(join(MODES, "autodev", "pack.toml"), "utf8"); + // the pack's handoff target is research — the skill's emit matches it + const pack = readFileSync(join(MODES, "develop", "pack.toml"), "utf8"); expect(pack).toMatch(/hypothesis_seed/); - expect(pack).toMatch(/target = "autoresearch"/); - expect(autodev).toMatch(/autoresearch/); + expect(pack).toMatch(/target = "research"/); + expect(autodev).toMatch(/research/); }); it("the handoff section marks the mid-session switch PENDING-D5 — parameterized: the assertion flips when slice 5 lands", () => { @@ -111,7 +120,7 @@ describe("the autodev mode-protocol skill (#807, D2 — mirrors autoresearch)", } // names the ABSENT BUNDLE PARTS case (card / gate pack not staged) expect(section).toMatch(/Absent bundle parts/i); - expect(section).toMatch(/gate pack|modes\/autodev/i); + expect(section).toMatch(/gate pack|modes\/develop/i); expect(section).toMatch(/never pretend|do not fabricate|never a silent/i); // and the absent dispatch surface (the walk's own fallback) expect(section).toMatch(/Absent dispatch surface/i); diff --git a/scripts/deploy-agents.mjs b/scripts/deploy-agents.mjs index 408dbf3b..4307c2da 100644 --- a/scripts/deploy-agents.mjs +++ b/scripts/deploy-agents.mjs @@ -43,8 +43,8 @@ const RECEIPT_PATH = path.join(SOURCE_DIR, ".deploy-receipt.json"); // discovers the dir at runtime. const CARDS = [ "analyzer.md", - "autodev.md", - "autoresearch.md", + "develop.md", + "research.md", "experimenter.md", "hypothesizer.md", "implementer.md", From fd33cbcca0959439783aec5a9536f8558d4c222a Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 6 Sep 2026 21:34:25 -0400 Subject: [PATCH 3/4] test(amico-run): the doctor/upgrade fixtures follow the renamed mode surfaces Refs #858 --- packages/amico-run/test/helpers.ts | 12 ++--- packages/amico-run/test/surfaces.test.ts | 46 +++++++++---------- .../amico-run/test/upgrade-agents.test.ts | 24 +++++----- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/amico-run/test/helpers.ts b/packages/amico-run/test/helpers.ts index 04a3813b..15bfc860 100644 --- a/packages/amico-run/test/helpers.ts +++ b/packages/amico-run/test/helpers.ts @@ -198,7 +198,7 @@ function writeModeRegistryFixture( ); }; bundle( - "autodev", + "develop", ["implementer"], [ 'closing_artifact = "landed-delta record"', @@ -233,13 +233,13 @@ function writeModeRegistryFixture( "", "[[handoffs]]", 'kind = "hypothesis_seed"', - 'target = "autoresearch"', + 'target = "research"', "", ].join("\n"), "issue", ); bundle( - "autoresearch", + "research", ["hypothesizer", "experimenter", "analyzer"], [ 'closing_artifact = "experiment note + ledger delta"', @@ -276,7 +276,7 @@ function writeModeRegistryFixture( "", "[[handoffs]]", 'kind = "issue_seed"', - 'target = "autodev"', + 'target = "develop"', "", ].join("\n"), "hypothesis", @@ -331,7 +331,7 @@ export function buildDoctorWorld(opts: DoctorWorldOpts = {}): DoctorWorld { // ── agent cards: source (amicode repo) + both deployments + receipt ── const agentsSrc = join(repoAmicode, "packages", "extension", "agents"); - const CARDS = ["analyzer.md", "autodev.md", "autoresearch.md", "experimenter.md", "hypothesizer.md", "implementer.md", "librarian.md"]; + const CARDS = ["analyzer.md", "develop.md", "research.md", "experimenter.md", "hypothesizer.md", "implementer.md", "librarian.md"]; for (const c of CARDS) { mkdirSync(agentsSrc, { recursive: true }); writeFileSync(join(agentsSrc, c), `---\nmode: ${c.replace(".md", "")}\n---\n# ${c}\n`); @@ -518,7 +518,7 @@ export function advanceRegistryOnRemote(bare: string, newTag: string, newRevisio join(clone, "packages", "extension", "package.json"), JSON.stringify({ name: "amicode", version: base }, null, 2) + "\n", ); - const pack = join(clone, "packages", "extension", "modes", "autodev", "pack.toml"); + const pack = join(clone, "packages", "extension", "modes", "develop", "pack.toml"); writeFileSync(pack, readFileSync(pack, "utf8").replace("never delete tests to force green.", "never delete tests to force green. Bumped registry content.")); const index = join(clone, "packages", "extension", "modes", "release-index.toml"); writeFileSync( diff --git a/packages/amico-run/test/surfaces.test.ts b/packages/amico-run/test/surfaces.test.ts index 6190993c..dd7f5731 100644 --- a/packages/amico-run/test/surfaces.test.ts +++ b/packages/amico-run/test/surfaces.test.ts @@ -144,11 +144,11 @@ describe("doctor v2 surface inventory — stale cells", () => { test("agent-cards-global stale: deployed card tampered (per-card digest diff)", async () => { const w = buildDoctorWorld(); - writeFileSync(join(w.config, "agents", "autodev.md"), "---\nmode: autodev\n---\n# TAMPERED\n"); + writeFileSync(join(w.config, "agents", "develop.md"), "---\nmode: develop\n---\n# TAMPERED\n"); const report = await surfaceInventory(ctxForWorld(w)); const g = bySurface(report, "agent-cards-global"); expect(g.verdict).toBe("stale"); - expect(g.evidence.join(" ")).toMatch(/card autodev\.md changed/); + expect(g.evidence.join(" ")).toMatch(/card develop\.md changed/); const st = bySurface(report, "agent-cards-staging"); expect(st.verdict).toBe("current"); // the OTHER deployment is unaffected cleanup(); @@ -171,12 +171,12 @@ describe("doctor v2 surface inventory — stale cells", () => { const w = buildDoctorWorld(); const receiptPath = join(w.repoAmicode, "packages", "extension", "agents", ".deploy-receipt.json"); const receipt = JSON.parse(readFileSync(receiptPath, "utf8")) as { sources: { card: string; sha256: string }[] }; - receipt.sources.find((s) => s.card === "autodev.md")!.sha256 = "sha256:" + "0".repeat(64); // lies about autodev.md, by name — order-independent (the fixture stages the full 7-card surface) + receipt.sources.find((s) => s.card === "develop.md")!.sha256 = "sha256:" + "0".repeat(64); // lies about develop.md, by name — order-independent (the fixture stages the full 7-card surface) writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + "\n"); const report = await surfaceInventory(ctxForWorld(w)); const g = bySurface(report, "agent-cards-global"); expect(g.verdict).toBe("stale"); - expect(g.evidence.join(" ")).toMatch(/receipt source digest for autodev\.md ≠ current source/); + expect(g.evidence.join(" ")).toMatch(/receipt source digest for develop\.md ≠ current source/); cleanup(); }); }); @@ -480,11 +480,11 @@ describe("doctor mode-registry component verdicts (#804)", () => { } // every bundle component of the fixture registry is walked const components = r.components!.map((c) => `${c.mode}/${c.component}`); - expect(components).toContain("autodev/card.md"); - expect(components).toContain("autodev/pack.toml"); - expect(components).toContain("autodev/mode.toml"); - expect(components).toContain("autodev/roles/implementer.md"); - expect(components).toContain("autoresearch/roles/hypothesizer.md"); + expect(components).toContain("develop/card.md"); + expect(components).toContain("develop/pack.toml"); + expect(components).toContain("develop/mode.toml"); + expect(components).toContain("develop/roles/implementer.md"); + expect(components).toContain("research/roles/hypothesizer.md"); expect(components).toContain("registry/release-compare"); expect(components).toContain("registry/deploy-receipt"); } @@ -493,14 +493,14 @@ describe("doctor mode-registry component verdicts (#804)", () => { test("tampering ONE bundle component flips the record stale with the component NAMED (AC4)", async () => { const w = buildDoctorWorld(); - writeFileSync(join(w.config, "modes", "autoresearch", "pack.toml"), "# TAMPERED PACK\n"); + writeFileSync(join(w.config, "modes", "research", "pack.toml"), "# TAMPERED PACK\n"); const report = await surfaceInventory(ctxForWorld(w)); const g = bySurface(report, "agent-cards-global"); expect(g.verdict).toBe("stale"); - const named = componentOf(g, (c) => c.mode === "autoresearch" && c.component === "pack.toml"); + const named = componentOf(g, (c) => c.mode === "research" && c.component === "pack.toml"); expect(named).toBeDefined(); expect(named!.verdict).toBe("stale"); - expect(named!.evidence.join(" ")).toMatch(/component autoresearch\/pack\.toml changed/); + expect(named!.evidence.join(" ")).toMatch(/component research\/pack\.toml changed/); // the OTHER deployment is unaffected expect(bySurface(report, "agent-cards-staging").verdict).toBe("current"); cleanup(); @@ -511,11 +511,11 @@ describe("doctor mode-registry component verdicts (#804)", () => { // delete the deployed role file: the manifest DECLARES it, the deployed // set lacks it — the same violation the vitest-side validator cell // pins on the SOURCE tree, judged here on the DEPLOYED tree - rmSync(join(w.config, "modes", "autodev", "roles", "implementer.md"), { force: true }); + rmSync(join(w.config, "modes", "develop", "roles", "implementer.md"), { force: true }); const report = await surfaceInventory(ctxForWorld(w)); const g = bySurface(report, "agent-cards-global"); expect(g.verdict).toBe("stale"); - const named = componentOf(g, (c) => c.mode === "autodev" && c.component === "roles/implementer.md"); + const named = componentOf(g, (c) => c.mode === "develop" && c.component === "roles/implementer.md"); expect(named).toBeDefined(); expect(named!.verdict).toBe("stale"); expect(named!.evidence.join(" ")).toMatch(/missing from the deployed set/); @@ -528,13 +528,13 @@ describe("doctor mode-registry component verdicts (#804)", () => { test("a half-staged bundle (card new, roles old) reads stale with roles named — never current (AC4)", async () => { const w = buildDoctorWorld(); // the card keeps matching the release; the role copy is OLD (stale bytes) - writeFileSync(join(w.config, "modes", "autodev", "roles", "implementer.md"), "---\nmode: implementer\n---\n# OLD ROLE BYTES\n"); + writeFileSync(join(w.config, "modes", "develop", "roles", "implementer.md"), "---\nmode: implementer\n---\n# OLD ROLE BYTES\n"); const report = await surfaceInventory(ctxForWorld(w)); const g = bySurface(report, "agent-cards-global"); expect(g.verdict).toBe("stale"); - const card = componentOf(g, (c) => c.mode === "autodev" && c.component === "card.md"); + const card = componentOf(g, (c) => c.mode === "develop" && c.component === "card.md"); expect(card!.verdict).toBe("current"); // the card alone is fine — - const role = componentOf(g, (c) => c.mode === "autodev" && c.component === "roles/implementer.md"); + const role = componentOf(g, (c) => c.mode === "develop" && c.component === "roles/implementer.md"); expect(role!.verdict).toBe("stale"); // — but the bundle as a unit is stale expect(role!.evidence.join(" ")).toMatch(/roles\/implementer\.md changed/); cleanup(); @@ -590,12 +590,12 @@ describe("doctor mode-registry component verdicts (#804)", () => { test("a version gap between the bundle's doctor floor and THIS doctor fails LOUDLY (AC5)", async () => { const w = buildDoctorWorld(); - const manifestPath = join(w.config, "modes", "autodev", "mode.toml"); + const manifestPath = join(w.config, "modes", "develop", "mode.toml"); writeFileSync(manifestPath, readFileSync(manifestPath, "utf8").replace('doctor = "1"', 'doctor = "2"')); const report = await surfaceInventory(ctxForWorld(w)); const g = bySurface(report, "agent-cards-global"); expect(g.verdict).toBe("stale"); - const floorRow = componentOf(g, (c) => c.mode === "autodev" && c.component === "version-floor"); + const floorRow = componentOf(g, (c) => c.mode === "develop" && c.component === "version-floor"); expect(floorRow!.verdict).toBe("failed"); expect(floorRow!.evidence.join(" ")).toMatch(/version gap/); expect(floorRow!.evidence.join(" ")).toMatch(/never a silent degrade/); @@ -619,7 +619,7 @@ describe("doctor mode-registry component verdicts (#804)", () => { // the deployed bytes still match the machine's OWN release — the bundle // components themselves are current; the release compare is the staleness const g = bySurface(report, "agent-cards-global"); - expect(componentOf(g, (c) => c.mode === "autodev" && c.component === "card.md")!.verdict).toBe("current"); + expect(componentOf(g, (c) => c.mode === "develop" && c.component === "card.md")!.verdict).toBe("current"); cleanup(); }); @@ -671,13 +671,13 @@ describe("doctor mode-registry component verdicts (#804)", () => { const w = buildDoctorWorld(); // drift the CHECKOUT's registry ahead (uncommitted) — deployed matches tag writeFileSync( - join(w.repoAmicode, "packages", "extension", "modes", "autodev", "pack.toml"), + join(w.repoAmicode, "packages", "extension", "modes", "develop", "pack.toml"), "# CHECKOUT-ONLY DRIFT\n", ); const report = await surfaceInventory(ctxForWorld(w)); const g = bySurface(report, "agent-cards-global"); expect(g.verdict).toBe("current"); - expect(componentOf(g, (c) => c.mode === "autodev" && c.component === "pack.toml")!.verdict).toBe("current"); + expect(componentOf(g, (c) => c.mode === "develop" && c.component === "pack.toml")!.verdict).toBe("current"); cleanup(); }); }); @@ -794,7 +794,7 @@ describe("doctor v2 JSON contract", () => { { surface: "agent-cards-global", version: "1", source_version: "1", verdict: "current", evidence: ["ok"], components: [ - { mode: "autodev", component: "card.md", verdict: "current", evidence: ["byte-matches release"] }, + { mode: "develop", component: "card.md", verdict: "current", evidence: ["byte-matches release"] }, { mode: "registry", component: "release-compare", verdict: "stale", evidence: ["current to v0.3.1, stale to release v0.3.2"] }, ], }, diff --git a/packages/amico-run/test/upgrade-agents.test.ts b/packages/amico-run/test/upgrade-agents.test.ts index ea1042b2..b4b25240 100644 --- a/packages/amico-run/test/upgrade-agents.test.ts +++ b/packages/amico-run/test/upgrade-agents.test.ts @@ -23,7 +23,7 @@ function stageAgentsWorld(): DoctorWorld { mkdirSync(join(w.repoAmicode, "scripts"), { recursive: true }); copyFileSync(REAL_SCRIPT, join(w.repoAmicode, "scripts", "deploy-agents.mjs")); // stage stale: tamper one GLOBAL deployed card (per-card digest drift) - writeFileSync(join(w.config, "agents", "autodev.md"), "---\nmode: autodev\n---\n# TAMPERED\n"); + writeFileSync(join(w.config, "agents", "develop.md"), "---\nmode: develop\n---\n# TAMPERED\n"); return w; } @@ -84,8 +84,8 @@ describe("upgrade agents — stale deployment", () => { expect(lastReceipt(w).outcome).toBe("upgraded"); // the tampered card was actually repaired from source - const repaired = readFileSync(join(w.config, "agents", "autodev.md"), "utf8"); - const source = readFileSync(join(w.repoAmicode, "packages", "extension", "agents", "autodev.md"), "utf8"); + const repaired = readFileSync(join(w.config, "agents", "develop.md"), "utf8"); + const source = readFileSync(join(w.repoAmicode, "packages", "extension", "agents", "develop.md"), "utf8"); expect(repaired).toBe(source); cleanup(); }); @@ -141,13 +141,13 @@ describe("upgrade agents — mode-bundle convergence (#804)", () => { copyFileSync(REAL_SCRIPT, join(w.repoAmicode, "scripts", "deploy-agents.mjs")); // stage stale: tamper the GLOBAL deployed bundle pack (component drift — // the doctor names the component; the card digests alone stay clean) - writeFileSync(join(w.config, "modes", "autodev", "pack.toml"), "# TAMPERED PACK\n"); + writeFileSync(join(w.config, "modes", "develop", "pack.toml"), "# TAMPERED PACK\n"); const pre = await surfaceInventory(ctxForWorld(w)); const preGlobal = pre.surfaces.find((r) => r.surface === "agent-cards-global")!; expect(preGlobal.verdict).toBe("stale"); const named = (preGlobal.components ?? []).find( - (c) => c.mode === "autodev" && c.component === "pack.toml", + (c) => c.mode === "develop" && c.component === "pack.toml", ); expect(named, "the offending component is named in the pre-flight record").toBeDefined(); @@ -156,8 +156,8 @@ describe("upgrade agents — mode-bundle convergence (#804)", () => { expect((r.json as Record).outcome).toBe("upgraded"); expect((r.json as Record).verification).toBe(true); // the tampered component was repaired from the registry source - expect(readFileSync(join(w.config, "modes", "autodev", "pack.toml"), "utf8")).toBe( - readFileSync(join(w.repoAmicode, "packages", "extension", "modes", "autodev", "pack.toml"), "utf8"), + expect(readFileSync(join(w.config, "modes", "develop", "pack.toml"), "utf8")).toBe( + readFileSync(join(w.repoAmicode, "packages", "extension", "modes", "develop", "pack.toml"), "utf8"), ); // an independent doctor re-run agrees: both records current const independent = await surfaceInventory(ctxForWorld(w)); @@ -195,14 +195,14 @@ describe("upgrade agents — pre-flight gates + aborts", () => { test("deploy-agents.mjs absent from the checkout → aborted-environment, nothing deployed", async () => { const w = buildDoctorWorld(); - writeFileSync(join(w.config, "agents", "autodev.md"), "---\nmode: autodev\n---\n# TAMPERED\n"); - const tampered = readFileSync(join(w.config, "agents", "autodev.md"), "utf8"); + writeFileSync(join(w.config, "agents", "develop.md"), "---\nmode: develop\n---\n# TAMPERED\n"); + const tampered = readFileSync(join(w.config, "agents", "develop.md"), "utf8"); const r = await upgradeVerb(verbArgs(w, ["--root-receipts", receiptsDir(w)])); expect(r.code).toBe(1); const receipt = r.json as Record; expect(receipt.outcome).toBe("aborted-environment"); // nothing was deployed - expect(readFileSync(join(w.config, "agents", "autodev.md"), "utf8")).toBe(tampered); + expect(readFileSync(join(w.config, "agents", "develop.md"), "utf8")).toBe(tampered); cleanup(); }); }); @@ -231,7 +231,7 @@ describe("upgrade agents — role-card convergence (#806)", () => { for (const role of ROLE_CARD_NAMES) { writeFileSync(join(w.config, "agents", `${role}.md`), oldArtifact(role)); writeFileSync(join(w.staging, ".opencode", "agents", `${role}.md`), oldArtifact(role)); - const bundle = role === "implementer" ? "autodev" : "autoresearch"; + const bundle = role === "implementer" ? "develop" : "research"; writeFileSync(join(w.config, "modes", bundle, "roles", `${role}.md`), oldArtifact(role)); writeFileSync(join(w.staging, ".opencode", "modes", bundle, "roles", `${role}.md`), oldArtifact(role)); } @@ -299,7 +299,7 @@ test("doctor record names agent-cards-global / agent-cards-staging alias the age const cleanup = () => cleanupTracked(); try { // stage drift so the aliased run has something to do - writeFileSync(join(w.config, "agents", "autodev.md"), "---\nmode: autodev\n---\n# TAMPERED\n"); + writeFileSync(join(w.config, "agents", "develop.md"), "---\nmode: develop\n---\n# TAMPERED\n"); for (const alias of ["agent-cards-global", "agent-cards-staging"]) { const argv = [...verbArgs(w), "--root-receipts", receiptsDir(w)]; argv[0] = alias; // the panel sends the doctor record name as the surface From c355cbbbcec1df00d26fbe3d5d0776537e205258 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 6 Sep 2026 21:34:46 -0400 Subject: [PATCH 4/4] =?UTF-8?q?feat(app):=20the=20picker=20reads=20the=20t?= =?UTF-8?q?hree=20modes=20=E2=80=94=20fixed=20order,=20implied=20posture,?= =?UTF-8?q?=20read-resolve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #858 (spec-20260907-011500 D1): the overlay's picker sort honors the config's agent_order APP-SIDE (fork PR #305's precedence: order primary, default_agent pin secondary, alphabetical last) — the product order plan → develop → research renders even on an engine without the #305 sort. Stock build stays reachable and is MARKED implied (one i18n'd suffix, 18 locales) — never a fourth named tile. Persisted selections carrying the old director ids read-resolve to the renamed cards. Manifest hashes registered for all 22 edited overlay files (#848 gate). Refs #858 --- packages/app-bundle/manifest.json | 46 ++++++------ .../app/src/components/prompt-input-v2.tsx | 12 ++- .../app/src/context/local-agent.test.ts | 74 ++++++++++++++++++- .../packages/app/src/context/local-agent.ts | 56 +++++++++++++- .../packages/app/src/context/local.tsx | 16 +++- .../overlay/packages/app/src/i18n/ar.ts | 1 + .../overlay/packages/app/src/i18n/br.ts | 1 + .../overlay/packages/app/src/i18n/bs.ts | 1 + .../overlay/packages/app/src/i18n/da.ts | 1 + .../overlay/packages/app/src/i18n/de.ts | 1 + .../overlay/packages/app/src/i18n/en.ts | 1 + .../overlay/packages/app/src/i18n/es.ts | 1 + .../overlay/packages/app/src/i18n/fr.ts | 1 + .../overlay/packages/app/src/i18n/ja.ts | 1 + .../overlay/packages/app/src/i18n/ko.ts | 1 + .../overlay/packages/app/src/i18n/no.ts | 1 + .../overlay/packages/app/src/i18n/pl.ts | 1 + .../overlay/packages/app/src/i18n/ru.ts | 1 + .../overlay/packages/app/src/i18n/th.ts | 1 + .../overlay/packages/app/src/i18n/tr.ts | 1 + .../overlay/packages/app/src/i18n/uk.ts | 1 + .../overlay/packages/app/src/i18n/zh.ts | 1 + .../overlay/packages/app/src/i18n/zht.ts | 1 + 23 files changed, 194 insertions(+), 28 deletions(-) diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index e94bbbdd..a3296924 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -6,7 +6,7 @@ "fork_sha": "d161eb0cfc6d03a53311e083b590005b4161c13a", "upstream_base": "v1.18.29", "upstream_base_sha": "16747470f976aca3d362ad730bcd3fe82ecc2c9a", - "extracted_at": "2026-09-06T20:54:10.664Z", + "extracted_at": "2026-09-07T01:34:44.000Z", "per_package": { "packages/app": { "A": 132, @@ -1011,7 +1011,7 @@ "packages/app/src/components/pane-bridge.tsx": "756b3306b51c963fac6778716f6c6c4626f0f3bf75c93b7cb4cab58ea0bf3d4e", "packages/app/src/components/profile-popover.tsx": "eff742e90544455f13234a9351bb1b82189663cba2cd3172bdcea1a8a37284e8", "packages/app/src/components/prompt-input-clipboard-structure.test.ts": "946b718c2164da394602f26e4d9919d752da0d0b9bee714f345ba16c274542da", - "packages/app/src/components/prompt-input-v2.tsx": "9ec794b2b9dbcfbfeb9d880df0038087fe6b8df219c5c96b1971f7fc766b67d2", + "packages/app/src/components/prompt-input-v2.tsx": "dc16eae49d33019bf16d9c29250f7e0a1f41301f6a3fe43f4625f67f4b782eb5", "packages/app/src/components/prompt-input.tsx": "1bc71d585a5ada2caf6d190d702dcabbfb56642e740a08ad6aedf078809b60e5", "packages/app/src/components/prompt-project-selector.tsx": "0157c954b938bce2b5aca9a82f4a72338e361f4c9027e55a7fd5a14a1654d294", "packages/app/src/components/report-bug-button.css": "909809b6241d53c386aa4c8ea34539ba162a8998fff755834c783025bc53a5fd", @@ -1051,9 +1051,9 @@ "packages/app/src/context/layout-tabs.test.ts": "4d9fdbe963306f164b2f48eac3b6a28c5a703c1758bf14b2cc4796720d8fa72c", "packages/app/src/context/layout-tabs.ts": "741506ce165f68cdb0b8f2931a2dad3e26c05880f9286c3daf04ec979bac21c5", "packages/app/src/context/layout.tsx": "29a39367295b0a70fa0f7603a6b91b4d64ad0a134616ebfe79488245caaad293", - "packages/app/src/context/local-agent.test.ts": "a5a9d60bb4401d409218cc247c1cc06ede8b7878ddf01dc6080328f54eb9cade", - "packages/app/src/context/local-agent.ts": "0aab67e695dc3bb45a733ac0df80a0a5e14cfe29b2375b6dc3cd07fbccea33e2", - "packages/app/src/context/local.tsx": "3ab8b9fc2db082df4ba485373679f00a95d0ffe3c691a0afee1dc53db7eabd9e", + "packages/app/src/context/local-agent.test.ts": "18478fb1d3436a7250b7000bff03d81d42c603aa80d50b89a649d443bd4e71b5", + "packages/app/src/context/local-agent.ts": "ddbd335781ece4d70caa0309472f0720ef2f34b5d66770ee403419b5fe195604", + "packages/app/src/context/local.tsx": "0aaf90b42819c43bd129650c55e47ecc5ee72894aa6255aa22e21e6c8a0aa157", "packages/app/src/context/models.tsx": "8904525767b36ed5ba93e85a0fddb9bf737a8d066dfff084380ce1a67182f535", "packages/app/src/context/platform.tsx": "edd9b1773feb5c084b922a57b6914f6a473eed558b44f9de0eeb1c4d4e2b49da", "packages/app/src/context/server-sdk.test.ts": "5cd22962c8ea1e4374915508a765281ceb9f808ad8bbf241c06b00b48621c816", @@ -1066,26 +1066,26 @@ "packages/app/src/context/vault-panel.ts": "f91cb5fb49a9f0d46bd2a1b131f9a218b30147e2ad3617d2a2dc4f1e14599a53", "packages/app/src/context/workbench.tsx": "69a44da2b69989bd2c67ebffdb49941841f3f4718d6a70d659c828b57928d9eb", "packages/app/src/context/zoom-keybind.test.ts": "a1385c93639687a7dac7d21f8f02841d82a55e42465ce3836b97b16c82b52469", - "packages/app/src/i18n/ar.ts": "203f2af61e60c6159bab64c53ebfdfcf6c5522bdec9abdbc33e3bd581ab3c744", - "packages/app/src/i18n/br.ts": "efb5c0084a32608a2af2fdb717828edf3ba93ee6139c00bf4a533c8786f7400e", - "packages/app/src/i18n/bs.ts": "d0aa1226c0bbf15d72ce62f0aea222e8d409f51a25d0310ffcabaa08b0e1e643", - "packages/app/src/i18n/da.ts": "eef0b00d71cf80bb6f2f3f5e7f0db44db346a89a2e6b5461940867d08ccbc1c7", - "packages/app/src/i18n/de.ts": "d82a1122c8544843d157c019ac8bb9f7b1bd531f2c923cbea363b049b4f759e7", + "packages/app/src/i18n/ar.ts": "a8f0d95181d30c9dcb2d771cb6f39f9c1cdc21e886422d57d8ea5d9cb11ca571", + "packages/app/src/i18n/br.ts": "71d0947e6fd87ed21b4a297bcd5ecda73836bc7f1f4ea916a15e1593256096ef", + "packages/app/src/i18n/bs.ts": "34f33770c2a6ad6c784129669eabe73e9efd1971236b54795283159974a31012", + "packages/app/src/i18n/da.ts": "3fad4a2289414f88f6c3eacb2b9fdb67a943d3c7d13648e17886ec65d4939fdc", + "packages/app/src/i18n/de.ts": "8a371c52fd93546ad53e7ccd0fcd269b9198f78944c80b57993adcfe61bc722c", "packages/app/src/i18n/desktop-native.ts": "8fed66d36a0b6cdbf50e3a50f14b6f8ab3570e342e0905e5a926d7dc6c57f8fa", - "packages/app/src/i18n/en.ts": "7d9944c369d710ecf5166c9a05a39fa1f0d4303bb414ccce30de5d89ba60f154", - "packages/app/src/i18n/es.ts": "3661a863467277c49a7c89d7b799f65622d2a0beef7c7199c79ce79e62f97cd5", - "packages/app/src/i18n/fr.ts": "aa4e824191d7c0927ee92167b2e5152819694e60247089ab923cbfbcaeaa9b8f", - "packages/app/src/i18n/ja.ts": "2998b685dfc1664f32ac07acdde4eb38fb9721e08df674259578f8170b916839", - "packages/app/src/i18n/ko.ts": "46b318e6ac79c1601d1f7a76ae99749bab2c9d741d5604c9f88992001d3c32ba", - "packages/app/src/i18n/no.ts": "1a1eb5aa980f7d876839e10c5c562e34b24b04ea09ff64d1da062d661f6b4d66", + "packages/app/src/i18n/en.ts": "0d77de0d7b314f766eebc8d4ccbfe112fbdce9cdbbf80a634f7c829dc8df4528", + "packages/app/src/i18n/es.ts": "574146b271e67aaee40b4a6b7c0c5a63b151e90741e41b357ec26356dab68ba2", + "packages/app/src/i18n/fr.ts": "a103d1dd04d4e31e55c02abb66cf55a88280619eeea7738433c070dcb2223889", + "packages/app/src/i18n/ja.ts": "ad83d0a490c9da89f6d154ba241b86957a6d674dfe289475a32dfdb0b41a9c0b", + "packages/app/src/i18n/ko.ts": "5610523e63bfe78f82a18a693b51f0746f76a455cc10a648f9ad762e783a7c91", + "packages/app/src/i18n/no.ts": "3bf55fa633a2c9e52fd0650c2310bfb39186cb7a3dea084e5bee5f7c4983e3e2", "packages/app/src/i18n/parity.test.ts": "2c8a1d3af6648cbce73c4fee45008c9928d1126a6b044cb0a32dbc566967c161", - "packages/app/src/i18n/pl.ts": "e691e569af0ae51dc587e4ca377dfcdbe4b4ea5fa1dedc30c643853ac1b6e535", - "packages/app/src/i18n/ru.ts": "6dd9ecccb8c0f3b00a56c4d4e9b17ae428101e7586ea94a2e82472ecf6cdd8ba", - "packages/app/src/i18n/th.ts": "7606ba17eb157d601f0162ba7f786e6df5b072c7b0a31252d3e5e6a4a0e70082", - "packages/app/src/i18n/tr.ts": "2ace124cfcfc856b336d26fa068296515bff81ddd1aabffaaa894ec59932ae2d", - "packages/app/src/i18n/uk.ts": "58b8fa6cc337bb6dd690f970694c217227122ca7b72e99559e886eb4398cd5e5", - "packages/app/src/i18n/zh.ts": "952324539d560a0fd179daf7d43308f8b0678493bd3482e2050666da70184992", - "packages/app/src/i18n/zht.ts": "dccb8da565506352fa12cd2b406c2bbf23e4afdb90f8aa3634e6b9eaa9e54b57", + "packages/app/src/i18n/pl.ts": "e0e24a520fd12caae23f1f74577180471f0b6fbdc6123d84732245c4c939e112", + "packages/app/src/i18n/ru.ts": "a48961eeb3587a197b23b5a5f4e3e9a6438b4080a00a3228396eb15e55c1b56b", + "packages/app/src/i18n/th.ts": "a3bfa601f80241d93452395d00d0d0743d69d2b7c6228b2097316ee94f4d27d6", + "packages/app/src/i18n/tr.ts": "c35f1e2b3db2cda0c9ad0e8e1f22cd4a99210207c310f5812a57339293ee5c2a", + "packages/app/src/i18n/uk.ts": "818bc6ddbf405d37cfec315f92d3ce2c622180eb804b63388fbda781c2d57ba7", + "packages/app/src/i18n/zh.ts": "d178ee9db16eedd00d71b6d0b5c258b287e98a8d4c607032fc31340b1ea968ae", + "packages/app/src/i18n/zht.ts": "4bb972fbebf9bd02c3e7bae4e2507f92cedad0ab8c3dcd3f92b8a7814820e2cf", "packages/app/src/pages/error.tsx": "db9ba0847cfb205100ad6133cce30c6ea2ce983802af44947e6d2b746c2a6ec5", "packages/app/src/pages/home-projects.test.ts": "a90155c37210a0e5b7a095bbfc3ba2d40681f02927d9726148a706bed5ba7891", "packages/app/src/pages/home-projects.ts": "d55124578dc839b98e2084b488d818e30e4a1abf1ae264499b83e403c2aabca7", diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx index be6ae492..b1eb6755 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx @@ -22,6 +22,7 @@ import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { usePermission } from "@/context/permission" +import { isImpliedAgent } from "@/context/local-agent" import { type ImageAttachmentPart, usePrompt } from "@/context/prompt" import { usePlatform } from "@/context/platform" import { useSDK } from "@/context/sdk" @@ -398,7 +399,16 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): get agent() { return props.controls.agents.visible && props.controls.agents.options.length > 0 ? { - options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })), + // #858 — the implied posture is marked, never a fourth named + // tile: the underlying default agent (stock `build`) stays + // reachable but renders the implied suffix, not a mode name. + options: () => + props.controls.agents.options.map((name) => ({ + id: name, + label: isImpliedAgent(name) + ? `${name} · ${language.t("agent.picker.implied")}` + : name, + })), current: () => props.controls.agents.current, onSelect: (value: string) => props.controls.agents.select(value), keybind: () => command.keybindParts("agent.cycle"), diff --git a/packages/app-bundle/overlay/packages/app/src/context/local-agent.test.ts b/packages/app-bundle/overlay/packages/app/src/context/local-agent.test.ts index 75b60b04..c27ea0f2 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/local-agent.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/local-agent.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { hasAgentChoice, hasCustomAgent, resolveAgent } from "./local-agent" +import { hasAgentChoice, hasCustomAgent, resolveAgent, orderByPickerConfig, impliedAgent, isImpliedAgent } from "./local-agent" describe("hasCustomAgent", () => { test("detects explicitly custom agents", () => { @@ -41,4 +41,76 @@ describe("resolveAgent", () => { test("uses the first agent when build is unavailable", () => { expect(resolveAgent([{ name: "custom" }], "missing")?.name).toBe("custom") }) + + test("resolves the old director ids through the read-resolve alias (#858)", () => { + const renamed = [{ name: "plan" }, { name: "develop" }, { name: "research" }, { name: "build" }] + expect(resolveAgent(renamed, "autodev")?.name).toBe("develop") + expect(resolveAgent(renamed, "autoresearch")?.name).toBe("research") + // build stays valid — it exits the picker, not the vocabulary + expect(resolveAgent(renamed, "build")?.name).toBe("build") + }) +}) + +describe("orderByPickerConfig (#858 — the fixed order plan → develop → research, #305 semantics)", () => { + const agents = [{ name: "research" }, { name: "build" }, { name: "plan" }, { name: "develop" }, { name: "custom" }] + + test("agent_order is the PRIMARY sort key — listed agents in declared order, unlisted after", () => { + const out = orderByPickerConfig(agents, { agent_order: ["plan", "develop", "research"] }) + expect(out.map((a) => a.name)).toEqual(["plan", "develop", "research", "build", "custom"]) + }) + + test("unlisted agents keep their server order beneath the listed ones; the implicit build pin + alphabetical order them", () => { + const out = orderByPickerConfig( + [{ name: "zeta" }, { name: "build" }, { name: "alpha" }, { name: "plan" }], + { agent_order: ["plan"] }, + ) + // build is the implicit default pin (#305's fallback) — it leads the + // unlisted block; alpha/zeta follow alphabetically + expect(out.map((a) => a.name)).toEqual(["plan", "build", "alpha", "zeta"]) + }) + + test("the default_agent pin is the secondary key among the unlisted (#305 precedence)", () => { + const out = orderByPickerConfig( + [{ name: "zeta" }, { name: "build" }, { name: "alpha" }], + { agent_order: ["plan"], default_agent: "zeta" }, + ) + expect(out.map((a) => a.name)).toEqual(["zeta", "alpha", "build"]) + }) + + test("no agent_order config falls back to the incoming order unchanged (stock behavior)", () => { + expect(orderByPickerConfig(agents, {}).map((a) => a.name)).toEqual([ + "research", + "build", + "plan", + "develop", + "custom", + ]) + expect(orderByPickerConfig(agents, undefined).map((a) => a.name)).toEqual([ + "research", + "build", + "plan", + "develop", + "custom", + ]) + }) + + test("an agent_order entry naming no shipped agent is inert", () => { + const out = orderByPickerConfig(agents, { agent_order: ["plan", "ghost", "develop", "research"] }) + expect(out.map((a) => a.name)).toEqual(["plan", "develop", "research", "build", "custom"]) + }) +}) + +describe("the implied posture marker (#858 — the default posture is reachable and marked implied)", () => { + test("build — the underlying default agent — is the implied posture", () => { + expect(isImpliedAgent("build")).toBe(true) + expect(isImpliedAgent("plan")).toBe(false) + expect(isImpliedAgent("develop")).toBe(false) + expect(isImpliedAgent("research")).toBe(false) + expect(isImpliedAgent("custom")).toBe(false) + }) + + test("impliedAgent returns the marked option shape; other agents pass through unmarked", () => { + expect(impliedAgent("build", "implizit")).toEqual({ name: "build", implied: true }) + expect(impliedAgent("plan", "implizit")).toEqual({ name: "plan", implied: false }) + }) }) diff --git a/packages/app-bundle/overlay/packages/app/src/context/local-agent.ts b/packages/app-bundle/overlay/packages/app/src/context/local-agent.ts index a1de48b4..91ce7912 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/local-agent.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/local-agent.ts @@ -10,6 +10,60 @@ export function hasAgentChoice(items: T[]) { return hasCustomAgent(items) || items.length > 1 } +// ── the mode-id read-resolve alias (spec-20260907-011500 D1, #858) ────────── +// autodev → develop, autoresearch → research. READ-RESOLVE, never +// migrate-on-write: a persisted session selection carrying an old id binds +// the renamed card at read time. `build` is NOT aliased — it exits the +// picker, not the vocabulary. The alias window's exit rides the next +// mode-bundle CONTRACT-VERSION bump. +const MODE_ID_ALIASES: Record = { + autodev: "develop", + autoresearch: "research", +} + export function resolveAgent(items: T[], name?: string) { - return items.find((item) => item.name === name) ?? items.find((item) => item.name === "build") ?? items[0] + const wanted = name === undefined ? undefined : MODE_ID_ALIASES[name] ?? name + return items.find((item) => item.name === wanted) ?? items.find((item) => item.name === "build") ?? items[0] +} + +/** Picker ordering (#858 — the fixed order plan → develop → research, fork + * PR #305's `agent_order` semantics honored APP-SIDE per the overlay + * architecture): `agent_order` is the PRIMARY sort key — listed agents in + * declared order, unlisted agents after every listed one; the + * `default_agent` pin is the secondary key among the unlisted, then + * alphabetical (exactly #305's sortBy precedence). No config, and the list + * passes through unchanged (stock behavior for a standalone server). */ +export function orderByPickerConfig( + items: T[], + config?: { agent_order?: string[]; default_agent?: string } | undefined, +): T[] { + const order = config?.agent_order + if (!order || order.length === 0) return items + const indexOf = (name: string) => { + const i = order.indexOf(name) + return i === -1 ? Number.MAX_SAFE_INTEGER : i + } + const isDefault = (name: string) => (config?.default_agent ? name === config.default_agent : name === "build") + return [...items].sort( + (a, b) => indexOf(a.name) - indexOf(b.name) || Number(isDefault(b.name)) - Number(isDefault(a.name)) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0), + ) +} + +/** The implied posture (#858): the underlying default agent — stock `build` + * — is the implied auto. It stays reachable (selection, Tab-cycle, old + * sessions) but is not one of the three named modes, so the picker marks it + * instead of giving it a fourth named tile. */ +export function isImpliedAgent(name: string): boolean { + return name === "build" +} + +export interface AgentPickerOption { + name: string + implied: boolean +} + +/** Map a picker agent name to its option shape: the implied posture carries + * the marker (the label renders the i18n'd suffix). */ +export function impliedAgent(name: string, _impliedLabel?: string): AgentPickerOption { + return { name, implied: isImpliedAgent(name) } } diff --git a/packages/app-bundle/overlay/packages/app/src/context/local.tsx b/packages/app-bundle/overlay/packages/app/src/context/local.tsx index 63812e17..23b405ce 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/local.tsx +++ b/packages/app-bundle/overlay/packages/app/src/context/local.tsx @@ -7,7 +7,7 @@ import { useModels } from "@/context/models" import { useSettings } from "@/context/settings" import { useProviders } from "@/hooks/use-providers" import { Persist, persisted } from "@/utils/persist" -import { hasAgentChoice, resolveAgent } from "./local-agent" +import { hasAgentChoice, orderByPickerConfig, resolveAgent } from "./local-agent" import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant" import { useSDK } from "./sdk" import { useSync } from "./sync" @@ -67,7 +67,19 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const settings = useSettings() const id = createMemo(() => params.id || undefined) - const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden)) + // #858 — the fixed picker order plan → develop → research: the config's + // `agent_order` (fork PR #305's field, written by the extension) is the + // PRIMARY sort key, honored APP-SIDE per the overlay architecture — an + // engine build without the #305 sort still renders the product order. + // The SDK config type may predate the field; read it defensively, never + // guess. + const pickerConfig = () => { + const cfg = sync().data.config as { agent_order?: string[]; default_agent?: string } | undefined + return cfg && typeof cfg === "object" ? { agent_order: cfg.agent_order, default_agent: cfg.default_agent } : undefined + } + const list = createMemo(() => + orderByPickerConfig(sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden), pickerConfig()), + ) const agentsVisible = createMemo(() => settings.visibility.customAgents() || hasAgentChoice(list())) const connected = createMemo(() => new Set(providers.connected().map((item) => item.id))) diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts index ee705cb3..5d03072b 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts @@ -65,6 +65,7 @@ export const dict = { "command.agent.cycle.description": "التبديل إلى الوكيل التالي", "command.agent.cycle.reverse": "تغيير الوكيل للخلف", "command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق", + "agent.picker.implied": "ضمني", "command.model.variant.cycle": "تغيير جهد التفكير", "command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/br.ts b/packages/app-bundle/overlay/packages/app/src/i18n/br.ts index 48297f5a..35230c27 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/br.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/br.ts @@ -65,6 +65,7 @@ export const dict = { "command.agent.cycle.description": "Mudar para o próximo agente", "command.agent.cycle.reverse": "Alternar agente (reverso)", "command.agent.cycle.reverse.description": "Mudar para o agente anterior", + "agent.picker.implied": "implícito", "command.model.variant.cycle": "Alternar nível de raciocínio", "command.model.variant.cycle.description": "Mudar para o próximo nível de esforço", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts b/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts index a7860225..29e2fb54 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts @@ -71,6 +71,7 @@ export const dict = { "command.agent.cycle.description": "Prebaci na sljedećeg agenta", "command.agent.cycle.reverse": "Promijeni agenta unazad", "command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta", + "agent.picker.implied": "implicitno", "command.model.variant.cycle": "Promijeni nivo razmišljanja", "command.model.variant.cycle.description": "Prebaci na sljedeći nivo", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/da.ts b/packages/app-bundle/overlay/packages/app/src/i18n/da.ts index 78fbeb3e..6cf8bf76 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/da.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/da.ts @@ -71,6 +71,7 @@ export const dict = { "command.agent.cycle.description": "Skift til næste agent", "command.agent.cycle.reverse": "Skift agent baglæns", "command.agent.cycle.reverse.description": "Skift til forrige agent", + "agent.picker.implied": "implicit", "command.model.variant.cycle": "Skift tænkeindsats", "command.model.variant.cycle.description": "Skift til næste indsatsniveau", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/de.ts b/packages/app-bundle/overlay/packages/app/src/i18n/de.ts index a7c10843..6380c4cf 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/de.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/de.ts @@ -69,6 +69,7 @@ export const dict = { "command.agent.cycle.description": "Zum nächsten Agenten wechseln", "command.agent.cycle.reverse": "Agent rückwärts wechseln", "command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln", + "agent.picker.implied": "implizit", "command.model.variant.cycle": "Denkaufwand wechseln", "command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/en.ts b/packages/app-bundle/overlay/packages/app/src/i18n/en.ts index ad7ba6fe..ff768f95 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/en.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/en.ts @@ -71,6 +71,7 @@ export const dict = { "command.agent.cycle.description": "Switch to the next agent", "command.agent.cycle.reverse": "Cycle agent backwards", "command.agent.cycle.reverse.description": "Switch to the previous agent", + "agent.picker.implied": "implied", "command.model.variant.cycle": "Cycle thinking effort", "command.model.variant.cycle.description": "Switch to the next effort level", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/es.ts b/packages/app-bundle/overlay/packages/app/src/i18n/es.ts index e8fe8f30..6ee0bde7 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/es.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/es.ts @@ -71,6 +71,7 @@ export const dict = { "command.agent.cycle.description": "Cambiar al siguiente agente", "command.agent.cycle.reverse": "Alternar agente hacia atrás", "command.agent.cycle.reverse.description": "Cambiar al agente anterior", + "agent.picker.implied": "implícito", "command.model.variant.cycle": "Alternar esfuerzo de pensamiento", "command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts b/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts index 8c20be49..9613c4e8 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts @@ -65,6 +65,7 @@ export const dict = { "command.agent.cycle.description": "Passer à l'agent suivant", "command.agent.cycle.reverse": "Changer d'agent (inverse)", "command.agent.cycle.reverse.description": "Passer à l'agent précédent", + "agent.picker.implied": "implicite", "command.model.variant.cycle": "Changer l'effort de réflexion", "command.model.variant.cycle.description": "Passer au niveau d'effort suivant", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts index e9935d14..fff71a0f 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts @@ -65,6 +65,7 @@ export const dict = { "command.agent.cycle.description": "次のエージェントに切り替え", "command.agent.cycle.reverse": "エージェントを逆順に切り替え", "command.agent.cycle.reverse.description": "前のエージェントに切り替え", + "agent.picker.implied": "暗黙", "command.model.variant.cycle": "思考レベルの切り替え", "command.model.variant.cycle.description": "次の思考レベルに切り替え", "command.prompt.mode.shell": "シェル", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts index b1533e3a..22bb3bf3 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts @@ -61,6 +61,7 @@ export const dict = { "command.agent.cycle.description": "다음 에이전트로 전환", "command.agent.cycle.reverse": "에이전트 역순환", "command.agent.cycle.reverse.description": "이전 에이전트로 전환", + "agent.picker.implied": "암시됨", "command.model.variant.cycle": "생각 수준 순환", "command.model.variant.cycle.description": "다음 생각 수준으로 전환", "command.prompt.mode.shell": "셸", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/no.ts b/packages/app-bundle/overlay/packages/app/src/i18n/no.ts index 8b05e8ea..9b4a643f 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/no.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/no.ts @@ -70,6 +70,7 @@ export const dict = { "command.agent.cycle.description": "Bytt til neste agent", "command.agent.cycle.reverse": "Bytt agent bakover", "command.agent.cycle.reverse.description": "Bytt til forrige agent", + "agent.picker.implied": "implisitt", "command.model.variant.cycle": "Bytt tenkeinnsats", "command.model.variant.cycle.description": "Bytt til neste innsatsnivå", "command.prompt.mode.shell": "Shell", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts b/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts index dab56fbe..eb616f1c 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts @@ -65,6 +65,7 @@ export const dict = { "command.agent.cycle.description": "Przełącz na następnego agenta", "command.agent.cycle.reverse": "Przełącz agenta wstecz", "command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta", + "agent.picker.implied": "niejawny", "command.model.variant.cycle": "Przełącz wysiłek myślowy", "command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku", "command.prompt.mode.shell": "Terminal", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts index ebb0ab6a..bfd1a689 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts @@ -71,6 +71,7 @@ export const dict = { "command.agent.cycle.description": "Переключиться к следующему агенту", "command.agent.cycle.reverse": "Цикл агентов назад", "command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту", + "agent.picker.implied": "неявный", "command.model.variant.cycle": "Цикл режимов мышления", "command.model.variant.cycle.description": "Переключиться к следующему уровню усилий", "command.prompt.mode.shell": "Оболочка", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/th.ts b/packages/app-bundle/overlay/packages/app/src/i18n/th.ts index b0b3e500..fa5db609 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/th.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/th.ts @@ -71,6 +71,7 @@ export const dict = { "command.agent.cycle.description": "สลับไปยังเอเจนต์ถัดไป", "command.agent.cycle.reverse": "เปลี่ยนเอเจนต์ย้อนกลับ", "command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า", + "agent.picker.implied": "โดยนัย", "command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด", "command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป", "command.prompt.mode.shell": "เชลล์", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts b/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts index e56b09ea..ffdc4aeb 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts @@ -75,6 +75,7 @@ export const dict = { "command.agent.cycle.description": "Sonraki ajana geç", "command.agent.cycle.reverse": "Ajanı geri değiştir", "command.agent.cycle.reverse.description": "Önceki ajana geç", + "agent.picker.implied": "örtük", "command.model.variant.cycle": "Düşünme eforu değiştir", "command.model.variant.cycle.description": "Sonraki efor seviyesine geç", "command.prompt.mode.shell": "Kabuk", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts b/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts index ae8f3e1d..68a58172 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts @@ -71,6 +71,7 @@ export const dict = { "command.agent.cycle.description": "Перемкнути на наступного агента", "command.agent.cycle.reverse": "Перемкнути агента в зворотному напрямку", "command.agent.cycle.reverse.description": "Перемкнути на попереднього агента", + "agent.picker.implied": "неявний", "command.model.variant.cycle": "Перемкнути рівень мислення", "command.model.variant.cycle.description": "Перемкнути на наступний рівень зусилля", "command.prompt.mode.shell": "Команда", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts b/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts index 3fc4e14d..61f7841f 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts @@ -93,6 +93,7 @@ export const dict = { "command.agent.cycle.description": "切换到下一个智能体", "command.agent.cycle.reverse": "反向切换智能体", "command.agent.cycle.reverse.description": "切换到上一个智能体", + "agent.picker.implied": "隐式", "command.model.variant.cycle": "切换思考强度", "command.model.variant.cycle.description": "切换到下一个强度等级", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts b/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts index dd1fbeeb..b037132a 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts @@ -75,6 +75,7 @@ export const dict = { "command.agent.cycle.description": "切換到下一個代理程式", "command.agent.cycle.reverse": "反向循環代理程式", "command.agent.cycle.reverse.description": "切換到上一個代理程式", + "agent.picker.implied": "隱式", "command.model.variant.cycle": "循環思考強度", "command.model.variant.cycle.description": "切換到下一個強度等級", "command.prompt.mode.shell": "Shell",