docs(AGENTS.md): perform housekeeping on agent-facing docs (#2678)

This commit is contained in:
Makito
2026-09-28 01:24:04 +08:00
committed by GitHub
parent b4289d3401
commit 2feea5ad3d
19 changed files with 687 additions and 614 deletions
@@ -0,0 +1,39 @@
---
name: enforce-rules-for-pinia-synced
description: Enforce AIRI's rules for Pinia stores that use pinia-plugin-synced for cross-window synchronization. Use when adding `synced` to a Pinia store, changing a synced store's state or actions, or writing watchers, persistence, or tests for synchronized state in AIRI renderers.
---
# Enforce AIRI Pinia Synchronization Rules
Apply these rules to every Pinia store that uses `pinia-plugin-synced`.
## Synchronization Model
- Treat `pinia-plugin-synced` as snapshot replication and leader-routed RPC. It does not share Vue refs between renderers.
- Add `synced` only to stores that need cross-window ownership. Synchronize the smallest serializable source-of-truth state.
- `state: true` sends a full-store proposal after each local mutation. Keep transient and high-frequency state in an unsynchronized store.
- State, action arguments, and action results must support `structuredClone`.
- Keep computed values, query status, runtime clients, controllers, pending promises, and component state outside synchronized state.
## Watchers and Invariants
- Remote snapshots run local Vue watchers. A watcher on synchronized state must not write synchronized state directly.
- A watcher can call a synchronized action to enforce a leader-owned invariant. The watcher must await the action. The action must be idempotent because each renderer can observe the same snapshot.
- Enforce cross-field invariants inside explicit actions before the state commit. Do not repair replicated state with a watcher.
## Actions
- Every returned function in a setup store is a Pinia action. Use computed values or pure helpers for read-only projections.
- List only leader-owned side-effecting actions under `synced.actions`. These actions must be asynchronous, and callers must await them.
- Unlisted actions run in the caller renderer. Their mutations become full-state proposals when `state: true`.
## Persistence and Leadership
- Keep synchronization and persistence as separate boundaries. Give persisted synchronized state one explicit persistence owner.
- Do not add bidirectional persistence composables or storage-event listeners to synchronized state. Use explicit persistence commands.
- Set the leadership mode explicitly for every Electron renderer. Utility and minimal windows must use `follower-only`.
## Tests
- Add a multi-window regression test for synchronization changes. A remote snapshot must not produce a local synchronized-state proposal.
- If a watcher calls a synchronized action, verify that repeated calls converge without repeated side effects.
@@ -0,0 +1,4 @@
interface:
display_name: "Enforce Pinia Sync Rules"
short_description: "Apply AIRI cross-window Pinia synchronization standards"
default_prompt: "Use $enforce-rules-for-pinia-synced to change or review a synchronized Pinia store under AIRI's cross-window rules."
@@ -0,0 +1,154 @@
---
name: enforce-rules-for-typescript
description: Enforce AIRI's TypeScript and Vue source rules for imports, naming, comments, JSDoc, fallbacks, stateful and protocol code, and module design. Use when writing, refactoring, or reviewing TypeScript or Vue source code in the AIRI monorepo.
---
# Enforce AIRI TypeScript Rules
Apply these rules to every TypeScript and Vue source change in AIRI. The root `AGENTS.md` gives the core code rules. This skill gives the detailed rules.
## Imports and Types
- Import types from the module or package that owns the contract. Do not redeclare an external or public contract locally to use a narrower subset.
- Do not route type imports through local runtime assembly modules when the original side-effect-free type source is available.
- If Node-only and browser-only types mix in one import chain, move the type declarations into a neutral type file. Keep the runtime modules environment-specific.
- Do not import values from a module that has side effects only to get its types.
- If a wrong or missing export causes an error, trace the full import chain and side-effect chain first. Fix the package exports or the owning boundary. Do not add a local workaround import at the leaf.
- Before you fix an import or type error, examine the compilation behavior and the type declarations. Also examine the `exports` field in `package.json`, and the browser and Node entry points of the dependency.
- Keep JSON Schemas compliant with providers. Give an explicit `type: object` and the required fields. Do not use unbounded records.
## Naming
- Let module boundaries provide context. Do not repeat package, product, protocol, or transport names in symbols. The exception is a symbol that crosses a boundary where that context is otherwise lost.
- Name functions after domain operations, not implementation layers.
- Use nouns for resolved domain concepts. Use verbs for transformations and side effects.
- If a symbol needs several ownership qualifiers to be clear, reconsider the module boundary or introduce a clearer domain concept.
## Comments
- Write a comment only for information that the code cannot express clearly: intent, constraints, ownership, invariants, precedence, lifecycle, ordering, side effects, protocol shape, or non-obvious fallbacks.
- Do not add comments that only restate names, types, or visible operations.
- Treat a contract comment as an explanation of the relationship between a producer and its consumers.
- Explain why a value exists in the system before you explain how the code represents it.
- Describe the decision, behavior, or invariant that a value controls.
- If different values select different control-flow or UI paths, describe each observable outcome.
- If a value crosses a module or component boundary, identify the consumer and how it applies the value.
- Put representation details after behavior: units, coordinate systems, thresholds, clamps, and source API fields.
- Put background evidence after the contract: browser behavior, issue links, investigation history, and removal conditions.
- If the name, type, and surrounding code express the full contract, omit the comment.
- Put an implementation comment next to the branch, calculation, transition, or side effect that it explains.
- In calculation-heavy code, explain non-obvious coordinate systems, units, conversions, clamps, rounding, aggregation, and precedence. Put each explanation next to the related intermediate value or branch.
- Prefer clearer names, types, and structured state over comments that compensate for hidden or encoded concepts.
- When you move code, keep the accurate comments. Remove the comments that no longer describe current behavior.
- Write an investigation-heavy comment as short paragraphs. When useful, give the context and the observed failure. Then give the reason that the obvious fix is not sufficient. Give the chosen fix and its removal condition or references.
- Use these markers:
- `// TODO:` for follow-up work.
- `// REVIEW:` for a concern that needs another opinion.
- `// NOTICE:` for workarounds, magic values, external constraints, and other important non-obvious context. The root `AGENTS.md` gives the required format for a workaround.
## JSDoc
- Use JSDoc for public APIs, package-level exports, shared architectural boundaries, and non-trivial exported functions, classes, and types.
- In JSDoc, document only the contract details that the signature cannot express: assumptions, side effects, lifecycle, and return guarantees.
- Do not export a helper only to satisfy tests or documentation rules. Keep implementation helpers private unless production code reuses them.
- Do not add JSDoc to trivial helpers, local projections, or pass-through functions.
- Do not use fixed section templates that restate names and signatures. Prefer precise names and branch-local comments for implementation details.
- For exported test helpers or non-obvious reusable test fixtures, add `@example` when it clarifies the intended usage.
- Do not attach JSDoc or `@example` blocks to ordinary `describe`, `it`, or `expect*` calls.
- For exported interfaces and type aliases, keep the top-level JSDoc on what the type represents. Put detailed semantics on the related fields.
- Document generic parameters with `@param`. Add `@default` to every option that has a default value.
- Give each runner and CLI entry point `/** ... */` JSDoc with a clear ASCII call-stack diagram. Use `{@link ...}` references where applicable.
- For a server orchestrator, add a call-stack diagram only when it clarifies a stable architecture boundary. Do not add diagrams to shallow glue code.
- Use this format for the call-stack section:
```ts
/**
* ...
*
* Call stack:
*
* collectEvalEntries (../runner)
* -> {@link createRunnerSchedule}
* -> {@link createMatrixCombinations}
* -> {@link VievalScheduledTask}[]
*/
```
- Give `/** ... */` JSDoc with an `@example` to each exported normalizer, shared normalizer, and non-obvious local normalizer. This applies to normalizers of outputs, formats, file names, and values. It does not apply to config default normalization.
- In the example, show a representative input and output. Use this format:
```ts
/**
* Normalizes <target>.
*
* @example
* normalizeTarget('ExampleInput')
* // => 'example-output'
*/
```
## Fallbacks and Precedence
- If a fallback chain has more than two sources, make the precedence explicit.
- Some fallback sources represent different schema versions, compatibility behavior, specificity levels, or user and system overrides. For these sources, explain why each non-primary branch exists and why it has that priority.
- Do not use nested ternaries for a fallback chain when any branch is non-obvious. Use named intermediate variables or `if` / `else if` blocks, so that each comment can be next to its branch.
- Do not use a new object or array as a casual fallback. Expressions such as `value ?? {}`, `value ?? []`, `value || {}`, and `value || []` create a new reference each time.
- Never use an inline object or array fallback in a reactive getter, computed value, watcher source, or Pinia state projection. New references can cause false changes, watcher loops, and state broadcasts.
- If an immutable empty fallback is valid, reuse a stable module-level value. If consumers must not mutate the value, freeze it.
- Use `??` only when `null` and `undefined` mean that a value is missing. Use `||` only when `false`, `0`, and an empty string must also select the fallback.
- In non-trivial domain or protocol code, a fallback can return an empty string, stale value, cached value, default value, or ignored result. At that return or branch site, explain why the fallback is safe.
- The root `AGENTS.md` gives the rules for backward-compatibility fallbacks.
## Stateful and Protocol Code
- Some code implements a protocol, state machine, lifecycle, cache, request and response flow, event routing, watcher, session, cookie, or cleanup sequence. Document the state model of this code near the implementation.
- Distinguish these kinds of state in names or nearby comments: persisted configuration, discovered filesystem state, runtime-loaded state, cache state, session or cookie state, watcher state, and external side effects.
- Some methods look like state transitions, such as `setEnabled`, `load`, `unload`, `dispose`, `start`, `stop`, or `refresh`. If the owning type or module does not make it obvious, make clear which state each of these methods changes.
- When you match events or responses, document the correlation keys and isolation rules. Examples are `requestId`, `sessionId`, `ownerExtensionId`, `bindingId`, the route namespace, and the source window.
- Make each ignored event understandable in its event handler. A handler can ignore an event because of a route mismatch, owner mismatch, stale request ID, disposed lifecycle, or wrong source. Show that reason in code or in a named predicate.
- For a request and response flow, define or name the envelope shape near the producer and the consumer.
- Document what happens to pending requests on timeout, close, unload, dispose, and publish failure.
- When cleanup spans multiple owners, keep the order visible. Explain why the order is important.
- When you return a snapshot, fallback value, stale value, or cached value, document its freshness at the return site.
- For watchers, event listeners, and async background work, make ownership and shutdown behavior explicit. Tell what starts and stops the work, and whether duplicate starts are allowed. Tell what happens to in-flight work during unload or dispose.
## Module Design
- Prefer deep modules over shallow modules. A module must hide a meaningful decision: policy, persistence boundary, protocol or schema contract, scheduling semantics, model prompt contract, domain invariant, or lifecycle concern.
- Do not split code by execution order alone. A module boundary must represent a stable responsibility that a reader can understand without all of the sibling files.
- Keep cohesive domain flows together until there is proven pressure to split them. A cohesive module of 200 to 400 lines is better than several shallow modules that pass the same context or options to each other.
- Split a module only when the new module owns a distinct responsibility. Do not split a file only to reduce nesting or line count, or to create a test seam.
- Do not divide a module into sections with separators such as `========`. Use cohesive groups of private helpers.
- Before you create a new `createXService` or `XDependencies`, make sure that `X` adds value. Value means policy, validation, state, retry or error handling, an IO boundary, or a reusable abstraction. If `X` adds no value, keep it as a private helper or inline it.
- Do not create a pass-through service such as `createXService({ yService })` when `X` adds no policy, validation, state, or abstraction.
- Keep special cases near the branch that they affect.
- Some helpers manipulate encoded keys, ownership, filesystem paths, routes, or protocol-shaped data. Make the invariant of such a helper explicit in its structure, name, or nearby documentation.
- Keep reusable domain contracts and rendering or building logic in the package that owns the domain. Runtime entry points wire dependencies and call those boundaries. They do not inline large reusable contracts.
- Keep runtime entry points lean. Move heavy logic into services or modules.
- Prefer early returns, and keep functions simple. Limit nesting when it improves readability. Do not add pass-through helpers or shallow modules only to reduce indentation.
- The root `AGENTS.md` gives the rules for classes and dependency injection.
## Constants, Options, and Tables
- Do not move everything into constants. Keep a constant that is used one or two times near its usage. Usually, put it near the top of the file, after the imports. Add a `/** ... */` comment that tells why the constant exists.
- For configurable options with defaults, prefer the merge functions of `@moeru/std`. When possible, define the defaults as a documented object, not as separate standalone constants.
- For retry, backoff, and limit values, do not use one standalone constant for all of them.
- Do not use table-driven style too much. In many cases, keep the table array inline and map it directly with `.map(...)`.
## OS, Process, and Paths
- For non-obvious OS, exec, process, argument, networking, file, or directory handling, explain the constraint or purpose near the code.
- Do not hardcode Unix, macOS, or Windows path literals. Use path-safe array arguments and cross-platform handling.
## Readability Refactors
- A readability-only change must keep the runtime behavior. If the behavior changes, add focused tests and document the contract change.
## Review Checklist
When you review a complex TypeScript module, examine these points:
- Can a reader identify owned state, external side effects, lifecycle transitions, cleanup, and freshness without tracing several neighboring files?
- Are protocol envelopes, correlation keys, isolation rules, and fallback precedence explicit at their decision points?
- Do module and helper boundaries hide meaningful policy, or do they only forward context or hide special cases?
- Do comments explain non-obvious decisions next to the related code, without restating names, types, or visible operations?
@@ -0,0 +1,4 @@
interface:
display_name: "Enforce TypeScript Rules"
short_description: "Apply AIRI TypeScript and Vue source standards"
default_prompt: "Use $enforce-rules-for-typescript to write or review TypeScript and Vue code under AIRI's source standards."
@@ -22,7 +22,7 @@ Apply these rules to every affected UI file in AIRI.
- Do not use long inline class strings such as `class="px-2 py-1 flex items-center bg-white/50 dark:bg-black/50"`.
- Do not use attributify-style groups such as `px="2" py="1" flex="~ items-center" bg="white/50 dark:black/50"`.
- When touching legacy utility classes, progressively refactor them into readable grouped arrays.
- If a component that you change has legacy utility classes, refactor those classes into readable grouped arrays in the same change.
## Reuse Project Styling Infrastructure
@@ -1,6 +1,6 @@
---
name: enforce-rules-for-vitest
description: Enforce AIRI's testing and Vitest practices. Use when creating, editing, reviewing, or debugging tests; reproducing a reported bug or issue; changing Vitest configuration; mocking IPC, services, providers, platform APIs, or imports; or diagnosing test import and runtime-boundary failures in the AIRI monorepo.
description: Enforce AIRI's testing and Vitest practices in the AIRI monorepo. Use when you create, edit, review, or debug tests. Also use when you reproduce a reported bug or issue, change Vitest configuration, or diagnose test import and runtime-boundary failures. Also use when you mock IPC, services, providers, platform APIs, or imports.
---
# Enforce AIRI Vitest Rules
@@ -9,32 +9,50 @@ Apply these rules to every test change in AIRI.
## Choose the Test Scope
- Use the Vitest project that owns the affected code and keep runs targeted for speed.
- Use Vitest to verify the behavior of each module that you implement.
- Use the Vitest project that owns the affected code. Keep runs targeted for speed.
- Grow component and end-to-end coverage progressively. Prefer Vitest browser mode when the behavior depends on DOM or Web Platform APIs.
- Use the smallest automated test that faithfully exercises the behavior: prefer a unit test, then the smallest suitable higher-level test.
- Use the smallest automated test that faithfully exercises the behavior. Prefer a unit test, then the smallest suitable higher-level test.
- Do not rely on smoke-only tests.
## Reproduce Bugs Before Fixing Them
1. For an investigated bug or issue, try to add a test-only reproduction before changing production code.
1. For an investigated bug or issue, try to add a test-only reproduction before you change production code.
2. When reproduction is possible, include the tracker identifier in the test case name:
- Use `Issue #<number>` for a GitHub issue.
- Use the Linear issue key for an internal Linear bug.
3. Put the actual report URL in a comment directly above the regression test. Use the GitHub issue URL, Discord message or thread URL, or Linear issue URL as appropriate.
4. Confirm that the reproduction fails for the reported reason before implementing the fix.
4. Confirm that the reproduction fails for the reported reason before you implement the fix.
5. After the fix, add a comment below the report URL that explains the root cause and the fix. Use this format:
```ts
// ROOT CAUSE:
//
// If XXXX, some XXX case happens.
// This happens because where line ...
//
// <before-patch behavior/code>
//
// We fixed this by XXX, XXX, XXX.
// <after-patch behavior/code>
```
## Mock Real Boundaries
- Mock Electron IPC and Electron services with `vi.fn` or `vi.mock`; never require a real Electron runtime.
- For external providers and services, add mock-based tests and, when feasible, integration-style tests guarded by environment variables, but do not mock Pinia, Vue components. Vitest import mocks are allowed for these boundaries.
- Mock Electron IPC and Electron services with `vi.fn` or `vi.mock`. Never require a real Electron runtime.
- For external providers and services, add mock-based tests. When feasible, also add integration-style tests guarded by environment variables. Vitest import mocks are allowed for these boundaries.
- Do not mock Pinia or Vue components.
- Test through stable public behavior. Do not create new exports, dependency bags, or wrapper services only to make private implementation details mockable.
- Assert observable behavior, including mock calls and parameters, with explicit `expect` statements.
- Prefer one assertion per line so failures remain readable.
- Prefer one assertion per line so that failures stay readable.
## Preserve Runtime Integrity
- Do not test impossible runtime states. Avoid assertions against constants that cannot change or object mutations that can only occur inside the same test setup.
- Do not test impossible runtime states. Do not assert against constants that cannot change. Do not assert against object mutations that can only occur inside the same test setup.
- Do not replace `globalThis` properties or built-in modules with direct `Object.defineProperty(...)` mocks.
- When behavior depends on a different Node global or built-in state, use `node:worker_threads` to load an isolated worker or build a minimal CLI reproduction.
- For DOM and Web Platform APIs, use Vitest browser mode instead of hard-mocking platform internals. Progressively refactor existing direct platform mocks when touched.
- When behavior depends on a different Node global or built-in state, use `node:worker_threads` to load an isolated worker. You can also build a minimal CLI reproduction.
- For DOM and Web Platform APIs, use Vitest browser mode instead of hard-mocking platform internals.
- If a test that you change hard-mocks a platform API, move that test to browser mode in the same change.
## Fix Import Boundaries, Not Tests
@@ -42,10 +60,10 @@ Never use Vitest mocks, hoisting, dynamic imports, `as unknown as`, or test-only
If a test cannot import a module, investigate and fix the production boundary:
- package exports and declarations;
- import-time side effects;
- mixed Node and browser type dependencies;
- circular imports;
- an incorrect public module shape.
- package exports and declarations
- import-time side effects
- mixed Node and browser type dependencies
- circular imports
- an incorrect public module shape
Keep the test importing the same supported boundary that production consumers use.
+1 -1
View File
@@ -6,7 +6,7 @@ This is the `moeru-ai/airi` pnpm monorepo. Prefer the smallest safe change and k
- Follow the root `AGENTS.md` instructions.
- Do not modify unrelated files.
- Do not mix documentation, agent configuration, and runtime logic changes in one patch.
- Do not mix agent configuration changes and runtime logic changes in one patch. Put documentation that describes the changed code in the same patch.
- Do not change package files, lockfiles, or workspace configuration unless the task explicitly requires it.
- Preserve existing test style and helpers.
- Do not claim tests passed unless you actually ran them.
+132 -327
View File
@@ -1,285 +1,86 @@
# Project AIRI Agent Guide
Concise but detailed reference for contributors working across the `moeru-ai/airi` monorepo. Improve code when you touch it; avoid one-off patterns.
This file gives the core rules for all work in the `moeru-ai/airi` monorepo. Skills and nested `AGENTS.md` files give the rules for specific tasks. Before you start a task, find it in [Task Routing](#task-routing) and read the listed file.
## Tech Stack (by surface)
## Project Map
- **Desktop (stage-tamagotchi)**: Electron, Vue, Vite, TypeScript, Pinia, VueUse, Eventa (IPC/RPC), UnoCSS, Vitest, ESLint.
- **Web (stage-web)**: Vue 3 + Vue Router, Vite, TypeScript, Pinia, VueUse, UnoCSS, Vitest, ESLint. Backend: WIP.
- **Mobile (stage-pocket)**: Vue 3 + Vue Router, Vite, TypeScript, Pinia, VueUse, UnoCSS, Vitest, ESLint, Kotlin, Swift, Capacitor.
- **UI/Shared Packages**:
- `packages/stage-ui`: Core business components, composables, stores shared by stage-web & stage-tamagotchi (heart of stage work).
- `packages/stage-ui-three`: Three.js bindings + Vue components.
- `packages/stage-ui-pixi`: Planned Pixi bindings.
- `packages/stage-shared`: Shared logic across stage-ui, stage-ui-three, stage-web, stage-tamagotchi.
- `packages/ui`: Standardized primitives (inputs, textarea, buttons, layout) built on reka-ui; minimal business logic.
- `packages/i18n`: Central translations.
- Server channel: `packages/server-runtime`, `packages/server-sdk`, `packages/server-shared` (power `services/` and `plugins/`).
- Legacy: `crates/` (old Tauri desktop; current desktop is Electron).
All apps use Vue 3, Vite, TypeScript, Pinia, VueUse, UnoCSS, Vitest, and ESLint.
## Structure & Responsibilities
| Path | Content |
| --- | --- |
| `apps/stage-web` | The web app. It uses Vue Router. |
| `apps/stage-tamagotchi` | The Electron desktop app. |
| `apps/stage-pocket` | The mobile app. It uses Vue Router, Capacitor, Kotlin, and Swift. |
| `packages/stage-ui` | Business components, composables, and stores that stage-web and stage-tamagotchi share. |
| `packages/stage-ui-three` | Three.js bindings and Vue components. |
| `packages/stage-shared` | Logic that the stage packages and the stage apps share. |
| `packages/stage-pages` | Shared page bases. |
| `packages/stage-layouts` | Shared layouts, including the stage-web settings layout. |
| `packages/ui` | Primitives built on reka-ui, such as inputs, buttons, and layout. They have minimal business logic. |
| `packages/i18n` | All translations. |
| `packages/server-runtime`, `packages/server-sdk`, `packages/server-shared` | The server channel that `services/` and `plugins/` use. |
| `server/apps/api` | The hosted Hono resource API and its business domains. |
| `server/apps/auth` | The standalone Better Auth and OIDC service. |
| `server/packages` | Backend-only schema and Node infrastructure packages. |
| `server/dev/caddy` | Local Auth and API edge routing. |
| `server/docker-compose.yaml` | The complete local backend stack. |
- **Hosted backend** (`server/`)
- `server/apps/api`: Hono resource API and business domains.
- `server/apps/auth`: standalone Better Auth and OIDC service.
- `server/packages`: backend-private schema and Node infrastructure packages.
- `server/dev/caddy`: local-only Auth/API edge routing.
- `server/docker-compose.yaml`: complete local backend stack.
- **Apps**
- `apps/stage-web`: Web app; composables/stores in `src/composables`, `src/stores`; pages in `src/pages`; devtools in `src/pages/devtools`; router config via `vite.config.ts`.
- `apps/stage-tamagotchi`: Electron app; renderer pages in `src/renderer/pages`; devtools in `src/renderer/pages/devtools`; settings layout at `src/renderer/layouts/settings.vue`; router config via `electron.vite.config.ts`.
- Settings/devtools routes rely on `<route lang="yaml"> meta: layout: settings </route>`; ensure routes/icons are registered accordingly (`apps/stage-tamagotchi/src/renderer/layouts/settings.vue`, `apps/stage-web/src/layouts/settings.vue`).
- Shared page bases: `packages/stage-pages`.
- Stage pages: `apps/stage-web/src/pages`, `apps/stage-tamagotchi/src/renderer/pages` (plus devtools folders).
- **Stage UI internals** (`packages/stage-ui/src`)
- Providers: `stores/providers.ts` and `stores/providers/` (standardized provider definitions).
- Modules: `stores/modules/` (AIRI orchestration building blocks).
- Composables: `composables/` (business-oriented Vue helpers).
- Components: `components/`; scenarios in `components/scenarios/` for page/use-case-specific pieces.
- Stories: `packages/stage-ui/stories`, `packages/stage-ui/histoire.config.ts` (e.g. `components/misc/Button.story.vue`).
- **IPC/Eventa**: Always use `@moeru/eventa` for type-safe, framework/runtime-agnostic IPC/RPC. Define contracts centrally (e.g., `apps/stage-tamagotchi/src/shared`) and follow usage patterns in `apps/stage-tamagotchi/src/main/services/electron` for main/renderer integration.
- **Dependency Injection**: Use `injeca` for services/electron modules/plugins/frontend; see `apps/stage-tamagotchi/src/main/index.ts` for composition patterns.
- **Build/CI/Lint**: `.github/workflows` for pipelines; `eslint.config.js` for lint rules.
- **Styles**: UnoCSS config at `uno.config.ts`; check `apps/stage-web/src/styles` for existing animations; prefer UnoCSS over Tailwind.
Key files and directories:
## Key Path Index (what lives where)
- Pages are in `apps/stage-web/src/pages` and `apps/stage-tamagotchi/src/renderer/pages`. Devtools pages are in the `devtools` folder of each.
- The router configuration is in `apps/stage-web/vite.config.ts` and `apps/stage-tamagotchi/electron.vite.config.ts`.
- The settings layouts are `packages/stage-layouts/src/layouts/settings.vue` (stage-web) and `apps/stage-tamagotchi/src/renderer/layouts/settings.vue`.
- A settings or devtools page needs `<route lang="yaml"> meta: layout: settings </route>`. Register its route and icon in the settings layout.
- In `packages/stage-ui/src`, provider definitions are in `stores/providers/` and orchestration modules are in `stores/modules/`.
- Business composables are in `packages/stage-ui/src/composables/`. Business components are in `packages/stage-ui/src/components/`.
- Put a component for one page or one use case in `packages/stage-ui/src/components/scenarios/`.
- Stories are in `packages/stage-ui/stories`, with `packages/stage-ui/histoire.config.ts`. An example is `packages/stage-ui/src/components/misc/Button.story.vue`.
- Eventa contracts are in `apps/stage-tamagotchi/src/shared`. Main and renderer usage examples are in `apps/stage-tamagotchi/src/main/services/electron`.
- The dependency injection example is `apps/stage-tamagotchi/src/main/index.ts`.
- Style configuration is in `uno.config.ts`. Existing animations are in `apps/stage-web/src/styles`.
- CI pipelines are in `.github/workflows`. Lint rules are in `eslint.config.ts`.
- `packages/stage-ui`: Core stage business components/composables/stores.
- `src/stores/providers.ts` and `src/stores/providers/`: provider definitions (standardized).
- `src/stores/modules/`: AIRI orchestration modules.
- `src/composables/`: reusable Vue composables (business-oriented).
- `src/components/`: business components; `src/components/scenarios/` for page/use-case-specific pieces.
- Stories: `packages/stage-ui/stories`, `packages/stage-ui/histoire.config.ts` (e.g. `components/misc/Button.story.vue`).
- `packages/stage-ui-three`: Three.js bindings + Vue components.
- `packages/stage-ui-pixi`: Planned Pixi bindings.
- `packages/stage-shared`: Shared logic across stage-ui, stage-ui-three, stage-web, stage-tamagotchi.
- `packages/ui`: Standardized primitives (inputs/textarea/buttons/layout) built on reka-ui.
- `packages/i18n`: All translations.
- Hosted backend: `server/apps/api`, `server/apps/auth`, `server/packages`, and local tooling under `server/dev`.
- Server channel: `packages/server-runtime`, `packages/server-sdk`, `packages/server-shared` (power `services/` and `plugins/`).
- Legacy desktop: `crates/` (old Tauri; Electron is current).
- Pages: `packages/stage-pages` (shared bases); `apps/stage-web/src/pages` and `apps/stage-tamagotchi/src/renderer/pages` for app-specific pages; devtools live in each app’s `.../pages/devtools`.
- Router configs: `apps/stage-web/vite.config.ts`, `apps/stage-tamagotchi/electron.vite.config.ts`.
- Devtools/layouts: `apps/stage-tamagotchi/src/renderer/layouts/settings.vue`, `apps/stage-web/src/layouts/settings.vue`.
- IPC/Eventa contracts/examples: `apps/stage-tamagotchi/src/shared`, `apps/stage-tamagotchi/src/main/services/electron`.
- DI examples: `apps/stage-tamagotchi/src/main/index.ts` (injeca).
- Styles: `uno.config.ts` (UnoCSS), `apps/stage-web/src/styles` (animations/reference).
- Build pipeline refs: `.github/workflows`; lint rules in `eslint.config.js`.
- Documented solutions: `docs/solutions/` records past fixes and workflow learnings, organized by category with YAML frontmatter (`module`, `tags`, `problem_type`); relevant when implementing, debugging, or verifying in documented areas.
- Tailwind/UnoCSS: prefer UnoCSS; if standardizing styles, add shortcuts/rules/plugins in `uno.config.ts`.
## Commands
## Commands (pnpm with filters)
Use pnpm workspace filters to limit a command to one workspace. Replace `<package name>` with the `name` field in `package.json`, for example `@proj-airi/stage-tamagotchi`.
> Use pnpm workspace filters to scope tasks. Examples below are generic; replace the filter with the target workspace name (e.g. `@proj-airi/stage-tamagotchi`, `@proj-airi/stage-web`, `@proj-airi/stage-ui`, etc.).
- Typecheck one workspace: `pnpm -F <package name> typecheck`. For stage-tamagotchi, this command runs `tsc` and `vue-tsc`.
- Test one file: `pnpm exec vitest run <path>`.
- Test one workspace: `pnpm -F <package name> exec vitest run`.
- Test all projects: `pnpm test:run`. If Vitest finds no tests, examine the `include` patterns in `vitest.config.ts`.
- Lint: `pnpm lint`. ESLint also does the formatting. To fix lint and format errors, run `pnpm lint:fix`.
- Build one workspace: `pnpm -F <package name> build`. For stage-tamagotchi, this command runs the typecheck and `electron-vite build`.
- **Typecheck**
- `pnpm -F <package.json name> typecheck`
- Example: `pnpm -F @proj-airi/stage-tamagotchi typecheck` (runs `tsc` + `vue-tsc`).
- **Unit tests (Vitest)**
- Targeted: `pnpm exec vitest run <path/to/file>`
e.g. `pnpm exec vitest run apps/stage-tamagotchi/src/renderer/stores/tools/builtin/widgets.test.ts`
- Workspace: `pnpm -F <package.json name> exec vitest run`
e.g. `pnpm -F @proj-airi/stage-tamagotchi exec vitest run`
- Root `pnpm test:run`: runs all tests across registered projects. If no tests are found, check `vitest.config.ts` include patterns.
- Root `vitest.config.ts` includes `apps/stage-tamagotchi` and other projects; each app/package can have its own `vitest.config`.
- **Lint**
- `pnpm lint` and `pnpm lint:fix`
- Formatting is handled via ESLint; `pnpm lint:fix` applies formatting.
- **Build**
- `pnpm -F <package.json name> build`
- Example: `pnpm -F @proj-airi/stage-tamagotchi build` (typecheck + electron-vite build).
## Scope and Checks
## Before You Start
- Keep each change inside the scope of the task.
- In each function, component, or test that you change, correct the code that does not obey a current rule. Do not change other code only to improve it.
- Do not add one-off patterns. Before you write a new utility or function, search for an existing internal implementation.
- If new logic can become a shared utility, propose the shared approach to the user.
- If a refactor is small, do it step by step.
- After a task, run the typecheck and the related tests for each workspace that you changed.
- If you change a shared package or an exported type, also run the root `pnpm typecheck`.
- Run `pnpm lint` before you finish.
- Report only the checks that you ran. Give each command and its result.
- At the end, summarize the changes, the commands that you ran, and the follow-up work.
## Architecture decisions
- Put project-wide and client ADRs in `docs/ai/adr/`.
- Put hosted backend ADRs in `server/docs/ai/adr/`.
- Both paths are relative to the repository root.
- Keep ADRs in this repository and include them in the related commits and pull requests.
## Enforced Repository Skills
- For testing, Vitest, regression reproduction, mocks, or test import-boundary work, always use [`enforce-rules-for-vitest` skill](.agents/skills/enforce-rules-for-vitest/SKILL.md).
- For UnoCSS, Vue styling, UI components, animations, icons, or color-mode work, always use [`enforce-rules-for-unocss` skill](.agents/skills/enforce-rules-for-unocss/SKILL.md).
- For web or Electron workflows that upload a local file through an HTML input, a dynamically created input, or a file chooser, invoke [`$use-agent-browser-with-input-file`](.agents/skills/use-agent-browser-with-input-file/SKILL.md). Also invoke `$agent-browser`, and invoke `$agent-browser-electron` when the target is Electron.
- For AIRI Live2D, VRM, or MMD import and rendering tests across stage-web, stage-tamagotchi, or stage-pocket, invoke [`$use-agent-browser-for-airi`](.agents/skills/use-agent-browser-for-airi/SKILL.md). It invokes `$use-agent-browser-with-input-file` for the upload mechanism and adds AIRI-specific routes, state preparation, format behavior, and renderer verification.
- For editing, writing, refactoring, re-writing code, submitting issues, Pull Requests, and docs, comments, invoke [`$simple-english`](./agents/skills/simple-english/SKILL.md).
## Development Practices
- Favor clear module boundaries; shared logic goes in `packages/`.
- Keep runtime entrypoints lean; move heavy logic into services/modules.
- Use Valibot for schema validation; keep schemas close to their consumers.
- Use Eventa (`@moeru/eventa`) for structured IPC/RPC contracts where needed.
- Use `errorMessageFrom(error)` from `@moeru/std` to extract error messages instead of manual patterns like `error instanceof Error ? error.message : String(error)`. Pair with `?? 'fallback'` when a default is needed.
- Do not add backward-compatibility guards. If extended support is required, write refactor docs and spin up another Codex or Claude Code instance via shell command to complete the implementation with clear instructions and the expected post-refactor shape.
- If the refactor scope is small, do a progressive refactor step by step.
- For new feature requirements or requirement-related tasks involving `node:*` built-in modules, DOM operations, Vue composables, React hooks, Vite plugins, or GitHub Actions workflows, always do deep research for suitable existing libraries or open source modules first. Before choosing any library, always ask the user to choose and help judge which option is right. Never choose generalized utility libraries on your own (for example, `es-toolkit`, utilities from `github.com/unjs`, or tiny tools from `github.com/tinylib`) without explicit user confirmation. If the user is working spec-driven, list candidate choices in a clear and concise Markdown comparison table.
## TypeScript / IPC / Tools
- Keep JSON Schemas provider-compliant (explicit `type: object`, required fields; avoid unbounded records).
- For Electron, and backend related packages, use `injeca` for dependency management; avoid new class hierarchies unless extending browser APIs (classes are harder to mock/test).
- Centralize Eventa contracts; use `@moeru/eventa` for all events.
- Import types from the module or package that owns the contract. Do not redeclare external/public contracts locally just to use a narrower subset, and do not route type imports through local runtime assembly modules when the original side-effect-free type source is available.
- Omit TypeScript and JavaScript source extensions from relative imports, dynamic imports, and re-exports. Write `./module` instead of `./module.ts` or `./module.js`; keep extensions only when the runtime or asset format requires them.
- Do not directly modify or override `tsconfig.json` to make an import/type error disappear. First investigate compilation behavior, `package.json` `exports` declarations, type declarations, and whether the dependency exposes the intended browser/node entrypoints.
- When Node-only and browser-only types are mixed through one import chain, split the type declarations into a neutral type file and keep runtime modules environment-specific. Avoid importing values from modules that carry side effects just to obtain types.
- If a wrong export or missing export causes an error, trace the full import chain and side-effect chain before changing imports at the leaf. Prefer fixing package/module exports and the owning boundary over adding local workaround imports.
- Treat circular imports as a design problem. If a cycle appears, first reconsider ownership, module boundaries, and whether shared types or pure helpers need to move. If the cycle cannot be resolved confidently, ask the user for direction before continuing.
- When a user asks to use a specific tool or dependency, first check Context7 docs with the search tool, then inspect actual usage of the dependency in this repo.
- If multiple names are returned from Context7 without a clear distinction, ask the user to choose or confirm the desired one.
- If docs conflict with typecheck results, inspect the dependency source under `node_modules` to diagnose root cause and fix types/bugs.
## i18n
- Add/modify translations in `packages/i18n`; avoid scattering i18n across apps/packages.
- By default, modify only the English source locale and the locale used by the developer working on the change. Other locale files are managed through the Crowdin integration; direct local edits may be replaced by untranslated source content after the next Crowdin upload or sync. Avoid editing other locales unless explicitly requested.
### Glossary
`packages/i18n/glossary/terms.yaml` gives the approved English term for each product concept.
`pnpm -F @proj-airi/i18n glossary:build` writes the TBX file that Crowdin imports. `schema.ts`
documents each field.
- Read `terms.yaml` before you write or change a string that a user sees. Use the term it gives.
- Do not edit `packages/i18n/glossary/translations/`. The next Crowdin download replaces it.
- Take terms from the interface. Search the documentation to confirm a term, not to find one.
**Add a term when it prevents one of two failures:**
- A translator who reads only the source string chooses the wrong word.
- Two translators choose different words, and both words are correct.
**Do not add a term when:**
- It is code. A translated control token, JSON key, file name, or package name breaks the
application, and the application shows no error.
- It is a common English word with its usual meaning, such as "Speed" or "Volume".
- Its parts each have an entry and it says no more than its parts, such as "VRM model".
- One feature uses it, and only two translators could disagree about it. But keep a rare term if a
translator can get it wrong: `Tachie` occurs two times and is the strongest entry in the file.
`terms.yaml` holds no rationale, because each field must map to a TBX element. Give the reason in
the pull request.
**Write a definition in one sentence.** Describe the thing, not the word, and put a negative rule in
the `note`. Keep each sentence short and active: translators read them, and most do not read English
as a first language.
## Readability, Naming, and Comments
### Naming
## Code Rules
- Put shared logic in `packages/`. Keep module boundaries clear.
- Use kebab-case for all file names.
- Let module boundaries provide context. Avoid repeating package, product, protocol, or transport names in symbols unless the symbol crosses a boundary where that context would otherwise be lost.
- Name functions after domain operations rather than implementation layers.
- Use nouns for resolved domain concepts and verbs for transformations or side effects.
- If a symbol needs several ownership qualifiers to be understandable, reconsider the module boundary or introduce a clearer domain concept.
- Do not use `any`. Prefer type generics. Use `as unknown as <Type>` only when you cannot fix the type safely.
- Omit TypeScript and JavaScript source extensions in relative imports, dynamic imports, and re-exports. Write `./module`, not `./module.ts` or `./module.js`. Keep an extension only when the runtime or asset format requires it.
- Do not edit `tsconfig.json` to hide an import or type error. Find and fix the cause.
- Treat a circular import as a design problem. First reconsider ownership and module boundaries. If you cannot resolve the cycle confidently, ask the user.
- Use `@moeru/eventa` for all IPC, RPC, and cross-process events. Define the contracts in one central place, such as `apps/stage-tamagotchi/src/shared`.
- Use dependency injection only at external boundaries: databases, model runtimes, queues, caches, filesystems, networks, clocks, environments, and feature gates. Use `injeca` for it.
- Do not add dependency objects for internal functions that only call sibling helpers or forward parameters.
- Prefer classes for runtime or browser APIs. Also prefer classes for substantial business modules that own state, lifecycle, or a stable domain boundary.
- Prefer functions for pure transformations and local helpers.
- Use Valibot for schema validation. Keep each schema near its consumers.
- Use `errorMessageFrom(error)` from `@moeru/std` to get an error message. Add `?? 'fallback'` when you need a default. For other error handling, prefer the `@moeru/std` patterns.
- Mark every workaround with a `// NOTICE:` comment in this format:
### Comments
- Comments should explain information the code cannot express clearly: intent, constraints, ownership, invariants, precedence, lifecycle, ordering, side effects, protocol shape, or non-obvious fallbacks.
- Do not add comments that only restate names, types, or visible operations.
- Treat a contract comment as an explanation of the relationship between a producer and its consumers.
- Explain why a value exists in the system before you explain how the code represents it.
- Describe the decision, behavior, or invariant that a value controls.
- If different values select different control-flow or UI paths, describe each observable outcome.
- When a value crosses a module or component boundary, identify the consumer and how it applies the value.
- Put representation details after behavior: units, coordinate systems, thresholds, clamps, and source API fields.
- Put background evidence after the contract: browser behavior, issue links, investigation history, and removal conditions.
- If the name, type, and surrounding code express the full contract, omit the comment.
- Place implementation comments next to the branch, calculation, transition, or side effect they explain.
- For calculation-heavy code, explain non-obvious coordinate systems, units, conversions, clamps, rounding, aggregation, and precedence beside the relevant intermediate values or branches.
- Prefer clearer names, types, and structured state over comments that compensate for hidden or encoded concepts.
- Keep accurate comments when moving code and remove comments that no longer describe current behavior.
- Format investigation-heavy comments as short paragraphs. When useful, cover context, observed failure, why the obvious fix is insufficient, the chosen fix, and its removal condition or references.
- Use markers:
- `// TODO:` for follow-up work.
- `// REVIEW:` for concerns that need another opinion.
- `// NOTICE:` for workarounds, magic values, external constraints, and other important non-obvious context.
### Fallbacks and Precedence
- Any fallback chain with more than two sources must make precedence explicit.
- If fallback sources represent different schema versions, compatibility behavior, specificity levels, or user/system overrides, each non-primary branch must explain why that case exists and why it has that priority.
- Avoid nested ternaries for fallback chains when any branch is non-obvious. Use named intermediate variables or `if` / `else if` blocks so comments can live next to the relevant branch.
- Do not use a new object or array as a casual fallback. Expressions such as `value ?? {}`, `value ?? []`, `value || {}`, and `value || []` create a new reference each time.
- Never use an inline object or array fallback in a reactive getter, computed value, watcher source, or Pinia state projection. New references can cause false changes, watcher loops, and state broadcasts.
- If an immutable empty fallback is valid, reuse a stable module-level value. Freeze the value when consumers must not mutate it.
- Use `??` only when `null` and `undefined` mean that a value is missing. Use `||` only when `false`, `0`, and an empty string must also select the fallback.
- Do not keep backward-compatibility fallbacks silently. If a fallback is temporary, mark it with `// NOTICE:` and include the removal condition. If it is permanent, document it as supported policy instead of calling it legacy.
- If a fallback returns an empty string, stale value, cached value, default value, or ignored result in non-trivial domain/protocol code, explain why that fallback is safe at the return or branch site.
### Stateful and Protocol Code
- For code that implements a protocol, state machine, lifecycle, cache, request/response flow, event routing, watcher, session, cookie, or cleanup sequence, document the state model near the implementation.
- Distinguish persisted configuration, discovered filesystem state, runtime-loaded state, cache state, session/cookie state, watcher state, and external side effects in names or nearby comments.
- Methods that look like state transitions, such as `setEnabled`, `load`, `unload`, `dispose`, `start`, `stop`, or `refresh`, should make clear which state they change when that is not obvious from the owning type/module.
- When matching events or responses, explicitly document the correlation keys and isolation rules, such as `requestId`, `sessionId`, `ownerExtensionId`, `bindingId`, route namespace, or source window.
- Event handlers must make ignored events understandable. If an event is ignored because of route mismatch, owner mismatch, stale request id, disposed lifecycle, or wrong source, the reason should be visible in code or captured in a named predicate.
- For request/response flows, define or name the envelope shape close to the producer and consumer.
- Document what happens to pending requests on timeout, close, unload, dispose, and publish failure.
- When cleanup spans multiple owners, keep the ordering visible and explain why the order matters.
- When returning a snapshot, fallback value, stale value, or cached value, document freshness semantics at the return site.
- For watchers, event listeners, and async background work, make ownership and shutdown behavior explicit: what starts and stops the work, whether duplicate starts are allowed, and what happens to in-flight work during unload or dispose.
### Pinia Cross-Window Synchronization
- Treat `pinia-plugin-synced` as snapshot replication and leader-routed RPC. It does not share Vue refs between renderers.
- Add `synced` only to stores that need cross-window ownership. Synchronize the smallest serializable source-of-truth state.
- `state: true` sends a full-store proposal after each local mutation. Keep transient and high-frequency state in an unsynchronized store.
- State, action arguments, and action results must support `structuredClone`.
- Keep computed values, query status, runtime clients, controllers, pending promises, and component state outside synchronized state.
- Remote snapshots run local Vue watchers. A watcher on synchronized state must not write synchronized state directly.
- A watcher can call a synchronized action to enforce a leader-owned invariant. The watcher must await the action. The action must be idempotent because each renderer can observe the same snapshot.
- Enforce cross-field invariants inside explicit actions before the state commit. Do not repair replicated state with a watcher.
- Every returned function in a setup store is a Pinia action. Use computed values or pure helpers for read-only projections.
- List only leader-owned side-effecting actions under `synced.actions`. These actions must be asynchronous, and callers must await them.
- Unlisted actions run in the caller renderer. Their mutations become full-state proposals when `state: true`.
- Keep synchronization and persistence as separate boundaries. Give persisted synchronized state one explicit persistence owner.
- Do not add bidirectional persistence composables or storage-event listeners to synchronized state. Use explicit persistence commands.
- Set the leadership mode explicitly for every Electron renderer. Utility and minimal windows must use `follower-only`.
- Add a multi-window regression test for synchronization changes. A remote snapshot must not produce a local synchronized-state proposal. If a watcher calls a synchronized action, verify that repeated calls converge without repeated side effects.
### Readability Refactors
- Readability-only changes should preserve runtime behavior. If behavior changes, add focused tests and document the contract change explicitly.
## Module Design
- Prefer deep modules over shallow modules. A module should hide a meaningful decision: policy, persistence boundary, protocol/schema contract, scheduling semantics, model prompt contract, domain invariant, or lifecycle concern.
- Do not split code by execution order alone. A module boundary should represent a stable responsibility that can be understood without reading all sibling files.
- Keep cohesive domain flows together until there is proven pressure to split. A 200-400 line cohesive module is preferable to several shallow modules that pass the same context/options through each other.
- Prefer classes for runtime or browser APIs and substantial business modules that own state, lifecycle, or a stable domain boundary. Prefer functions for pure transformations and local helpers.
- Use dependency injection only at external boundaries such as databases, model runtimes, queues, caches, filesystems, networks, clocks, environments, and feature gates. Do not introduce dependency objects for internal functions that only call sibling helpers or forward parameters.
- Before creating a new `createXService` or `XDependencies`, verify that `X` adds policy, validation, state, retry/error handling, IO boundary, or a reusable abstraction. If not, keep it as a private helper or inline it.
- Avoid pass-through services such as `createXService({ yService })` when `X` adds no meaningful policy, validation, state, or abstraction.
- Keep special cases close to the branch they affect. If a helper manipulates encoded keys, ownership, filesystem paths, routes, or protocol-shaped data, make that invariant explicit in its structure, name, or nearby documentation.
- Test through stable public behavior. Do not create new exports, dependency bags, or wrapper services only to make private implementation details mockable.
- Keep reusable domain contracts and rendering/building logic in the package that owns that domain. Runtime entrypoints should wire dependencies and call those boundaries instead of inlining large reusable contracts.
## PR / Workflow Tips
- When asked to create, open, publish, or prepare a pull request, always use the repo-local `create-pr` skill. For user-visible changes it orchestrates `use-vishot` and the matching runtime variant, then uploads before/after screenshots as GitHub user assets in the PR body.
- After you create a pull request, get its review threads, comments, and check status. If a review identifies a confirmed error, fix it, run focused checks, push the update, reply with evidence, and resolve the thread.
- Rebase pulls; branch naming `username/feat/short-name`; clear commit messages (gitmoji is prohibited).
- Summarize changes, how tested (commands), and follow-ups.
- Improve legacy you touch; avoid one-off patterns.
- Keep changes scoped; use workspace filters (`pnpm -F <package> <script>`).
- Maintain structured `README.md` documentation for each `packages/` and `apps/` entry, covering what it does, how to use it, when to use it, and when not to use it.
- Always run `pnpm typecheck` and `pnpm lint` after finishing a task.
- Use Conventional Commits for commit messages (e.g., `feat(<package name>): add runner reconnect backoff`).
- Before planning or writing new utilities/functions, always search for existing internal implementations first. If the logic could become shared utilities, proactively propose that shared approach to users and developers.
## TypeScript Coding Regulations
These guidelines apply to all TypeScript code across the monorepo:
- Do not create commits during implementation for this spec.
- For implemented modules, use Vitest whenever possible to verify behavior and passing tests.
- Every workaround must use this `// NOTICE:` format:
```ts
// NOTICE:
// Why this workaround is needed.
@@ -287,67 +88,71 @@ These guidelines apply to all TypeScript code across the monorepo:
// Source/context (file, issue, URL, or node_modules reference).
// Removal condition (when it can be safely deleted).
```
- Prefer type generics wherever possible. Do not use `any`. Only use `as unknown as <target expected type>` when avoiding it is nearly impossible and the type cannot be fixed safely.
- Use JSDoc for public APIs, package-level exports, shared architectural boundaries, and non-trivial exported functions, classes, and types. Document only contract details that the signature cannot express, such as assumptions, side effects, lifecycle, and return guarantees.
- Avoid exporting helper functions only to satisfy tests or documentation rules. Keep implementation helpers private unless production code reuses them.
- Do not add JSDoc to trivial helpers, local projections, or pass-through functions. Avoid fixed section templates that restate names and signatures; prefer precise names and branch-local comments for implementation details.
- For exported test helpers or non-obvious reusable test fixtures, include `@example` when it clarifies intended usage.
- Do not attach JSDoc or `@example` blocks to ordinary `describe`, `it`, or `expect*` calls.
- For exported interfaces and type aliases, keep the top-level JSDoc focused on what the type represents and put detailed semantics on the relevant fields. Document generic parameters with `@param`, and add `@default` to every option that has a default value.
- For runner and CLI entrypoints, `/** ... */` JSDoc is required and must include a clear ASCII call-stack diagram using `{@link ...}` references where applicable. For server orchestrators, add the call-stack diagram only when it clarifies a stable architecture boundary; do not add diagrams to shallow glue code.
- Use this call-stack section format in orchestrator/runner/CLI JSDoc:
```ts
/**
* ...
*
* Call stack:
*
* collectEvalEntries (../runner)
* -> {@link createRunnerSchedule}
* -> {@link createMatrixCombinations}
* -> {@link VievalScheduledTask}[]
*/
```
- For non-obvious OS, exec, process, argument, networking, file, or directory handling, explain the constraint or purpose near the relevant code.
- Prefer `es-toolkit` first when creating utilities.
- For error handling, prefer `@moeru/std` patterns whenever possible.
- For exported normalizers, shared normalizers, or non-obvious local normalizers that normalize outputs, formats, filenames, or values (excluding config default normalization), add `/** ... */` JSDoc with an `@example` showing representative input and output.
- Use this normalizer documentation format:
```ts
/**
* Normalizes <target>.
*
* @example
* normalizeTarget('ExampleInput')
* // => 'example-output'
*/
```
- Do not move everything into constants. One-time or two-time constants should remain near usage (typically near the top after imports) with clear `/** ... */` explaining why.
- For configurable options with defaults, prefer `@moeru/std` merge functions and define defaults as documented objects when possible, instead of broad standalone constants.
- For retry, backoff, and limit values, do not use one standalone constant to cover everything.
- Avoid hardcoded Unix/macOS/Windows path literals; prefer path-safe array arguments and cross-platform handling.
- For test cases, do not rely on smoke-only tests. Reproduce bugs/failures before patching, then keep comments explaining root cause and fix rationale.
- Use this root-cause block format in regression tests when relevant:
```ts
// ROOT CAUSE:
//
// If XXXX, some XXX case happens.
// This happens because where line ...
//
// <before-patch behavior/code>
//
// We fixed this by XXX, XXX, XXX.
// <after-patch behavior/code>
```
- Do not split modules into sections using separators like `========`; use cohesive private helper groups or split into modules only when the new module owns a distinct responsibility. Do not split files merely to reduce nesting, line count, or create test seams.
- Do not overuse table-driven style. In many cases, keep table arrays inline and map directly with `.map(...)`.
- Prefer early returns and keep functions simple. Limit nesting when it improves readability, but do not introduce pass-through helpers or shallow modules solely to reduce indentation.
## Readability Review Checklist
### Backward Compatibility
When reviewing complex TypeScript modules, check:
- Do not add backward-compatibility guards.
- If old callers or old data need extended support, write a refactor document with the target shape. Then start another Codex or Claude Code session from the shell to do the refactor.
- If a compatibility fallback must stay for a limited time, mark it with `// NOTICE:` and give its removal condition.
- If a compatibility fallback is permanent, document it as supported policy. Do not call it legacy.
- A nested `AGENTS.md` can override this rule for a public contract. It must name the root rule that it overrides.
- Are owned state, external side effects, lifecycle transitions, cleanup, and freshness semantics identifiable without tracing several neighboring files?
- Are protocol envelopes, correlation keys, isolation rules, and fallback precedence explicit at their decision points?
- Do module and helper boundaries hide meaningful policy rather than merely forwarding context or obscuring special cases?
- Do comments explain non-obvious decisions beside the relevant code without restating names, types, or visible operations?
### Dependencies
- Prefer `es-toolkit` for general utilities. Ask the user before you add another general utility library, such as a package from `unjs` or `tinylib`.
- Some features use `node:*` built-in modules, DOM operations, Vue composables, React hooks, Vite plugins, or GitHub Actions workflows. For these features, research existing libraries first.
- Before you choose a library, ask the user to choose. Help the user compare the options.
- If the user works spec-driven, list the candidate libraries in a Markdown comparison table.
- If the user asks you to use a specific tool or dependency, read its documentation in Context7 first. Then examine how this repository uses it.
- If Context7 returns several names without a clear difference, ask the user to choose one.
- If the documentation conflicts with the typecheck result, examine the dependency source in `node_modules` to find the cause.
## Documentation
- Put project-wide and client ADRs in `docs/ai/adr/`. Put hosted backend ADRs in `server/docs/ai/adr/`, and obey the ADR rules in `server/AGENTS.md`.
- Put a documentation change in the same commit or PR as the code that it describes. This rule applies to ADRs, `README.md` files, and component references.
- Put agent configuration changes in a separate commit or PR. Agent configuration includes `AGENTS.md`, `CLAUDE.md`, skills, and `.github/copilot-instructions.md`.
- Keep a structured `README.md` in each `packages/` and `apps/` entry. It tells what the entry does, how to use it, when to use it, and when not to use it.
- Put all translations in `packages/i18n`. By default, edit only the English source locale and your own locale.
- Do not edit other locales unless the user asks. Crowdin manages them, and the next Crowdin sync can replace local edits.
- When you add a nested `AGENTS.md`, add a `CLAUDE.md` next to it that contains only `@AGENTS.md`. The root `CLAUDE.md` stops Claude Code from reading a nested `AGENTS.md` without this file.
## Writing
- For text, obey the [`simple-english`](.agents/skills/simple-english/SKILL.md) skill. Text includes documentation, code comments, commit messages, and PR and issue text.
- Do not apply the skill to code, identifiers, commands, or quoted errors.
- This repository breaks these rules most often:
- Do not write "should", "may", "might", "could", or "would". Write "must" or "can", or state the fact.
- Do not use semicolons. Write two sentences.
- Write "for example" and "that is". Do not write "e.g.", "i.e.", or "etc.".
- Write an instruction in 20 words or fewer. Write a description in 25 words or fewer.
- Put a condition before its command.
## Git and Pull Requests
- Use Conventional Commits, for example `feat(<package name>): add runner reconnect backoff`. Do not use gitmoji.
- Name branches `username/feat/short-name`.
- Rebase when you pull.
- To create, open, publish, or prepare a pull request, use the [`create-pr`](.agents/skills/create-pr/SKILL.md) skill. It also covers visual evidence and review follow-up.
## Task Routing
Skills are in `.agents/skills/<name>/SKILL.md`. Read the listed file before you start the task.
| Task | Read |
| --- | --- |
| Write, refactor, or review TypeScript or Vue code | [`enforce-rules-for-typescript`](.agents/skills/enforce-rules-for-typescript/SKILL.md) |
| Write or debug tests, reproduce a bug, add mocks, or fix test import boundaries | [`enforce-rules-for-vitest`](.agents/skills/enforce-rules-for-vitest/SKILL.md) |
| Change UnoCSS, Vue styles, UI components, animations, icons, or color mode | [`enforce-rules-for-unocss`](.agents/skills/enforce-rules-for-unocss/SKILL.md) |
| Add `synced` to a Pinia store, or change a synced store | [`enforce-rules-for-pinia-synced`](.agents/skills/enforce-rules-for-pinia-synced/SKILL.md) |
| Write or change a string that users see, or a glossary term | [`packages/i18n/AGENTS.md`](packages/i18n/AGENTS.md) |
| Work under `server/` | [`server/AGENTS.md`](server/AGENTS.md). For `server/apps/api`, also read [`server/apps/api/AGENTS.md`](server/apps/api/AGENTS.md). |
| Write documentation, code comments, commit messages, or PR and issue text | [`simple-english`](.agents/skills/simple-english/SKILL.md) |
| Create or prepare a pull request | [`create-pr`](.agents/skills/create-pr/SKILL.md) |
| Upload a local file through a file input or a file chooser in a web or Electron app | [`use-agent-browser-with-input-file`](.agents/skills/use-agent-browser-with-input-file/SKILL.md) and [`agent-browser`](.agents/skills/agent-browser/SKILL.md). For Electron, also read [`agent-browser-electron`](.agents/skills/agent-browser-electron/SKILL.md). |
| Test Live2D, VRM, or MMD import and rendering in stage-web, stage-tamagotchi, or stage-pocket | [`use-agent-browser-for-airi`](.agents/skills/use-agent-browser-for-airi/SKILL.md) |
These directories also have their own `AGENTS.md`. Read it before you work in the directory:
- `packages/model-driver-mediapipe/`
- `services/computer-use-mcp/`
+34
View File
@@ -0,0 +1,34 @@
# i18n Guide
Scope: `packages/i18n/**`, and every string that a user sees. The root `AGENTS.md` gives the rules for which locale files to edit.
## Glossary
`packages/i18n/glossary/terms.yaml` gives the approved English term for each product concept.
`pnpm -F @proj-airi/i18n glossary:build` writes the TBX file that Crowdin imports.
`packages/i18n/glossary/schema.ts` documents each field.
- Read `terms.yaml` before you write or change a string that a user sees. Use the term it gives.
- Do not edit `packages/i18n/glossary/translations/`. The next Crowdin download replaces it.
- Take terms from the interface. Search the documentation to confirm a term, not to find one.
**Add a term when it prevents one of two failures:**
- A translator who reads only the source string chooses the wrong word.
- Two translators choose different words, and both words are correct.
**Do not add a term when:**
- It is code. A translated control token, JSON key, file name, or package name breaks the
application, and the application shows no error.
- It is a common English word with its usual meaning, such as "Speed" or "Volume".
- Its parts each have an entry and it says no more than its parts, such as "VRM model".
- One feature uses it, and only two translators can disagree about it. But keep a rare term if a
translator can get it wrong: `Tachie` occurs two times and is the strongest entry in the file.
`terms.yaml` holds no rationale, because each field must map to a TBX element. Give the reason in
the pull request.
**Write a definition in one sentence.** Describe the thing, not the word, and put a negative rule in
the `note`. Keep each sentence short and active: translators read them, and most do not read English
as a first language.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+1 -1
View File
@@ -4,7 +4,7 @@
# the editor marks each term and offers the approved translation.
#
# Every field maps to one TBX element and carries the TBX name. schema.ts holds the mapping.
# AGENTS.md, under "i18n / Glossary", says when a term belongs here and when it does not.
# packages/i18n/AGENTS.md, under "Glossary", says when a term belongs here and when it does not.
#
# This repository owns English. Crowdin owns the other 8 languages, and the translated terms
# arrive in packages/i18n/glossary/translations/ through a pull request.
+5 -5
View File
@@ -11,14 +11,14 @@ This package is an experimental single-person mocap pipeline for stage-web devto
## Style / Conventions
- Prefer functional programming (FP) and pure functions where practical.
- Use factory functions + closures for stateful modules (example: `createMocapEngine()`).
- Avoid classes unless extending browser APIs or required by external libraries.
- This package overrides the root `AGENTS.md` rule on classes. Use factory functions and closures for stateful modules (example: `createMocapEngine()`).
- Use a class only to extend a browser API, or when an external library requires one.
- Keep the backend boundary clean:
- Engine/scheduler should not import `@mediapipe/tasks-vision`.
- The engine and the scheduler must not import `@mediapipe/tasks-vision`.
- MediaPipe specifics live under `src/backends/`.
- Keep types stable and narrow:
- Stage consumers depend on `src/types.ts` as the contract.
- New fields should be optional and backwards compatible.
- This package overrides the root `AGENTS.md` rule on backward compatibility. New fields in `src/types.ts` must be optional, so that existing consumers keep working.
## Key Files
@@ -30,6 +30,6 @@ This package is an experimental single-person mocap pipeline for stage-web devto
## Performance Notes
- `detectForVideo()` is synchronous; avoid blocking UI:
- `detectForVideo()` is synchronous. Do not block the UI:
- Engine drops frames when backend reports `isBusy()`.
- Scheduler controls per-task rates (`hz`) to cap work.
@@ -0,0 +1 @@
@AGENTS.md
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+56
View File
@@ -0,0 +1,56 @@
# API Server Guide
Agent-facing guide for `server/apps/api`.
## Overview
Hono-based Node.js resource API. The sibling `server/apps/auth` workspace app owns Better Auth, OIDC, sessions, and account lifecycle; this package owns billing, chat sync, LLM gateway forwarding, and business observability. **Multi-instance deployed on Railway** — design all features assuming N>1 instances sharing the same Postgres and Redis.
## Deployment Model
- Hosted on **Railway**, multiple instances behind a load balancer.
- Independent applications: `server/apps/api/src/main.ts` and `server/apps/auth/src/main.ts`. There is no runtime role CLI and neither app imports the other service graph.
- The API has no background polling loops or fire-and-forget tasks — every business write happens inside the request thread.
- Stateless per-instance: no local state that matters across requests.
- Cross-instance coordination via Redis Pub/Sub (WebSocket broadcast). DB-level idempotency (`(userId, requestId)` partial unique index on `flux_transaction`) covers retries.
- Rate limiting is currently **in-memory** (not distributed) — keep this in mind when adding rate-sensitive features.
## Tech Stack
Hono, Drizzle ORM, PostgreSQL, Redis, Stripe, OpenTelemetry, Valibot, injeca (DI), tsx. Better Auth lives only in `server/apps/auth`.
## Commands
```sh
pnpm -F @proj-airi/api-server dev # dev with dotenvx (.env.local)
pnpm -F @proj-airi/api-server typecheck
pnpm -F @proj-airi/api-server exec vitest run # all server tests
pnpm exec vitest run server/apps/api/src/... # single test file
pnpm -F @proj-airi/api-server db:generate # drizzle-kit generate
pnpm -F @proj-airi/api-server db:push # drizzle-kit push
pnpm -F @proj-airi/auth-server auth:generate # better-auth → server/packages/auth-shared/src/schema.ts
```
Local observability is maintained in `proj-airi/airi-railway`; run its `otel/docker-compose.yaml` stack.
## Architecture Summary
**Entry & DI**: `server/apps/api/src/main.ts` → `src/server.ts` → `src/app.ts`; `server/apps/auth/src/main.ts` → `src/server.ts`. The workspace apps have separate package manifests, env schemas, Dockerfiles, and composition roots.
**Layering**:
- **Routes** (`src/routes/`): thin — param validation (Valibot), auth guards, error mapping. No business logic here.
- **Services** (`src/services/`): core business logic and DB transactions.
- **Schemas** (`src/schemas/`): Drizzle table definitions. Drizzle loads migrations from `drizzle/` at startup.
**Middleware chain** (`/api/*`): CORS → hono/logger → optional otel → sessionMiddleware → bodyLimit(1MB) → per-route guards. WebSocket `/ws/chat` registered before bodyLimit.
**Error model**: `ApiError(statusCode, errorCode, message, details)` in `src/utils/error.ts`.
## Key Design Decisions
- **Flux read/write separation**: `FluxService` reads (Redis cache-aside), `BillingService` writes (single Postgres tx that mutates `user_flux` and writes the matching `flux_transaction` ledger row). Never put write-balance logic in `flux.ts`.
- **No async billing pipeline**: debits and credits update balance + ledger in one transaction. The `(user_id, request_id)` partial unique index gives DB-level idempotency for retries; LLM `request log` rows are written best-effort right after the response is delivered.
- **In-process LLM/TTS router**: `/api/v1/openai` is dispatched by `services/domain/llm-router` reading `LLM_ROUTER_CONFIG` (per-model upstream chain + envelope-encrypted keys). `chat/completions` walks LLM upstreams with key fallback; `audio/speech` delegates to a TTS adapter (`azure` / `dashscope-cosyvoice` / `volcengine`); `audio/voices` returns the adapter's compiled-in catalog. Server handles auth/billing/logging, not model execution.
- **Redis is cache + pub/sub, not truth**: balance cache, app_settings read cache, WebSocket cross-instance pub/sub. Truth is always Postgres.
- **Auth boundary**: `server/apps/auth` owns Better Auth + OIDC. The API's `sessionMiddleware` validates Auth-issued JWTs and fills context but doesn't block; `authGuard` returns 401.
- **Multi-instance safe**: all writes go through Postgres transactions; cross-instance messaging uses Redis Pub/Sub. No async work or in-process singletons.
+1 -56
View File
@@ -1,56 +1 @@
# Server CLAUDE.md
Agent-facing guide for `server/apps/api`.
## Overview
Hono-based Node.js resource API. The sibling `server/apps/auth` workspace app owns Better Auth, OIDC, sessions, and account lifecycle; this package owns billing, chat sync, LLM gateway forwarding, and business observability. **Multi-instance deployed on Railway** — design all features assuming N>1 instances sharing the same Postgres and Redis.
## Deployment Model
- Hosted on **Railway**, multiple instances behind a load balancer.
- Independent applications: `server/apps/api/src/main.ts` and `server/apps/auth/src/main.ts`. There is no runtime role CLI and neither app imports the other service graph.
- The API has no background polling loops or fire-and-forget tasks — every business write happens inside the request thread.
- Stateless per-instance: no local state that matters across requests.
- Cross-instance coordination via Redis Pub/Sub (WebSocket broadcast). DB-level idempotency (`(userId, requestId)` partial unique index on `flux_transaction`) covers retries.
- Rate limiting is currently **in-memory** (not distributed) — keep this in mind when adding rate-sensitive features.
## Tech Stack
Hono, Drizzle ORM, PostgreSQL, Redis, Stripe, OpenTelemetry, Valibot, injeca (DI), tsx. Better Auth lives only in `server/apps/auth`.
## Commands
```sh
pnpm -F @proj-airi/api-server dev # dev with dotenvx (.env.local)
pnpm -F @proj-airi/api-server typecheck
pnpm -F @proj-airi/api-server exec vitest run # all server tests
pnpm exec vitest run server/apps/api/src/... # single test file
pnpm -F @proj-airi/api-server db:generate # drizzle-kit generate
pnpm -F @proj-airi/api-server db:push # drizzle-kit push
pnpm -F @proj-airi/auth-server auth:generate # better-auth → server/packages/auth-shared/src/schema.ts
```
Local observability is maintained in `proj-airi/airi-railway`; run its `otel/docker-compose.yaml` stack.
## Architecture Summary
**Entry & DI**: `server/apps/api/src/main.ts` → `src/server.ts` → `src/app.ts`; `server/apps/auth/src/main.ts` → `src/server.ts`. The workspace apps have separate package manifests, env schemas, Dockerfiles, and composition roots.
**Layering**:
- **Routes** (`src/routes/`): thin — param validation (Valibot), auth guards, error mapping. No business logic here.
- **Services** (`src/services/`): core business logic and DB transactions.
- **Schemas** (`src/schemas/`): Drizzle table definitions. Drizzle loads migrations from `drizzle/` at startup.
**Middleware chain** (`/api/*`): CORS → hono/logger → optional otel → sessionMiddleware → bodyLimit(1MB) → per-route guards. WebSocket `/ws/chat` registered before bodyLimit.
**Error model**: `ApiError(statusCode, errorCode, message, details)` in `src/utils/error.ts`.
## Key Design Decisions
- **Flux read/write separation**: `FluxService` reads (Redis cache-aside), `BillingService` writes (single Postgres tx that mutates `user_flux` and writes the matching `flux_transaction` ledger row). Never put write-balance logic in `flux.ts`.
- **No async billing pipeline**: debits and credits update balance + ledger in one transaction. The `(user_id, request_id)` partial unique index gives DB-level idempotency for retries; LLM `request log` rows are written best-effort right after the response is delivered.
- **In-process LLM/TTS router**: `/api/v1/openai` is dispatched by `services/domain/llm-router` reading `LLM_ROUTER_CONFIG` (per-model upstream chain + envelope-encrypted keys). `chat/completions` walks LLM upstreams with key fallback; `audio/speech` delegates to a TTS adapter (`azure` / `dashscope-cosyvoice` / `volcengine`); `audio/voices` returns the adapter's compiled-in catalog. Server handles auth/billing/logging, not model execution.
- **Redis is cache + pub/sub, not truth**: balance cache, app_settings read cache, WebSocket cross-instance pub/sub. Truth is always Postgres.
- **Auth boundary**: `server/apps/auth` owns Better Auth + OIDC. The API's `sessionMiddleware` validates Auth-issued JWTs and fills context but doesn't block; `authGuard` returns 401.
- **Multi-instance safe**: all writes go through Postgres transactions; cross-instance messaging uses Redis Pub/Sub. No async work or in-process singletons.
@AGENTS.md
+4 -207
View File
@@ -35,213 +35,10 @@ shape for non-trivial work in this package.
- Do not let Gemini CLI edit the same worktree concurrently with Codex. If Gemini needs to write candidate code, use a separate worktree and require GPT-5.5 review before adopting any patch.
- Keep Copilot governance files, `.codex/` configuration, GitHub labels, and business/runtime changes in separate commits or PRs.
## Current Status Snapshot
## Terminal Lane Handoff
Updated for the current terminal-lane-v2 workstream.
The important truth is:
- `exec` is already a real mainline surface.
- `PTY` is no longer just a loose tool set; the workflow engine now has self-acquire support.
- Service-layer terminal E2Es for the current lane are present and treated as the terminal proof line.
- The AIRI chat terminal demo is now aligned with terminal lane v2 and no longer pre-creates PTY.
- The desktop shell now distinguishes `pty_session` from `terminal_and_apps`.
- AIRI chat self-acquire is now part of the strict release gate set, so PTY mainline support is no longer intentionally held back.
Do not rely on compressed chat summaries to resume this work. Use this file as the handoff source of truth and update it when terminal-lane behavior changes materially.
For terminal-lane facts, treat this file in alignment with:
- `README.md`
- `src/support-matrix.ts`
- `package.json` scripts
- implementation/tests for each referenced command or file
## Terminal Lane v2: What Is Already Landed
### 1. Terminal surface model exists
Terminal-capable workflow steps now have explicit terminal semantics instead of pure guesswork:
- `mode: 'exec' | 'auto' | 'pty'`
- `interaction: 'one_shot' | 'persistent'`
The main implementation lives in:
- `src/workflows/types.ts`
- `src/workflows/surface-resolver.ts`
- `src/terminal/interactive-patterns.ts`
### 2. Auto surface resolution is fixed to a small rule set
`auto` is intentionally narrow. It only upgrades to PTY when one of these is true:
1. The current `taskId + stepId` already has a bound PTY session.
2. The step explicitly declares `interaction: 'persistent'`.
3. The command matches `KNOWN_INTERACTIVE_COMMAND_PATTERNS`.
4. A failed/timed-out exec attempt surfaces one of `INTERACTIVE_OUTPUT_MARKERS`.
This rule set is covered by:
- `src/workflows/surface-resolver.test.ts`
- `src/terminal/interactive-patterns.test.ts`
### 3. Workflow engine can self-acquire PTY
The engine already contains the v2 shape:
- `AcquirePtyForStep`
- `StepTerminalProgress`
- suspension point `before_pty_acquire`
- PTY step family support:
- `pty_send_input`
- `pty_read_screen`
- `pty_wait_for_output`
- `pty_destroy_session`
The main implementation lives in:
- `src/workflows/engine.ts`
The intended behavior is:
- workflow resolves the terminal surface
- if PTY is needed, workflow acquires/binds PTY itself
- workflow continues inside the same workflow
- outward terminal reroute is now secondary, not the mainline proof
### 4. Service-layer PTY self-acquire E2E exists and is green
The current real terminal E2E for v2 is:
- `src/bin/e2e-terminal-self-acquire.ts`
This script now proves:
- **no pre-created PTY**
- workflow detects an interactive command
- engine self-acquires PTY
- command executes on PTY
- step succeeds without outward reroute
- run-state / binding / audit stay consistent
It currently uses:
- `workflow_validate_workspace`
- an interactive `checkCommand` of `vim --version`
This is the current service-level proof for terminal lane v2.
### 5. AIRI chat self-acquire demo is now on the v2 path
`src/bin/e2e-airi-chat-terminal-self-acquire.ts` follows the same product story:
- no harness-side `pty_create`
- AIRI calls the real workflow
- the workflow self-acquires PTY for the interactive validation step
- AIRI finishes with a natural-language summary for demo use
The latest successful reports live under:
- `.computer-use-mcp/reports/airi-chat-terminal-self-acquire-*`
The current package commands are:
- `pnpm -F @proj-airi/computer-use-mcp e2e:airi-chat-terminal-self-acquire`
- `pnpm -F @proj-airi/computer-use-mcp demo:terminal-self-acquire`
### 6. Support matrix already reflects the new direction
Relevant entries in `src/support-matrix.ts`:
- `terminal_exec` → `product-supported`
- `terminal_pty` → `product-supported`
- `terminal_exec_to_pty_reroute` → `covered` and explicitly labeled legacy fallback
- `terminal_auto_surface_resolution` → `covered`
- `terminal_pty_self_acquire` → `product-supported`
- `terminal_pty_step_family` → `covered`
The current strict release gates are:
- `pnpm -F @proj-airi/computer-use-mcp e2e:developer-workflow`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-exec`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-pty`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-self-acquire`
- `pnpm -F @proj-airi/computer-use-mcp e2e:airi-chat-terminal-self-acquire`
## What Is Still Not Finished
These are the real gaps. Do not talk yourself into thinking terminal lane is fully shipped before they are closed.
### 1. Desktop approval semantics are improved, but still need one more explicit review
`apps/stage-tamagotchi/src/renderer/App.vue` now distinguishes:
- `terminal_and_apps`
- `pty_session`
and it no longer pretends a PTY approval is the same thing as a generic terminal/app grant.
The current intended behavior is:
- `terminal_exec` / `open_app` / `focus_app` keep the old session-scoped auto-approve behavior
- `pty_create` stores a `pty_session` grant scope
- `pty_create` does **not** auto-approve future PTY creation requests
This is much closer to the product model, but it is still worth reviewing whenever approval UX changes again.
## Where To Look First
If you are continuing terminal lane work, read these first:
1. `src/workflows/engine.ts`
2. `src/workflows/surface-resolver.ts`
3. `src/terminal/interactive-patterns.ts`
4. `src/bin/e2e-terminal-self-acquire.ts`
5. `src/bin/e2e-airi-chat-terminal-self-acquire.ts`
6. `src/support-matrix.ts`
7. `apps/stage-tamagotchi/src/renderer/App.vue`
8. `apps/stage-tamagotchi/src/renderer/modules/computer-use-approval.ts`
That set is enough to reconstruct the current terminal-lane-v2 state without rereading the entire repo.
## Validation Commands
Use these as the baseline checks for terminal lane work:
### Service-level terminal lane
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-exec`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-pty`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-self-acquire`
- `pnpm -F @proj-airi/computer-use-mcp e2e:airi-chat-terminal-self-acquire`
### Core test coverage
- `pnpm -F @proj-airi/computer-use-mcp exec vitest run --config ./vitest.config.ts`
### Typecheck
- `pnpm -F @proj-airi/computer-use-mcp typecheck`
- `pnpm -F @proj-airi/stage-ui typecheck`
If `pnpm -F @proj-airi/stage-tamagotchi typecheck` behaves oddly in the current environment, run the two underlying commands directly:
- `pnpm -F @proj-airi/stage-tamagotchi run typecheck:node`
- `pnpm -F @proj-airi/stage-tamagotchi run typecheck:web`
## Handoff Rules
If you change terminal lane behavior, update this file before stopping.
At minimum, always rewrite these four facts:
1. Is PTY self-acquire the mainline, or does any path still depend on pre-created PTY?
2. Is AIRI chat E2E aligned with the service-level terminal lane, or still on an older path?
3. Is desktop approval using real `pty_session` semantics, or still old `terminal_and_apps` semantics?
4. Which terminal capabilities are `product-supported` vs only `covered` in `src/support-matrix.ts`?
If those four facts are stale, the next agent will lose time re-deriving context from code.
- Read [`terminal-lane-status.md`](terminal-lane-status.md) before you continue terminal lane work.
- If you change terminal lane behavior, update that file in the same change.
## Boundary Reminder
@@ -252,7 +49,7 @@ If those four facts are stale, the next agent will lose time re-deriving context
## Agent Operating Guardrails
- Keep changes narrow and evidence-backed.
- Keep documentation/config changes separate from runtime logic changes.
- Keep agent configuration changes separate from runtime logic changes. Update the documentation that describes the changed code, such as `terminal-lane-status.md`, in the same change.
- Do not touch desktop overlay, Electron bridge, Chrome extension, MCP handler registration, workspace memory, verification gate, or shell guard unless the task explicitly requires it.
- Keep planning, archive eligibility, compaction text, provider message emission, and runner semantics separate unless evidence requires a cross-layer change.
- Prefer pure helpers for shared contracts.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
@@ -0,0 +1,213 @@
# Terminal Lane Status
Scope: terminal lane work in `services/computer-use-mcp/**`.
This file is the handoff record for terminal lane work. It moved here from `AGENTS.md` without content changes.
## Current Status Snapshot
Updated for the current terminal-lane-v2 workstream.
The important truth is:
- `exec` is already a real mainline surface.
- `PTY` is no longer just a loose tool set; the workflow engine now has self-acquire support.
- Service-layer terminal E2Es for the current lane are present and treated as the terminal proof line.
- The AIRI chat terminal demo is now aligned with terminal lane v2 and no longer pre-creates PTY.
- The desktop shell now distinguishes `pty_session` from `terminal_and_apps`.
- AIRI chat self-acquire is now part of the strict release gate set, so PTY mainline support is no longer intentionally held back.
Do not rely on compressed chat summaries to resume this work. Use this file as the handoff source of truth and update it when terminal-lane behavior changes materially.
For terminal-lane facts, treat this file in alignment with:
- `README.md`
- `src/support-matrix.ts`
- `package.json` scripts
- implementation/tests for each referenced command or file
## Terminal Lane v2: What Is Already Landed
### 1. Terminal surface model exists
Terminal-capable workflow steps now have explicit terminal semantics instead of pure guesswork:
- `mode: 'exec' | 'auto' | 'pty'`
- `interaction: 'one_shot' | 'persistent'`
The main implementation lives in:
- `src/workflows/types.ts`
- `src/workflows/surface-resolver.ts`
- `src/terminal/interactive-patterns.ts`
### 2. Auto surface resolution is fixed to a small rule set
`auto` is intentionally narrow. It only upgrades to PTY when one of these is true:
1. The current `taskId + stepId` already has a bound PTY session.
2. The step explicitly declares `interaction: 'persistent'`.
3. The command matches `KNOWN_INTERACTIVE_COMMAND_PATTERNS`.
4. A failed/timed-out exec attempt surfaces one of `INTERACTIVE_OUTPUT_MARKERS`.
This rule set is covered by:
- `src/workflows/surface-resolver.test.ts`
- `src/terminal/interactive-patterns.test.ts`
### 3. Workflow engine can self-acquire PTY
The engine already contains the v2 shape:
- `AcquirePtyForStep`
- `StepTerminalProgress`
- suspension point `before_pty_acquire`
- PTY step family support:
- `pty_send_input`
- `pty_read_screen`
- `pty_wait_for_output`
- `pty_destroy_session`
The main implementation lives in:
- `src/workflows/engine.ts`
The intended behavior is:
- workflow resolves the terminal surface
- if PTY is needed, workflow acquires/binds PTY itself
- workflow continues inside the same workflow
- outward terminal reroute is now secondary, not the mainline proof
### 4. Service-layer PTY self-acquire E2E exists and is green
The current real terminal E2E for v2 is:
- `src/bin/e2e-terminal-self-acquire.ts`
This script now proves:
- **no pre-created PTY**
- workflow detects an interactive command
- engine self-acquires PTY
- command executes on PTY
- step succeeds without outward reroute
- run-state / binding / audit stay consistent
It currently uses:
- `workflow_validate_workspace`
- an interactive `checkCommand` of `vim --version`
This is the current service-level proof for terminal lane v2.
### 5. AIRI chat self-acquire demo is now on the v2 path
`src/bin/e2e-airi-chat-terminal-self-acquire.ts` follows the same product story:
- no harness-side `pty_create`
- AIRI calls the real workflow
- the workflow self-acquires PTY for the interactive validation step
- AIRI finishes with a natural-language summary for demo use
The latest successful reports live under:
- `.computer-use-mcp/reports/airi-chat-terminal-self-acquire-*`
The current package commands are:
- `pnpm -F @proj-airi/computer-use-mcp e2e:airi-chat-terminal-self-acquire`
- `pnpm -F @proj-airi/computer-use-mcp demo:terminal-self-acquire`
### 6. Support matrix already reflects the new direction
Relevant entries in `src/support-matrix.ts`:
- `terminal_exec` → `product-supported`
- `terminal_pty` → `product-supported`
- `terminal_exec_to_pty_reroute` → `covered` and explicitly labeled legacy fallback
- `terminal_auto_surface_resolution` → `covered`
- `terminal_pty_self_acquire` → `product-supported`
- `terminal_pty_step_family` → `covered`
The current strict release gates are:
- `pnpm -F @proj-airi/computer-use-mcp e2e:developer-workflow`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-exec`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-pty`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-self-acquire`
- `pnpm -F @proj-airi/computer-use-mcp e2e:airi-chat-terminal-self-acquire`
## What Is Still Not Finished
These are the real gaps. Do not talk yourself into thinking terminal lane is fully shipped before they are closed.
### 1. Desktop approval semantics are improved, but still need one more explicit review
`apps/stage-tamagotchi/src/renderer/App.vue` now distinguishes:
- `terminal_and_apps`
- `pty_session`
and it no longer pretends a PTY approval is the same thing as a generic terminal/app grant.
The current intended behavior is:
- `terminal_exec` / `open_app` / `focus_app` keep the old session-scoped auto-approve behavior
- `pty_create` stores a `pty_session` grant scope
- `pty_create` does **not** auto-approve future PTY creation requests
This is much closer to the product model, but it is still worth reviewing whenever approval UX changes again.
## Where To Look First
If you are continuing terminal lane work, read these first:
1. `src/workflows/engine.ts`
2. `src/workflows/surface-resolver.ts`
3. `src/terminal/interactive-patterns.ts`
4. `src/bin/e2e-terminal-self-acquire.ts`
5. `src/bin/e2e-airi-chat-terminal-self-acquire.ts`
6. `src/support-matrix.ts`
7. `apps/stage-tamagotchi/src/renderer/App.vue`
8. `apps/stage-tamagotchi/src/renderer/modules/computer-use-approval.ts`
That set is enough to reconstruct the current terminal-lane-v2 state without rereading the entire repo.
## Validation Commands
Use these as the baseline checks for terminal lane work:
### Service-level terminal lane
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-exec`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-pty`
- `pnpm -F @proj-airi/computer-use-mcp e2e:terminal-self-acquire`
- `pnpm -F @proj-airi/computer-use-mcp e2e:airi-chat-terminal-self-acquire`
### Core test coverage
- `pnpm -F @proj-airi/computer-use-mcp exec vitest run --config ./vitest.config.ts`
### Typecheck
- `pnpm -F @proj-airi/computer-use-mcp typecheck`
- `pnpm -F @proj-airi/stage-ui typecheck`
If `pnpm -F @proj-airi/stage-tamagotchi typecheck` behaves oddly in the current environment, run the two underlying commands directly:
- `pnpm -F @proj-airi/stage-tamagotchi run typecheck:node`
- `pnpm -F @proj-airi/stage-tamagotchi run typecheck:web`
## Handoff Rules
If you change terminal lane behavior, update this file before stopping.
At minimum, always rewrite these four facts:
1. Is PTY self-acquire the mainline, or does any path still depend on pre-created PTY?
2. Is AIRI chat E2E aligned with the service-level terminal lane, or still on an older path?
3. Is desktop approval using real `pty_session` semantics, or still old `terminal_and_apps` semantics?
4. Which terminal capabilities are `product-supported` vs only `covered` in `src/support-matrix.ts`?
If those four facts are stale, the next agent will lose time re-deriving context from code.