Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .agents/skills/codebase-design/DEEPENING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Deepening

How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [SKILL.md](SKILL.md): **module**, **interface**, **seam**, **adapter**.

## Dependency categories

When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam.

### 1. In-process

Pure computation, in-memory state, no I/O. Always deepenable: merge the modules and test through the new interface directly. No adapter needed.

### 2. Local-substitutable

Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface.

### 3. Remote but owned (Ports & Adapters)

Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter.

Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."*

### 4. True external (Mock)

Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter.

## Seam discipline

- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection.
- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them.

## Testing strategy: replace, don't layer

- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist; delete them.
- Write new tests at the deepened module's interface. The **interface is the test surface**.
- Tests assert on observable outcomes through the interface, not internal state.
- Tests should survive internal refactors, since they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface.
44 changes: 44 additions & 0 deletions .agents/skills/codebase-design/DESIGN-IT-TWICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Design It Twice

When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout): your first idea is unlikely to be the best.

Uses the vocabulary in [SKILL.md](SKILL.md): **module**, **interface**, **seam**, **adapter**, **leverage**.

## Process

### 1. Frame the problem space

Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:

- The constraints any new interface would need to satisfy
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
- A rough illustrative code sketch to ground the constraints, not a proposal, just a way to make the constraints concrete

Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.

### 2. Spawn sub-agents

Spawn 3+ sub-agents in parallel. Each must produce a **radically different** interface for the deepened module.

Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:

- Agent 1: "Minimize the interface: aim for 1–3 entry points max. Maximise leverage per entry point."
- Agent 2: "Maximise flexibility: support many use cases and extension."
- Agent 3: "Optimise for the most common caller: make the default case trivial."
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."

Include both [SKILL.md](SKILL.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.

Each sub-agent outputs:

1. Interface (types, methods, params, plus invariants, ordering, error modes)
2. Usage example showing how callers use it
3. What the implementation hides behind the seam
4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md))
5. Trade-offs: where leverage is high, where it's thin

### 3. Present and compare

Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**.

After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated: the user wants a strong read, not a menu.
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
dist
.git
.github
*.log
.DS_Store
.pi
.vscode
28 changes: 17 additions & 11 deletions .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,30 @@ name: node-ci

on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:

runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [22.x, 24.x]
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: yarn
- run: yarn test
- uses: actions/checkout@v7
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run test:ci
- run: npm run lint
- run: npm run check-types
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,9 @@ packages/**/dist

.vscode

lerna-debug.log
lerna-debug.log

.DS_Store

.agents/
.pi/
1 change: 0 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"no-labels": "error",
"no-extra-label": "error",
"sort-imports": "off",

"unicorn/prefer-string-starts-ends-with": "error",
"unicorn/prefer-string-trim-start-end": "error",
"unicorn/prefer-spread": "error",
Expand Down
12 changes: 0 additions & 12 deletions .travis.yml

This file was deleted.

41 changes: 41 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Chan

Chan is a changelog management tool (`@geut/chan`) with an optional AI layer that maintains a code knowledge base alongside the consumer-facing changelog.

## Language

### Artifacts

**Knowledge Base**:
The `.chan/code.md` file: an append-only, committed record of code changes (one entry per commit) plus an optional Context section. Supports changelog enhancement and future queries about how the codebase evolved.
_Avoid_: code.md file (when speaking conceptually), code base (reserved for the actual source tree)

**Changelog**:
The consumer-facing `CHANGELOG.md` curated by hand or by AI augmentation.
_Avoid_: knowledge base

**Context**:
The machine-owned `## Context` section at the top of the Knowledge Base: a terse, evidence-backed summary of the project (description, usage, runtimes, project types, requirements, notes), delimited by `chan:context` markers. Generated once at init by an Inspection and re-read as prompt context by later AI operations. The only part of the Knowledge Base exempt from the append-only rule.
_Avoid_: codebase context (that is the `AIConfig.context` option), project context

### Operations

**Inspection**:
The one-time AI operation that derives the Context from a Codebase Snapshot. Performed by the inspector (`createInspector` in chan-ai).
_Avoid_: analysis (reserved for commits), augmentation

**Analysis**:
The per-commit AI operation that produces a structured entry appended to the Knowledge Base.
_Avoid_: inspection

**Augmentation**:
The AI operation that turns one or more commits (plus Knowledge Base context) into a single changelog entry.

