Skip to content

Contributing

These are not style preferences. A change that breaks one of them will be asked to change, because the whole architecture rests on them.

  1. Extreme object orientation. The design is objects sending messages, not functions operating on data.
  2. Everything is an instance. Plain objects must not exist. Anything entering from a boundary is converted to an instance at the shortest architecturally correct distance from where it is formed. A message is passed and received as an instance — a plain object cannot be handed to a function, because it is not treated as a first-class object.
  3. Where a class can serve as a type, use class over interface.
  4. Namespaces and nesting are used deliberately, and the folder structure reveals them. A class name must let you infer its file path.
  5. Verify against the real thing. Script mocks hide what real CLIs and real operating systems do. Three defects were only ever found by running the actual CLI, and one only by running Windows in CI.

Everything arriving at a boundary becomes an instance at the shortest distance.

Boundary What arrives The instance it becomes
A line of CLI stdout A JSON string An Event subclass
The result of assembling argv string[] Argv
A permission request control_request JSON PermissionRequest
A file path string An AbstractPath subclass (one that knows about WSL conversion)

Paths matter most here. A string travelling around forces every reader to re-derive “is this a Windows path or a WSL one?”, whereas a WindowsPath / WslPath instance holds the answer in its type.

Principle 4 decides this, and it is not negotiable: the name gives the path.

ActiveCli.Adapter.Claude.ClaudeAdapter → src/ActiveCli/Adapter/Claude/ClaudeAdapter.ts

One class per file. Before adding a class, ask which namespace’s problem it solves:

If it… It belongs in
Is spelled differently by each CLI Adapter/<Cli>/
Solves an operating-system problem the same way for every CLI Process/
Answers a question by being itself, and imports nothing Path/, or alongside PermissionMode / SessionId
Is something a CLI told us Event/
Is something we tell a CLI Message/

An adapter must not reach into Process/ concerns like locating an executable, augmenting PATH, or killing trees. Those are universal, and adapters own only what their CLI spells differently.

There are no barrel files inside the tree. Every internal import names the file it wants, and src/index.ts is the only public surface — a new public class is added there explicitly.

Terminal window
npm test # vitest run — 132 tests, 12 files
npm run typecheck
npm run build # tsc, then copy-assets

Writing a test the architecture already supports

Section titled “Writing a test the architecture already supports”
Technique Where it is used Why it is possible
Real processes Session.test.ts runs node -e <script> as a scripted CLI The adapter contract is small enough to implement in a test
Pinned platform Launcher, tree and adapter tests Platform is a constructor parameter, never read directly
Pure logic extracted WindowsLauncher.pick(), JobSpawner.buildCommandLine() Made public so the rule is assertable without a Windows host
Substituted collaborators StubIdentity in registry tests Constructor-default injection throughout
Temp directories CliRegistry tests The registry directory is the caller’s to choose
Prose fixtures McpOutputParser.test.ts asserts against real claude mcp list output Parsing printed output is a contract worth pinning to samples

If a new class needs process.platform, take it as a constructor parameter with a default. That is what keeps a Windows rule assertable from a Mac.

The test workflow runs on ubuntu, macOS and windows-latest, and the Windows leg is not a formality.

On its first run it caught a defect that would never have surfaced on a Mac: an argument passing through cmd.exe as a&b|c<d>e was truncated to a, with cmd reporting 'b' is not recognized as an internal or external command. The common belief that Node’s CommandLineToArgvW quoting makes & literal is false — the original comment in the code said exactly that.

Since mcp add-json passes JSON dense with those very characters, Windows users’ MCP configuration was about to be silently truncated on save.

The fix is to caret-escape & | < > and nothing else. A first attempt that also escaped parentheses broke ordinary arguments like write("hi") — carets are only meaningful in unquoted context, and parentheses only in block syntax.

The lesson generalises: if a change touches process spawning, quoting, paths, or process trees, it needs the Windows leg to be green before it means anything.

Documentation and discussion are in Korean; code, comments, identifiers and commit messages are in English as the shared language.

  • npm test passes
  • npm run typecheck passes
  • npm run build passes, including copy-assets
  • CI is green on all three operating systems
  • Every new class’s path matches its name
  • No plain object crosses a boundary that should have produced an instance
  • Any new public class is exported from src/index.ts
  • Any behaviour that can only be confirmed against the real CLI was confirmed there