Skip to content
Merged
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ name: CI
on:
push:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
- "LICENSE"
- "*.txt"
pull_request:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
- "LICENSE"
- "*.txt"

permissions:
contents: read
Expand Down
108 changes: 108 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Agent Instructions

## Project overview

**zcode-acp-server** — a Node.js bridge that connects the ZCode agent backend
(`zcode app-server --stdio`) to any ACP-compatible editor (Zed, JetBrains, …)
via JSON-RPC over stdio. Translates ACP protocol requests into ZCode session
methods and streams events back as ACP `session/update` notifications.

## Commands

| Task | Command |
|------|---------|
| Build | `pnpm build` |
| Typecheck | `pnpm typecheck` |
| Test (all) | `pnpm test` |
| Test (single file) | `npx vitest run tests/<file>.test.ts` |
| Lint | `pnpm lint` |
| Format (changed files only) | `pnpm prettier --write <path>` |
| Smoke test | `pnpm smoke` |

**Package manager**: pnpm. **Node**: >=22. **Module system**: ESM (`"type": "module"`).

## Architecture

```
src/
├── index.ts Entry point — wires server to stdio ACP stream
├── server.ts ZcodeAcpServer — shared state + handler registration
├── backend/ ZCode subprocess client (JSON-RPC over stdio)
│ ├── client.ts Spawns + communicates with zcode app-server
│ ├── credentials.ts Reads ~/.zcode/v2/config.json for GLM API key
│ ├── listener.ts EventStreamListener — subscribes to session/events
│ └── types.ts ZCode protocol types
├── handlers/ ACP method handlers
│ ├── session.ts session/new, session/prompt (turn loop), load, resume
│ ├── slash.ts Slash-command interception (/compact, /mcp, etc.)
│ ├── extensions.ts ZCode extensions (fork, rewind, compact, steer, …)
│ ├── dispatch.ts InternalEvent → ACP session/update dispatch
│ ├── io.ts Client notification helpers
│ └── server-requests.ts Server→client requests (permission, elicitation)
├── config/ Discovery + runtime config
│ ├── plugin-commands.ts Load plugin commands from ~/.zcode/cli/
│ ├── skill-discovery.ts Discover Skills from filesystem
│ ├── mcp-discovery.ts Discover MCP servers from config + plugins
│ ├── auto-compact.ts Threshold-based auto-compaction
│ ├── options.ts Config options (model/mode/thought dropdowns)
│ └── runtime-model.ts Model switching overlay
├── translators/ ZCode event → ACP translation
│ ├── event-translator.ts Stream event → InternalEvent
│ ├── projection-differ.ts Snapshot diff for turn-completion reconciliation
│ └── tool-helpers.ts Diff builder, location extractor
├── interaction/ Permission, ExitPlanMode, AskUserQuestion handling
├── quota/ GLM Coding Plan usage API client (/quota command)
└── bin/quota.ts Standalone zcode-quota CLI
```

**Key boundary**: `backend/` talks to the ZCode subprocess. `handlers/` talks to
the ACP client (editor). `translators/` bridges the two event models. Never mix
ZCode protocol types into ACP notifications directly — always translate.

## Conventions

- **Logging**: use `log()` / `warn()` from `src/utils.ts`. Both write to stderr.
**Never use `console.log`** — stdout is the ACP JSON-RPC stream and any stray
output corrupts the protocol.
- **Debug logs**: `log()` is gated behind `ZCODE_ACP_DEBUG=1`. Use it for
verbose diagnostics. `warn()` is always emitted.
- **Formatting**: double quotes, semicolons, trailing commas, 100 char width.
- **Imports**: use `.js` extensions in relative imports (NodeNext resolution).
Sort imports alphabetically (ESLint `sort-imports` rule).
- **Error handling**: best-effort in event handlers — failures are logged via
`warn()`, never thrown into the event loop (would crash the bridge).
- **Tests**: mock `node:fs` with `vi.mock` and Map/Set-based fake filesystem.
See `tests/plugin-commands.test.ts` for the pattern.

## Gotchas

- **ZCode backend version drift**: the backend may change event payloads between
releases. When diff display or event handling breaks, check the raw backend
event with `ZCODE_ACP_DEBUG=1` before changing translator code.
- **`session/prompt` ordering**: subscribe to events BEFORE calling `session/send`
— short turns can complete before a late subscribe catches them.
- **Preempt lock**: concurrent prompts for the same session are serialized via
`withPreemptLock`. Don't bypass it — two simultaneous turns corrupt the listener.
- **AGENTS.md is workspace-scoped**: the global `~/.zcode/AGENTS.md` also exists;
this file takes precedence for this repo.

## Docs to read before sensitive changes

- `docs/ARCHITECTURE.md` — full architecture writeup
- `docs/PROTOCOL.md` — ACP + ZCode protocol mapping
- `docs/DEVELOPMENT.md` — dev setup and debugging guide
- `docs/TROUBLESHOOTING.md` — common issues and diagnostics