**Codebase Snapshot**:
The deterministic text gathered by chan (package.json, full README, top-level directory listing) that feeds an Inspection. Chan-ai never touches the filesystem to build it.
_Avoid_: inspection input, project description

### Invariants

**Append-only**:
The rule that existing Knowledge Base content is never rewritten; entries are only appended. The Context section is the sole exception.
13 changes: 13 additions & 0 deletions Dockerfile.cowork
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:22-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /workspace

COPY scripts/cowork-entrypoint.sh /usr/local/bin/cowork-entrypoint.sh
RUN chmod +x /usr/local/bin/cowork-entrypoint.sh

ENTRYPOINT ["cowork-entrypoint.sh"]
CMD ["tail", "-f", "/dev/null"]
21 changes: 21 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
services:
cowork:
build:
context: .
dockerfile: Dockerfile.cowork
container_name: chan-ai-cowork
volumes:
- .:/workspace
# Keep container-side node_modules isolated from the host so binaries
# and platform-specific packages work correctly inside Docker.
- node_modules:/workspace/node_modules
- chan-ai-dist:/workspace/packages/chan-ai/dist
working_dir: /workspace
stdin_open: true
tty: true
# No exposed ports: this is a CLI/test package with no HTTP service.
# Humans interact with it via `docker compose exec cowork ...`.

volumes:
node_modules:
chan-ai-dist:
135 changes: 135 additions & 0 deletions docs/TEST-DX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Testing chan's developer experience

This guide walks you through manually testing chan's DX on a throwaway (or real) project. It covers both the original **manual** flow and the new **chan + AI** flow (`chan auto`, `chan analyze`, the post-commit hook, and the `chan release` breaking-change guard).

The fastest path is to test **inside the cowork Docker container**, where the native dependencies are already installed correctly. The exact same steps work on your host after reinstalling host deps for your platform (`rm -rf node_modules && npm install`).

## 0. Make `chan` available on PATH

Inside the container, `chan` is linked globally so it resolves to the live TypeScript source (edits to `packages/chan/src/*.ts` are picked up instantly — no rebuild):

```bash
docker compose exec chan-ai sh -c 'cd /workspace/packages/chan && npm link'
chan --version # → 3.2.x
chan --help
```

On your host instead: `cd packages/chan && npm link` (after reinstalling host deps for macOS).

## 1. Create a throwaway project

```bash
docker compose exec chan-ai sh
mkdir -p /tmp/chan-dx && cd /tmp/chan-dx
git init && git config user.email you@test.com && git config user.name You
git config commit.gpgsign false
```

## 2. Configure AI

Create `.chanrc` in the project root. Opencode Zen is the provider most validated so far:

```json
{
"ai": {
"provider": "opencode",
"model": "kimi-k2.6",
"maxTokens": 1000,
"endpoint": "https://opencode.ai/zen/v1"
}
}
```

Export the key:

```bash
export OPENCODE_API_KEY=sk-...
```

To try other providers later, swap the `ai` block:

- `openai` + `OPENAI_API_KEY` (model e.g. `gpt-4o-mini`)
- `anthropic` + `ANTHROPIC_API_KEY` (model e.g. `claude-sonnet-4`)
- `ollama` — no key; requires `ollama serve` running on `localhost:11434` (model e.g. `llama3.1`)
- `openrouter` / `groq` / `together` / `google` are also registered

AI is considered **enabled** when `provider` + `model` resolve (from `.chanrc` or `--ai-provider`/`--ai-model`). There is no `--no-ai` toggle; remove `ai` from `.chanrc` to test the manual baseline.

## 3. Initialize + install the hook

```bash
chan init
chan hook install # sets git core.hooksPath to .chan/hooks; installs post-commit
git add . && git commit -m "chore: init project"
```

Inspect `.chan/code.md` — you should see a `## Commit <sha>` entry with AI analysis. **This is the core DX moment: committing automatically builds the knowledge base.** `.chan/code.md` is meant to be committed and shared — `git add .chan/` to see how it feels as a shared artifact.

## 4. Do real work and feel the AI flow

Make a few commits with conventional-ish messages:

```bash
echo 'export const add = (a,b) => a+b' > math.js && git add . && git commit -m "feat: add math.add"
echo 'export const sub = (a,b) => a-b' >> math.js && git add . && git commit -m "feat: add sub"
# a breaking one:
echo 'export const add = (a,b,c) => a+b+c' > math.js && git add . && git commit -m "feat!: add requires third arg"
```

