Ctrl K
ComunidadGuías…

AGENTS.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# mikan

Multi-platform AI coding agent for Slack, Telegram, Discord, and GitHub (`@geminixiang/mikan`). Each conversation has an isolated Conversation office: a workspace directory and sandbox runtime. The harness in `src/harness/` is built on `pi-durable` / `pi-ai`.

## Core idea

mikan is a thin layer over three foundations: pi-durable is the agent runtime, OpenConnector provides tools with credentials, and an OCI container is the sandbox. mikan connects them to chat platforms.

A model completes a task when it has the best information about it and the tools, with credentials, the task needs. mikan's job is to deliver both and leave the judgement to the model: what is relevant, what to read next, when it has enough. Code that makes that judgement for the model, such as relevance filters, traversal limits, or ranking rules, is complexity a design must justify with a measured failure that better information or a tool the model drives cannot fix. Review every design against this first.

## Maintenance

The agent maintains this repository and decides its design. The owner is consulted only for what reaches outside it (see Boundaries); a decision is recorded in its commit, research doc, or ADR instead of waiting for approval.

Features have been built thick, judged good by the eval that shipped them, and found wanting only after people depended on them. To keep that from repeating:

- Ship the thinnest thing that could work: a prompt line, a skill, or an existing CLI or MCP server before a tool or a module.
- A new feature's ADR states its removal condition: a usage or success number from production and the date to check it. When the check fails, remove the feature.
- Decide what stays from production usage, not from the eval that shipped it.
- People using a feature do not block its removal when another path does the same job; name that path in the ADR.

## Where to look

- Domain terms: `CONTEXT.md`. Module map, invariants, and decisions: `ARCHITECTURE.md`, `docs/adr/`.
- Before editing a module, read its `README.md` and its parent module's, if present. A README holds only what the code cannot show: contracts, invariants, and pitfalls. When a contract can be enforced, write a test and keep only the reason in the README.
- Before real Slack E2E, follow `docs/testing/slack-e2e.md`: Socket Mode delivers each event to one connected client, so another daemon on the same app silently answers instead. A missing reply needs intake evidence, not a longer model timeout.

## Commands

`package.json` lists every script. Non-obvious ones:

| Action                                          | Command                                     |
| ----------------------------------------------- | ------------------------------------------- |
| Install                                         | `npm install --ignore-scripts`              |
| Focused tests                                   | `npm test -- src/test/<name>.test.ts`       |
| Type-check everything                           | `npm run typecheck` (tests, e2e, examples)  |
| Format selected files                           | `npx oxfmt -c .config/oxfmtrc.json <paths>` |
| List duplicated code                            | `npm run duplication`                       |
| Find test-only or dead exports (manual cleanup) | `npm run knip:unused`                       |

Match verification to the change: behavior changes need a test that fails without them; documentation-only changes need formatting and `src/test/doc-references.test.ts`. The pre-commit hook runs the full gate.

## Tests

- In asynchronous tests, act, await the observable outcome (`vi.waitFor` or a deferred gate), then assert. Use elapsed time only when timing is under test, because sleeps pass or fail with machine load.
- Vitest shuffles test order and prints the seed. When a test fails only in some orders, reproduce it with `--sequence.seed=<seed>` and fix the shared state it leaks, for example with `mockResolvedValueOnce` instead of re-pointing a module mock. A rerun that passes proves nothing.
- Inject external clients into the owning operation and keep the production default; replacing process-wide globals leaks between tests.

## Enforced contracts

These rules have guards. When a guard fails, follow the owner or budget it names; change a guard only with a recorded reason.

- Imports come from the declaring module; only `package.json` `exports` entries re-export, and code outside `src/adapters/` reaches adapters only through the composition root (`main.ts`, `cli/`, `runtime/`). Each constant, default, schema, or metadata fact has one owning module. Guards: `src/test/source-guards.*.test.ts`; add a guard class with spelling tables when you give a fact an owner.
- Production double assertions through `unknown` match exact per-file budgets in `src/test/source-guards.boundaries.test.ts`. Lower a budget in the change that removes an assertion; prefer narrowing, a validator, or a typed seam to raising one.
- Production type assertions on parsed JSON (`JSON.parse(...) as T`, `response.json() as T`) match exact per-file budgets in `src/test/source-guards.boundaries.test.ts`, because an assertion trusts an external shape it never checked. Validate with `parseJsonSchemaValue` or a type guard instead.
- Production duplicated code matches exact per-file-pair clone budgets in `src/test/source-guards.duplication.test.ts`. Lower a budget in the change that removes a clone; move shared logic to one owning module instead of raising one.
- Every model-facing tool schema requires a `label` parameter, because the system prompt promises it and the presenter shows it as the run's current step; the TypeScript type does not carry the JSON Schema `required` list, so a missing label compiles. Use `defineHostFnTool`, which injects it. Guard: `src/test/tool-label-contract.test.ts`, whose exemptions each carry a reason.
- Provider-facing tool schemas have an object root, because OpenAI rejects top-level unions, `anyOf`, and `oneOf`; validate alternate invocation modes inside the object.

## Design rules