## Agent skills

### Issue tracker

GitHub Issues (`gh` CLI). See `docs/agents/issue-tracker.md`.

### Triage labels

Five canonical roles: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.

### Domain docs

Single-context (`CONTEXT.md` + `docs/adr/`). See `docs/agents/domain.md`.
51 changes: 51 additions & 0 deletions docs/agents/domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Domain Docs

How engineering skills should consume this repo's domain documentation when exploring the codebase.

## Before exploring, read these

- **`CONTEXT.md`** at the repo root, or
- **`CONTEXT-MAP.md`** at the repo root (if it exists) — it points to one `CONTEXT.md` per context. Read each file relevant to the current topic.
- **`docs/adr/`** — read ADRs related to the area you are about to work on. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.

If these files don't exist, **continue silently**. Don't flag the absence; don't proactively suggest creating them. The producer skill (`/grill-with-docs`) will lazily create them when terms or decisions are actually resolved.

## File structure

Single-context repo (most repos):

```
/
├── CONTEXT.md
├── docs/adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```

Multi-context repo (root has `CONTEXT-MAP.md`):

```
/
├── CONTEXT-MAP.md
├── docs/adr/ ← system-wide decisions
└── src/
├── ordering/
│ ├── CONTEXT.md
│ └── docs/adr/ ← context-specific decisions
└── billing/
├── CONTEXT.md
└── docs/adr/
```

## Use the glossary's vocabulary

When your output names a domain concept (issue title, refactor proposal, hypothesis, test name), use the term defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.

If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider), or there's a genuine gap (note it for `/grill-with-docs`).

## Flag ADR conflicts

If your output contradicts an existing ADR, call it out explicitly rather than silently overriding:

> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
22 changes: 22 additions & 0 deletions docs/agents/issue-tracker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Issue tracker: GitHub

This repo's issues and PRDs live in GitHub issues. All operations use the `gh` CLI.

## Conventions

- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
- **Read an issue**: `gh issue view <number> --comments`, filter comments with `jq`, and fetch labels at the same time.
- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'`, add `--label` and `--state` filters as needed.
- **Comment on an issue**: `gh issue comment <number> --body "..."`
- **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
- **Close**: `gh issue close <number> --comment "..."`

The repo is inferred from `git remote -v`; when running inside a clone, `gh` handles this automatically.

## When a skill says "publish to the issue tracker"

Create a GitHub issue.

## When a skill says "fetch the relevant ticket"

Run `gh issue view <number> --comments`.
15 changes: 15 additions & 0 deletions docs/agents/triage-labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Triage Labels

Skills use five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.

| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |

When a skill refers to a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.

Edit the right column to match the vocabulary you actually use.
30 changes: 29 additions & 1 deletion src/config/auto-compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
* the prompt response.
*/

import { randomUUID } from "node:crypto";

import type * as acp from "@agentclientprotocol/sdk";

Check warning on line 16 in src/config/auto-compact.ts

View workflow job for this annotation

GitHub Actions / Build & Test (22.x)

Expected 'all' syntax before 'single' syntax

import { compact } from "../handlers/extensions.js";
import type { ZcodeAcpServer } from "../server.js";
import { log, warn } from "../utils.js";

Check warning on line 20 in src/config/auto-compact.ts

View workflow job for this annotation

GitHub Actions / Build & Test (22.x)

Expected 'multiple' syntax before 'single' syntax
import { sendTextChunk } from "../handlers/io.js";

/** ENV: `ZCODE_ACP_AUTO_COMPACT_THRESHOLD` — absolute token count (0 = disabled). */
export function autoCompactThreshold(): number {
Expand Down Expand Up @@ -58,12 +61,37 @@
if (used < threshold) return;

log(`auto-compact: contextUsed=${used} >= threshold=${threshold}, compacting…`);
const msgId = randomUUID();
await sendTextChunk(
cx,
acpSid,
`🔄 auto-compact: context usage ${used.toLocaleString()} ≥ threshold ${threshold.toLocaleString()}, compressing…`,
msgId,
);
try {
// compact() handles: session/compact → waitForTurnIdle → emitInitialUsage.
await compact(server, { sessionId: acpSid }, cx);
const result = (await compact(server, { sessionId: acpSid }, cx)) as {
__lockTimeout?: boolean;
};
if (result.__lockTimeout) {
await sendTextChunk(
cx,
acpSid,
"⚠ auto-compact timed out (300s) — backend may still be processing",
msgId,
);
} else {
await sendTextChunk(cx, acpSid, "✓ auto-compact: context compressed", msgId);
}
log("auto-compact: done");
} catch (e) {
warn(`auto-compact: compact failed (${e instanceof Error ? e.message : String(e)})`);
await sendTextChunk(
cx,
acpSid,
`⚠ auto-compact failed: ${e instanceof Error ? e.message : String(e)}`,
msgId,
);
// Best-effort: never break the prompt response.
}
}
Loading
Loading