Each commit appends to `.chan/code.md` automatically via the post-commit hook.

## 5. `chan auto` — the headline feature

```bash
chan auto # infer action + message from HEAD
chan auto --commits <sha>,<sha> # cover a range (simulates a PR's commits)
chan auto "rewrite add signature" # you give the message, AI infers only the action
```

Check **both** `CHANGELOG.md` (new entry under `### Added` / `### Changed`) **and** `.chan/code.md` (a `## Action <type>` marker linking the SHAs, with the precise AI `Classification` preserved). This is where you judge whether the inferred action + message feel natural.

## 6. `chan <action> 'msg'` with AI (augmented manual mode)

```bash
chan added "support for three-arg add"
```

With AI on, the message gets augmented/classified and a `## Action` marker is written. Compare with the manual baseline (see step 8).

## 7. `chan release` breaking-change guard

```bash
chan release 1.5.0 # should ERROR: a breaking commit exists but 1.5.0 isn't x.0.0
chan release 2.0.0 # should proceed (breaking-appropriate)
chan release 1.5.0 --ci # should annotate instead of erroring (CI mode)
```

## 8. Compare with the manual flow (no-regression check)

Remove `ai` from `.chanrc` (or `unset OPENCODE_API_KEY` and use a provider without a key) and repeat:

- `chan added "..."` → unchanged manual behavior, writes only `CHANGELOG.md`
- `chan analyze` → no-op with a hint to configure AI
- `chan auto` → clear error: "`chan auto` requires AI to be configured"

This verifies the no-regression path and the clear AI-required errors.

## What to judge (the actual DX questions)

- Does the post-commit hook feel snappy or slow? (Each commit now makes an LLM call — latency matters.)
- Does `.chan/code.md` stay readable after 10–20 commits, or does it get noisy?
- Does `chan auto` infer the right `<action>` and a message you'd accept without editing?
- Does the breaking-change guard fire on the right things and stay quiet on non-breaking work?
- Does `chan auto --commits <prs-commits>` produce one sensible entry from multiple commits (the squash vs merge question)?
- Do you ever feel the urge to bypass AI? That tells you where the manual escape hatch matters.

## Tips

- **Iterating on chan itself while testing:** edits to `packages/chan/src/*.ts` are live immediately (tsx runs the TS source, and the workspace is bind-mounted into the container). No rebuild needed.
- **Test on a real project:** `cd` into that project, `npm link @geut/chan` (already linked globally in the container), add `.chanrc`, `chan hook install`, and work normally for an afternoon. That's the truest DX test.
- **`.chan/code.md` is meant to be committed** — add it to git in your test project to see how it feels as a shared artifact.
- **Provider coverage:** Opencode Zen is the most validated. OpenAI-direct, Anthropic-direct, and Ollama are still untested end-to-end — trying them is part of the HITL validation for issue 08.

## After the test

Anything rough you find becomes a follow-up issue (correlation tuning, prompt tweaks, the GitHub action). The one unchecked acceptance criterion on issue 08 is the HITL e2e validation: real commits → `chan analyze` → `chan auto`/`chan added` → `chan release` producing expected `CHANGELOG.md` and `code.md`. Once you've run that flow on a real provider, mark it done.
8 changes: 8 additions & 0 deletions docs/adr/0001-context-section-in-code-md.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Context section in code.md is machine-owned and exempt from append-only

The Knowledge Base (`.chan/code.md`) is append-only, but it carries one machine-owned section: `## Context`, delimited by `chan:context:start`/`chan:context:end` markers, generated once at `chan init` via an AI Inspection. `chan init` inserts or refills this section when it is missing or empty, and never touches it otherwise — existing Knowledge Base content is never rewritten. Markers make the section unambiguously parseable for later re-reading as prompt context for the analyzer/augmenter (follow-up work). Filesystem gathering (package.json, full README, top-level directory listing) lives in `chan`; chan-ai only prompts over the resulting Codebase Snapshot — the per-commit `tools` mechanism was deliberately not reused for inspection.

## Considered Options

- Strict append-only (never touch an existing file): rejected — users configuring AI after init would have no path to a Context without losing Knowledge Base history.
- Fully regenerable Context (e.g. `--refresh-context`): deferred as a cheap follow-up; insert-if-missing/empty covers the init use case.
Loading