- Keep object shapes stable: write `field: condition ? value : undefined` when absent and `undefined` are equivalent, and keep a conditional spread only where the property must be absent, such as for an `in` check.
- Put shared exported types in the module's `types.ts`. Use a named options interface when a signature spans several lines or crosses a module boundary; call the underlying function instead of adding a pass-through helper.
- End a `switch` over a mikan-owned union that does not return a value with `default: value satisfies never;`, so adding a variant fails to compile instead of being silently skipped. Value-returning switches already get this from their return type.
- Check preconditions first and return or throw early, keeping the happy path unnested; use EAFP when check-then-act would race or duplicate expensive work. For subprocesses, handle both the startup `error` event and exit, and keep the command or path in the error, because a missing executable otherwise escapes as an unhandled error.
- Delegate session and agent semantics to public `pi-durable` interfaces so mikan upgrades with Pi. Deep imports of Pi's private `dist` paths and copies of Pi internals break on upgrade; where Pi lacks a public hook, document the small local exception, verify it against native Pi behavior, and remove it once upstream exposes the capability.
- Treat files whose format Pi defines, such as `models.json`, as Pi's schema: reach a new need through a field Pi already documents (a built-in provider given only `baseUrl` and `apiKey` is Pi's proxy override), and add no mikan-only field. Where mikan's reader lags Pi, close the gap by matching Pi's behavior.
- Call models through pi-ai `Models`, built from the daemon's `models.json`: a classifier comes from pi-ai's built-in catalog, picked by which provider has a key, and generated text comes from the office's resolved `settings.json` model. A provider URL, model ID, or API key variable in code ties a feature to one vendor and leaves it outside the operator's routing.
- Design for two audiences, because the repository is public: an outside user with any one provider key, and a private deployment behind its own gateway. The private case is reached through the same generic configuration, never through code or settings shaped around one service.
- Derive office paths and vault keys from an `Office` value, never from raw platform conversation IDs (ADRs 0003–0005). Keep credential and mount isolation intact across sandbox backends, set secret file permissions explicitly, and write state that must not be partially visible with `atomicWritePrivateFile`.
- Name the need a comment would explain in a file or function instead; the urge to comment signals a split responsibility or unclear name.
- Solve the requested problem with the simplest design. Leave unrelated behavior unchanged, add no speculative abstraction or compatibility layer, and state consequential compatibility changes in the report.

## Developing a change

Take every change except a documentation-only one through these steps in order. When a step fails, return to step 1.

1. **Research**: read the code, its history, the ADRs, and the Pi docs, type declarations, and examples at the installed version until every unknown is listed with an answer and its source. Record the findings in `docs/research/`.
2. **Research again**: verify each answer with an experiment outside `src/`, on a copy of real data or through a dependency's public API, and hunt the cases the first pass missed: other platforms, other message paths, concurrency, existing data. Correct in the research doc every earlier claim an experiment overturns. Done when a full pass finds nothing new.
3. **Decide**: prototype each candidate outside `src/`, compare them with numbers (correctness, cost, latency, storage, code size), and evaluate them against the best-design bar below. Record each open choice with its trade-offs and the one chosen in the research doc or ADR.
4. **Acceptance eval**: before implementing, build a black-box eval of every behavior the change touches and run it on `main` as the baseline. Record each case with its baseline result and its expected result after the change, accepted regressions included, before implementing. Build it with the `mikan-acceptance-eval` skill.
5. **Implement and test** on `main`, failing test first for a behavior change.
6. **Verify**: rerun the eval; every case must match its expected result. Then exercise the change on the real platform with a local daemon.
7. **Ship**: commit to `main` with the eval's before and after table in the message, and see CI pass. When the change can alter the path a Slack message takes through a running daemon, also ask the owner to approve a run of the Slack E2E workflow (`slack-e2e.yml`) and see it pass. That path covers boot and onboarding, settings and model loading, Slack intake and replies, session storage and routing, the run loop and its tools, scheduled events, and any dependency the daemon loads, such as Pi. Changes outside it, such as `mikan migrate` steps, other platform adapters, the Admin portal, and test-only changes, are done when CI passes. State in the report which case applied.

A suspected bug is reproduced on the real platform before it is reported. Its issue states the steps, what the user saw with timings, and the cause, and its fix goes through the steps above.

The best design meets these, and when they conflict, the earlier one wins:

- **Serves the core idea**: delivers information and authenticated tools, and leaves judgement to the model.
- **Built on the foundations**: hand each responsibility to pi-durable, OpenConnector, the OCI sandbox, or another existing dependency through its public interface; own code only where none offers it, with the reason recorded.
- **Maintainable**: the least new code, state, and special cases; understandable from its README and ADR; unchanged by a Pi upgrade.
- **Acceptable in its trade-offs**: every cost measured in steps 3 and 4 and recorded in the report or ADR.

## Boundaries

- This repository is public. Commits, docs, tests, issues, PRs, and release notes use placeholders such as `C0123456789`, `acme`, and `example.com` in place of real organizations, customers, people, hosts, and Slack workspace, channel, user, or message IDs or links; describe production evidence without identifiers.
- Other agents may share this checkout. Stage explicit paths for your own changes, and compare against a commit with `git worktree add`. `git add -A`, `git add .`, `git stash`, `git checkout .`, `git reset --hard`, and `git clean` capture or discard work you did not make.
- When a request turns out to need a design change, such as a new setting, a schema field, or changed behavior, decide it through the steps above and record the options and the choice; a published release cannot be taken back. Before retiring a credential or dependency, list every reader, including raw `fetch` calls and direct env reads.
- The owner approves what reaches outside the repository: production changes, spending, credentials, real-platform E2E, UI automation, and messages to people. Use the access path the owner requests.
- Treat review comments from bots and review agents as evidence: fix those that identify a real bug, contract gap, security issue, or violation of these rules, and give a one-line reason for each one you decline.

## Reporting

Report changes, verification, and unresolved risks concisely in the user's language. Use tables for implementation decisions (choice, alternatives, rationale) and before/after differences when they clarify the work.