From b798df59d3ec8afb3601f2d4c9a92a6a254fe816 Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Thu, 13 Aug 2026 01:09:24 +0800 Subject: [PATCH] chore: add project standards (AGENTS.md, CI workflows, devlog, e2e scaffold) Bring billion-context-opencode to opencode-acp's contributing standard: - AGENTS.md (~436 lines): MIT monorepo dev spec. Documents the dual-shape mechanism (Object.assign + Plugin.define identity), @bili/core private workspace, call<->result pairing invariant, splice-in-place reassembly, per-session lock contract, gh-guard note, git-safety rules, and the absolute PR-merge prohibition (human-only). - .github/workflows/ci.yml: matrix Node 22/24, typecheck+build+test (job 'build') - .github/workflows/pr-checks.yml: runs check-pr.sh (job 'pr-validation') - .github/workflows/release.yml: release-branch merge -> tag + publish ONLY billion-context-opencode (--workspace) + GitHub Release; prerelease (-suffix) -> npm dev tag; workflow_dispatch force override - scripts/ci/check-pr.sh: branch regex, devlog presence, changelog-on-bump - devlog/: README + REQ/WORKLOG/DESIGN templates + this task's entry - scripts/e2e/: README (planned Docker harness) + run-e2e.sh stub - README.md: append Changelog section (### v0.1.0 baseline) Verified: check-pr.sh passes on this branch, typecheck clean, yaml parses. --- .github/workflows/ci.yml | 25 + .github/workflows/pr-checks.yml | 20 + .github/workflows/release.yml | 121 +++++ AGENTS.md | 436 ++++++++++++++++++ README.md | 7 + devlog/2026-08-13_project-standards/REQ.md | 55 +++ .../2026-08-13_project-standards/WORKLOG.md | 75 +++ devlog/DESIGN.template.md | 54 +++ devlog/README.md | 61 +++ devlog/REQ.template.md | 50 ++ devlog/WORKLOG.template.md | 74 +++ scripts/ci/check-pr.sh | 115 +++++ scripts/e2e/README.md | 63 +++ scripts/e2e/run-e2e.sh | 12 + 14 files changed, 1168 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pr-checks.yml create mode 100644 .github/workflows/release.yml create mode 100644 AGENTS.md create mode 100644 devlog/2026-08-13_project-standards/REQ.md create mode 100644 devlog/2026-08-13_project-standards/WORKLOG.md create mode 100644 devlog/DESIGN.template.md create mode 100644 devlog/README.md create mode 100644 devlog/REQ.template.md create mode 100644 devlog/WORKLOG.template.md create mode 100644 scripts/ci/check-pr.sh create mode 100644 scripts/e2e/README.md create mode 100644 scripts/e2e/run-e2e.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8c947dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [22, 24] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm run build + - run: npm run test diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..eb5eec2 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,20 @@ +name: PR Checks + +on: + pull_request: + branches: [master] + +jobs: + pr-validation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run PR validation + run: bash scripts/ci/check-pr.sh "${{ github.head_ref }}" "origin/${{ github.base_ref }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5a15f4d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,121 @@ +name: Release + +on: + push: + branches: [master] + workflow_dispatch: + inputs: + force: + description: "Force publish even if not a release branch merge" + required: false + default: "false" + +# Release is triggered ONLY on push to master (not on PRs). A separate auto-tag +# workflow would not work: GitHub Actions does not allow GITHUB_TOKEN-pushed +# workflows to trigger other workflows. Everything (tag + publish + release) is +# done in this one job. Only the `billion-context-opencode` package is published; +# @bili/core is private and never published. +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check commit type + id: check + run: | + MERGE_MSG=$(git log -1 --pretty=%B) + MERGE_TITLE=$(echo "$MERGE_MSG" | head -1) + echo "Commit title: $MERGE_TITLE" + + FORCE="${{ github.event.inputs.force }}" + IS_RELEASE="false" + + # Pattern 1: Standard merge of a release branch + # "Merge pull request #7 from ranxianglei/2026-08-13_release-v0.1.0" + if echo "$MERGE_TITLE" | grep -qE 'Merge pull request #[0-9]+ from .*[0-9]{4}-[0-9]{2}-[0-9]{2}_release-v'; then + IS_RELEASE="true" + echo "Release branch detected (standard merge)" + # Pattern 2: Squash merge of a release PR + # "release: v0.1.0 — monorepo + dual-shape single package (#9)" + elif echo "$MERGE_TITLE" | grep -qE '^release: v[0-9]+\.[0-9]+\.[0-9]+'; then + IS_RELEASE="true" + echo "Release branch detected (squash merge)" + elif [ "$FORCE" = "true" ]; then + IS_RELEASE="true" + echo "Force publish requested via workflow_dispatch" + else + echo "Not a release branch merge — skipping" + fi + + echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + + - name: Read version + if: steps.check.outputs.is_release == 'true' + id: version + run: | + VERSION=$(node -p "require('./packages/billion-context-opencode/package.json').version") + TAG="v${VERSION}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + # Detect prerelease versions (e.g. 0.2.0-dev.1, 0.2.0-beta.2) + if echo "$VERSION" | grep -q -- '-'; then + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + echo "npm_tag=dev" >> "$GITHUB_OUTPUT" + echo "Prerelease detected: $VERSION → npm tag: dev" + else + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "npm_tag=latest" >> "$GITHUB_OUTPUT" + echo "Stable release: $VERSION → npm tag: latest" + fi + + - uses: actions/setup-node@v4 + if: steps.check.outputs.is_release == 'true' + with: + node-version: 22 + cache: npm + registry-url: https://registry.npmjs.org + + - if: steps.check.outputs.is_release == 'true' + run: npm ci + + - if: steps.check.outputs.is_release == 'true' + run: npm run build + + - name: Create tag + if: steps.check.outputs.is_release == 'true' + run: | + TAG="${{ steps.version.outputs.tag }}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists — skipping" + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "$TAG" -m "release $TAG (auto-tagged on merge)" + git push origin "$TAG" + echo "Created tag $TAG" + fi + + - name: Publish to npm + if: steps.check.outputs.is_release == 'true' + run: | + NPM_TAG="${{ steps.version.outputs.npm_tag }}" + echo "Publishing billion-context-opencode@${{ steps.version.outputs.version }} with tag: $NPM_TAG" + npm publish --workspace billion-context-opencode --tag "$NPM_TAG" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create GitHub Release + if: steps.check.outputs.is_release == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.tag }} + generate_release_notes: true + prerelease: ${{ steps.version.outputs.is_prerelease }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1dd1c31 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,436 @@ +# billion-context-opencode Development Specification + +> **This document is the highest-priority specification for this project. All developers (including AI Agents) MUST comply unconditionally.** + +--- + +## 1. Project Overview + +### 1.1 What Is billion-context-opencode + +**billion-context-opencode** is an [opencode](https://opencode.ai) plugin that wires the [acp-kernel](https://github.com/ranxianglei/acp-kernel) compression engine into opencode, enabling **model-driven context management**: the model itself decides when and what to compress into high-fidelity summaries, via four `bili_`-prefixed tools (`bili_compress`, `bili_decompress`, `bili_search`, `bili_status`). + +The distinguishing feature of this package is its **dual-shape entry**: a single default export loads on **both opencode V1** (callable plugin factory) and **opencode V2** (`.id` + `.setup(ctx)`). See [§2.3](#23-the-dual-shape-mechanism-key-insight) for how this works. + +The package depends on `acp-kernel@0.0.19` and a private workspace `@bili/core` (framework-agnostic glue). It is an independent implementation — it does **not** depend on or conflict with `opencode-acp`. + +### 1.2 Tech Stack + +| Category | Technology | +| ------------------ | ----------------------------------------------------------------- | +| Language | TypeScript (strict, `noUncheckedIndexedAccess`, ESM) | +| Runtime | Node.js (CI matrix: 22, 24) | +| Build | `tsup` (single ESM bundle, kernel + zod + `@bili/core` inlined) | +| Test Runner | Node.js built-in: `node --import tsx --test tests/*.test.ts` | +| Package Manager | npm (workspaces monorepo) | +| Validation | `zod` | +| Compression engine | `acp-kernel` (consumed unmodified via public API) | + +### 1.3 Repository Info + +| Field | Value | +| ---------------- | ----------------------------------------------------------- | +| npm package | `billion-context-opencode` (the **only** published package) | +| Current version | `0.1.0` | +| GitHub | https://github.com/ranxianglei/billion-context-opencode | +| License | MIT | +| Default branch | `master` | + +--- + +## 2. Architecture + +### 2.1 Module Map + +``` +billion-context-opencode/ (npm-workspaces monorepo) +├── packages/ +│ ├── core/ @bili/core — PRIVATE workspace, framework-agnostic glue +│ │ ├── package.json name "@bili/core", private, main ./src/index.ts (consumed as raw TS) +│ │ └── src/ +│ │ ├── index.ts barrel export +│ │ ├── runtime.ts AcpRuntime: per-session state, per-session async lock, cores/model-limit/turn/config caches, LRU eviction (MAX_SESSIONS_IN_MEMORY=32) +│ │ ├── state.ts SessionStateStore: load/save CompressionState to ~/.cache/opencode-bili-acp/.acp.json +│ │ ├── config.ts resolveConfig: AdapterConfig → kernel defaultConfig (defers nudge thresholds to kernel); FALLBACK_LIMIT=200000 +│ │ ├── options.ts numOpt / strArrayOpt / boolOpt — option coercion helpers +│ │ ├── tokens.ts estimateTokens, collectCoveredMessageIds +│ │ ├── search-index.ts buildSearchDocs (compressed blocks + covered messages) +│ │ ├── compress-tool.ts makeCompressTool (bili_compress); ToolDef / ToolContext types +│ │ ├── decompress-tool.ts makeDecompressTool (bili_decompress) +│ │ ├── search-tool.ts makeSearchTool (bili_search) +│ │ ├── status-tool.ts makeStatusTool (bili_status) +│ │ ├── system-prompt.ts SYSTEM_PROMPT — compression philosophy + tool guide +│ │ └── log.ts debug / warn logging (BILI_ACP_DEBUG=1) +│ │ +│ └── billion-context-opencode/ the ONE published package +│ ├── package.json name "billion-context-opencode", version 0.1.0, MIT, main ./dist/index.js, deps acp-kernel@0.0.19 +│ ├── src/ +│ │ ├── index.ts dual-shape entry (see §2.3): V1 plugin factory + V2 setup +│ │ ├── messages-v1.ts V1 opencode msgs ↔ kernel CoreMessage[] (OctoMessage shape) +│ │ └── messages-v2.ts V2 opencode msgs ↔ kernel CoreMessage[] (V2Message shape) +│ ├── tests/ config.test.ts, messages.test.ts, state.test.ts +│ ├── tsconfig.json extends ../../tsconfig.base.json +│ └── tsup.config.ts bundles acp-kernel + zod + @bili/core inline (noExternal) +│ +├── tsconfig.base.json shared: strict, noUncheckedIndexedAccess, ES2022, bundler moduleResolution +├── package.json monorepo root: private, workspaces ["packages/*"] +├── README.md +└── smoke.mjs end-to-end check against dist/ +``` + +**Why a monorepo?** `@bili/core` holds all host-agnostic logic (runtime, config, state, tokens, the four tools, the system prompt). `packages/billion-context-opencode` is a thin host adapter that only knows how to convert opencode's message shapes (V1 `OctoMessage` and V2 `V2Message`) into the kernel's `CoreMessage[]` and reassemble results. This keeps the kernel-facing logic shared and testable, and lets the host adapter stay small. + +### 2.2 Core Data Flow + +``` +opencode (V1 transform hooks OR V2 "context" session hook) + │ + ├─ V1 path — biliAcpPluginV1(input, options) returns OctoHooks: + │ • experimental.chat.system.transform → setModelLimit + push SYSTEM_PROMPT + │ • experimental.chat.messages.transform → runPipelineV1(output.messages) + │ + └─ V2 path — setupV2(ctx): + • ctx.tool.transform(...) → add the 4 bili_ tools + • ctx.session.hook("context", event) → runPipelineV2(event.messages) + • ctx.catalog.model.list() → resolve model context limit + │ + ▼ +messages-v1.ts / messages-v2.ts octoToCoreMessages / v2ToCoreMessages → CoreMessage[] + │ + ▼ +AcpRuntime (per session; all work serialized via acquireLock) + ├─ stateFor(sid) load CompressionState from disk + ├─ collectCoveredMessageIds + estimateTokens + ├─ core.processTurn({messages, state, config, tokenCount, renderTags:"text-only"}) + │ kernel decides: prune compressed ranges, inject nudges, assign mNNNNN refs + ├─ setCores / cacheTurn / save(state) + └─ reassemble (v1 or v2) → splice rebuilt messages in place on the host array + │ + ▼ +bili_compress / bili_decompress / bili_search / bili_status (registered tools) + └─ each calls AcpRuntime under the per-session lock, then persists state +``` + +Both V1 and V2 feed the **same** `AcpRuntime` + kernel pipeline; they differ only in message shape conversion (`messages-v1.ts` vs `messages-v2.ts`) and how system prompt / model limit / tools are registered. + +### 2.3 The Dual-Shape Mechanism (Key Insight) + +The package's entire reason for existing as one entry is this export (`packages/billion-context-opencode/src/index.ts`): + +```typescript +export default Object.assign(biliAcpPluginV1, { + id: "billion-context-opencode", + setup: setupV2, +}) +``` + +**Why this works on both opencode major versions:** + +1. **`Object.assign(target, source)` returns `target`** — the *same* function object — with `source`'s enumerable own properties (`id`, `setup`) copied onto it. So the result is still `biliAcpPluginV1` (still callable), now carrying `.id` and `.setup`. +2. **opencode V1 loader** sees a function and calls it as `biliAcpPluginV1(input, options)`, which returns an `OctoHooks` object (the V1 transform hooks + `tool` map). The extra `.id`/`.setup` properties are simply ignored. +3. **opencode V2 loader** reads `.id` and calls `.setup(ctx)`. It never *calls* the function itself, so the function body (the V1 factory) never runs in V2. The crucial enabler: **`Plugin.define` in the V2 SDK (`@opencode-ai/plugin`) is an IDENTITY function** — `function define(plugin){ return plugin }` — with **no branding Symbol, no runtime validation**. V2 therefore accepts *any* object with `{ id, setup }`. Because JS functions are objects, a callable that also carries `{ id, setup }` satisfies both loaders simultaneously. + +**Load-bearing consequence:** the package deliberately does **NOT** import `@opencode-ai/plugin`. `setupV2`'s parameter is a structural `PluginSetupContext` type defined inline in `index.ts`. This keeps the built `dist/index.js` free of the SDK at runtime (zero package-resolution conflicts between V1 and V2 environments) while remaining type-safe at compile time. + +> **Do not refactor the dual-shape export into two separate entries** without a migration plan. It is the load-bearing mechanism that makes one package serve both opencode versions. Any change here MUST add a devlog `DESIGN.md`. + +### 2.4 Message Conversion (V1 vs V2) + +| Concern | V1 (`messages-v1.ts`) | V2 (`messages-v2.ts`) | +| --- | --- | --- | +| Host message shape | `OctoMessage { info, parts[] }` | `V2Message { id?, role, content[] }` | +| Conversion | `octoToCoreMessages` → `{cores, partIdToCoreIds}` | `v2ToCoreMessages` → `{cores, origin, partToCoreIds}` | +| Session id | `deriveSessionId(msgs)` (scan for first non-empty `info.sessionID`) | `event.sessionID` (provided by the hook) | +| Model limit | `system.transform` → `input.model.limit.context` | `ctx.catalog.model.list()` lookup by `{providerID,id}` | +| Reassembly | `reassemble` — tool part kept only if call **and** result both survive | `reassemble` — same call+result pairing rule; media-only msgs preserved in place | +| Nudge message | `makeNudgeMessage` (synthetic `OctoMessage`, role `user`) | `makeNudgeMessage` (`V2Message`, role `user`) | + +Both converters enforce a **call↔result pairing invariant**: a tool-call is emitted only when its matching tool-result also survived the kernel's pruning, and vice versa. Dropping one half would produce malformed provider history. Preserve this invariant in any change. + +### 2.5 Configuration + +Plugin options (declared as `AdapterConfig` in `packages/core/src/config.ts`): + +| Option | Default | Description | +| --- | --- | --- | +| `modelContextLimit` | auto (model limit, else `200000`) | Token limit for nudge math. Env `BILI_MODEL_CONTEXT_LIMIT` overrides. | +| `preserveRecentMessages` | `5` | Recent messages always kept visible. | +| `protectedTools` | `[]` | Tool-result message ids never compressed. | +| `debug` | `false` | Verbose logging. Env `BILI_ACP_DEBUG=1` also enables. | +| `coreOverrides` | `{}` | Raw `acp-kernel` config overrides (advanced; nudge thresholds default to the kernel's own adaptive values). | + +`resolveConfig` **defers all nudge/threshold math to the kernel's `defaultConfig`** (which scales growth floor/cap from `modelContextLimit`). Do not re-implement thresholds in the adapter. + +### 2.6 Storage Paths + +| What | Path | Notes | +| --- | --- | --- | +| Per-session state | `~/.cache/opencode-bili-acp/.acp.json` | `SessionStateStore` (disk JSON) | +| Built artifact | `packages/billion-context-opencode/dist/index.js` | self-contained, zero runtime deps | + +### 2.7 Bundling + +`tsup` (`packages/billion-context-opencode/tsup.config.ts`) marks `acp-kernel`, `zod`, `zod/v4`, and `@bili/core` as **`noExternal`** — they are bundled inline. The published `dist/index.js` is self-contained with **zero runtime dependencies**. This is intentional: it avoids version-resolution conflicts inside opencode's plugin sandbox. Do not add runtime `dependencies` to the published package without strong justification. + +--- + +## 3. Development Standards + +### 3.1 Build Commands + +All commands run from the repo root unless noted. + +```bash +npm install # install workspaces (run once / after dependency changes) +npm run build # = npm run build --workspace billion-context-opencode (tsup bundle) +npm run typecheck # = npm run typecheck --workspaces +npm run test # = npm run test --workspace billion-context-opencode (node --import tsx --test tests/*.test.ts) +node smoke.mjs # end-to-end check against dist/ (run after build) +``` + +Per-package (run from inside a package dir): + +```bash +npm run typecheck # tsc --noEmit for that package +npm test # tests/*.test.ts (billion-context-opencode only; @bili/core has no tests) +npm run build # tsup (billion-context-opencode only) +``` + +### 3.2 Build Output + +- `packages/billion-context-opencode/dist/index.js` — bundled ESM (the published artifact) +- `packages/billion-context-opencode/dist/index.js.map` — sourcemap +- Published files (per `main` + npm defaults): `dist/`, `README.md`, `LICENSE` +- `@bili/core` is **never published** (`"private": true`, `"version": "0.0.0"`); it is consumed as raw TS via the workspace at build time and inlined into the bundle. + +### 3.3 Testing + +**Test runner:** `node --import tsx --test tests/*.test.ts` + +**Test location:** `packages/billion-context-opencode/tests/` (flat). Current files: `config.test.ts`, `messages.test.ts`, `state.test.ts`. + +CI (`ci.yml`) runs `typecheck` + `build` + `test` on Node 22 and 24. + +### 3.4 Local Deployment / Smoke + +```bash +npm run build && node smoke.mjs # verify the bundle loads and tools register +``` + +For a clean opencode instance loading only this plugin, see the `test-clean/` harness and `README.md` "Clean test environment". + +### 3.5 npm Publishing + +**Only** `billion-context-opencode` is published (it is the only public package). `@bili/core` is private and must never be published. + +```bash +# Build + verify locally +npm run build +node smoke.mjs + +# Publish ONLY the public package +npm publish --workspace billion-context-opencode +``` + +**Releases are automated via CI** — see [§5.4](#54-release-workflow-automated-via-ci). Manual publish is a fallback only. + +--- + +## 4. Code Change Guidelines + +### 4.1 Module Dependencies + +``` +packages/core (no host knowledge — leaf) + ↑ consumed via workspace +packages/billion-context-opencode/src/index.ts (host adapter: V1 + V2) + ├─ messages-v1.ts (V1 shape conversion) + ├─ messages-v2.ts (V2 shape conversion) + └─ @bili/core (runtime, config, state, tools, system prompt) + └─ acp-kernel (the compression engine, consumed unmodified) +``` + +**Rules:** + +- `@bili/core` knows **nothing** about opencode (no V1/V2 types). It is host-agnostic. +- The host adapter (`packages/billion-context-opencode`) knows opencode shapes (V1/V2) and `@bili/core`'s public API — nothing else. +- `acp-kernel` is used **unmodified** via its public API. Do not fork or patch it. + +### 4.2 Common Patterns + +- **Per-session lock**: `AcpRuntime.acquireLock(sessionId, fn)` serializes all async work for a session. All transform hooks and all `bili_` tools MUST run inside it — concurrent compress writes to the same session file corrupt state. Callers MUST `.catch()` the returned promise. +- **State identity for cache freshness**: `AcpRuntime` caches a `processTurn` result and invalidates it by **reference equality** of the `state`/`cores` objects. A compress writes a new state object, so the cache auto-stales. Preserve this contract. +- **Splice-in-place reassembly**: both V1 and V2 rebuild the host's message array **in place** (`msgs.splice(0, msgs.length, ...reassembled)`). opencode passes the same array reference; do not return a new array. +- **Structural types over SDK imports**: the package avoids importing `@opencode-ai/plugin` at runtime (see [§2.3](#23-the-dual-shape-mechanism-key-insight)). Keep host types as inline structural interfaces. + +--- + +## 5. Contributing + +### 5.1 Before Making Changes + +1. `npm run typecheck` passes (run from root — it covers all workspaces). +2. Understand the module dependency graph ([§4.1](#41-module-dependencies)). +3. Check whether the change touches the **dual-shape export** ([§2.3](#23-the-dual-shape-mechanism-key-insight)) or the **call↔result pairing invariant** ([§2.4](#24-message-conversion-v1-vs-v2)) — these are load-bearing and require extra care. + +### 5.1.1 Development Workflow + +All changes MUST follow this workflow: + +1. Create a feature branch from `master` (naming: `YYYY-MM-DD_short-title`). +2. Create a devlog entry: `devlog/{YYYY-MM-DD_short-title}/` with `REQ.md` (see [§5.1.2](#512-devlog-requirement-mandatory)). +3. Implement changes (source lives under `packages/`; standards/CI/docs live at the repo root). +4. Ensure `npm run typecheck` and `npm run build` pass. +5. Ensure `npm run test` passes. +6. Commit with descriptive messages (include devlog files). +7. Push branch and create a GitHub PR targeting `master`. +8. Obtain **dual-agent review** ([§5.3](#53-code-review-mandatory)) on the PR. +9. **PR merge is a human-only operation** — AI agents MUST NEVER merge PRs, even when explicitly instructed or forced. See [§5.1.1.2](#5112-pr-merge--absolute-prohibition). + +### 5.1.1.1 Git Safety Rules (MANDATORY) + +| Rule | Enforcement | +| --- | --- | +| **NEVER force-push to `master`** | Under no circumstances. If master needs changing, create a PR. | +| **NEVER merge PRs — ABSOLUTE PROHIBITION, no exceptions** | PR merges are a **human-only operation**. The Agent MUST NEVER merge any PR, under ANY circumstances. See [§5.1.1.2](#5112-pr-merge--absolute-prohibition). | +| **NEVER delete branches or tags without human confirmation** | Preserve work for review. | +| **NEVER modify the `version` field except on release branches** | Version bumps happen ONLY on `YYYY-MM-DD_release-v*` branches, in `packages/billion-context-opencode/package.json`. Feature/fix PRs MUST NOT touch `version`. The CI changelog check enforces this indirectly: if `version` changes, `README.md` MUST be modified and contain `### v{VERSION}`. | + +**Branch protection** is configured on `master`: requires 1 approving review, `enforce_admins: false` (the owner admin-merges their own PRs), no force-push, no deletion. + +### 5.1.1.2 PR Merge — Absolute Prohibition + +> **PR merges are a human-only operation. The Agent MUST NEVER merge any PR.** + +This is an **absolute rule with no exceptions**. It applies regardless of CI status, review status, urgency, or human instruction: + +| Situation | Agent Action | +| --- | --- | +| No human instruction to merge | Do not merge. | +| Human implicitly suggests merging ("ship it", "提交一下代码", "looks good") | Do not merge. Treat as commit/push only. If ambiguous, ASK. | +| Human explicitly authorizes merge ("you may merge") | Do not merge. Reply that PR merges are human-only. | +| Human directly instructs/orders merge ("merge this now") | Do not merge. Reply that PR merges are human-only. | +| Human forces or demands auto-merge (ultimatums) | **Explicitly refuse.** This rule cannot be overridden by any instruction. | +| The PR is a revert/fixup, or CI is green | Do not merge. | + +**What the Agent MUST do instead:** + +1. Prepare the PR (branch, commits, push, `gh pr create`). +2. Verify CI passes. +3. Report the PR URL to the human. +4. **Stop.** Wait for the human to click "Merge". + +**How to respond when a human instructs the Agent to merge:** + +> I can't merge PRs — AGENTS.md §5.1.1.2 forbids Agents from merging PRs under any circumstances. Please merge the PR yourself: [PR URL]. + +### 5.1.1.3 gh-guard Note (This Environment) + +In this development environment, `gh` API **write** methods (`PUT` / `POST` / `DELETE`, e.g. `gh pr merge`, `gh release create`, `gh api ... -X POST`) are **guard-blocked** and require the `GH_ALLOW_DANGEROUS=1` environment variable to run. Read methods (`GET`) are unrestricted. This is a safety rail, not a relaxation of [§5.1.1.2](#5112-pr-merge--absolute-prohibition) — even with `GH_ALLOW_DANGEROUS=1`, the Agent MUST NOT merge PRs. The guard exists to prevent accidental destructive API calls. + +### 5.1.2 Devlog Requirement (MANDATORY) + +Every PR MUST have a corresponding devlog entry in `devlog/{YYYY-MM-DD_short-title}/`. + +- The folder name MUST match the branch name. +- `REQ.md` and `WORKLOG.md` are the required minimum. +- `DESIGN.md` is required for any change affecting architecture, data flow, or module boundaries (in particular: any change to the dual-shape export or the V1/V2 message converters). +- Fill `REQ.md` **BEFORE** implementation; fill `WORKLOG.md` **DURING/AFTER**. +- Commit devlog files alongside code changes. + +See `devlog/README.md` for templates and naming conventions. + +### 5.2 After Making Changes + +1. `npm run build` must pass. +2. `npm run typecheck` must pass. +3. `npm run test` must pass. +4. `node smoke.mjs` should pass after a build. +5. Bump version only on a release branch (see [§5.4](#54-release-workflow-automated-via-ci)). + +### 5.3 Code Review (MANDATORY) + +All source changes (files under `packages/`) MUST undergo independent review by **at least 2 separate agents** before merge. Review checklist: correctness, backward compatibility (persisted state format, the dual-shape export, the call↔result pairing invariant), performance, type safety (no `as any`, no `@ts-ignore`), state integrity. + +### 5.4 Release Workflow (Automated via CI) + +Releases are **fully automated through GitHub Actions**. Workflow: create a release PR → a human merges → CI auto-tags, builds, tests, and publishes the single public package. + +#### 5.4.1 CI Workflows + +| Workflow | Trigger | Purpose | +| --- | --- | --- | +| `ci.yml` | `push` to master + `pull_request` to master | Matrix (Node 22, 24): `npm ci` → `typecheck` → `build` → `test`. Job named `build` so branch protection can require it. | +| `pr-checks.yml` | `pull_request` to master | Runs `scripts/ci/check-pr.sh` (branch name, devlog presence, changelog-on-version-bump). Job named `pr-validation`. | +| `release.yml` | `push` to master (+ `workflow_dispatch`) | Detects release-branch merge, tags `v{VERSION}`, publishes `billion-context-opencode`, creates GitHub Release. | + +**Why one workflow for tag + publish?** GitHub Actions does not allow workflows pushed by `GITHUB_TOKEN` to trigger other workflows. A separate auto-tag workflow would not fire release.yml. The unified `release.yml` does everything in one job. + +#### 5.4.2 Release Process (Step-by-Step) + +**Step 1 — Create a release branch** from master, named `YYYY-MM-DD_release-v{VERSION}`: + +```bash +git checkout master && git pull origin master +git checkout -b YYYY-MM-DD_release-v{VERSION} +``` + +**Step 2 — Bump version + changelog + devlog:** + +- Edit `packages/billion-context-opencode/package.json` → set `version`. +- Edit `README.md` → append a changelog entry under `## Changelog` with a `### v{VERSION}` header. +- Create `devlog/YYYY-MM-DD_release-v{VERSION}/REQ.md` + `WORKLOG.md`. + +> **Prereleases**: a version containing `-` (e.g. `0.2.0-dev.1`) publishes to the npm `dev` tag and marks the GitHub Release as a prerelease. A version without `-` publishes to `latest`. + +**Step 3 — Verify, commit, push, create PR:** + +```bash +bash scripts/ci/check-pr.sh YYYY-MM-DD_release-v{VERSION} origin/master # must PASS +git add -A && git commit -m "release: v{VERSION} — title" +git push origin YYYY-MM-DD_release-v{VERSION} +gh pr create --title "release: v{VERSION} — title" --body "..." +``` + +**Step 4 — Merge PR (human-only; Agent MUST NOT merge — see [§5.1.1.2](#5112-pr-merge--absolute-prohibition)).** + +Wait for CI (`pr-validation`, `build`) to pass, then a human merges. + +**Step 5 — Auto-publish (fully automated):** merging the PR triggers `release.yml`: it detects the release-branch merge, creates `v{VERSION}` tag, runs `npm ci` → `npm run build` → `npm publish --workspace billion-context-opencode`, and creates the GitHub Release. `workflow_dispatch` with `force: true` publishes outside a release-branch merge (manual override). + +**Step 6 — Verify:** + +```bash +npm view billion-context-opencode version +gh release view v{VERSION} --repo ranxianglei/billion-context-opencode +``` + +#### 5.4.3 Prerequisites + +- **`NPM_TOKEN` secret** set in GitHub repo settings (Settings → Secrets → Actions). +- **Branch protection** on `master` requires the `pr-validation` and `build` checks to pass before merge, plus 1 approving review. +- **Release branch naming** must follow `YYYY-MM-DD_release-v{VERSION}` for auto-tagging. + +#### 5.4.4 Manual Publish (Legacy Fallback) + +If CI is down or `NPM_TOKEN` is misconfigured: + +```bash +git checkout master && git pull origin master +git status --porcelain # MUST be empty +npm run build +npm pack --dry-run 2>&1 # privacy audit +npm publish --workspace billion-context-opencode +npm view billion-context-opencode version +``` + +Only as a fallback. The automated workflow ([§5.4.2](#542-release-process-step-by-step)) is the standard process. + +### 5.5 Commit Convention + +Use descriptive commit messages. Examples: + +- `feat: wire V2 catalog model-limit resolution` +- `fix: preserve media-only messages in V2 reassembly` +- `release: v0.1.0 — monorepo + dual-shape single package` +- `chore: add project standards (AGENTS.md, CI, devlog)` diff --git a/README.md b/README.md index 11785ec..ffa4330 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,10 @@ src/ ## License MIT + +## Changelog + +### v0.1.0 — Monorepo + dual-shape single package (PR #7, #9) + +Initial npm-workspaces monorepo: `@bili/core` (private) + `billion-context-opencode` (published). +One dual-shape entry loads on opencode V1 (callable) and V2 (`.id`/`.setup`) via `Object.assign`. diff --git a/devlog/2026-08-13_project-standards/REQ.md b/devlog/2026-08-13_project-standards/REQ.md new file mode 100644 index 0000000..846101c --- /dev/null +++ b/devlog/2026-08-13_project-standards/REQ.md @@ -0,0 +1,55 @@ +# REQ - Project standards (AGENTS.md, CI, devlog, e2e skeleton) + +- Task ID: `2026-08-13_project-standards` +- Home Repo: `billion-context-opencode` +- Created: 2026-08-13 +- Status: Done +- Priority: P1 +- Owner: Sisyphus-Junior (delegated) +- References: derived from `opencode-acp` standards (adapted, not copied) + +## 1. Background & Problem Statement + +- **Context**: `billion-context-opencode` is a new npm-workspaces monorepo + (`@bili/core` private + `billion-context-opencode` published) with a dual-shape + entry that loads on opencode V1 and V2. It had source code and a root + `package.json` but no project standards, no CI, no devlog convention, and no + PR/release automation. +- **Current behavior (symptom)**: No `AGENTS.md`, no GitHub Actions, no devlog + templates, no `check-pr.sh`. Contributors have no enforced conventions; releases + are manual. +- **Expected behavior**: A focused `AGENTS.md` spec, three CI workflows + (ci/pr-checks/release), devlog templates + README, a PR-validation script, and an + e2e harness skeleton. +- **Impact**: Establishes the contributing/release process and CI gates from the + start, before the package is published more widely. + +## 3. Constraints & Non-Goals + +- **Constraints**: + - DO NOT switch branches (worktree is on `2026-08-13_project-standards` synced to + master `e533c65`). + - DO NOT modify anything under `packages/` (source code) — only top-level + standards/CI/docs files. + - License is MIT (not AGPL). Adapt `opencode-acp`'s structure, do NOT copy it + verbatim; keep AGENTS.md focused (~250–400 lines). + - Only `billion-context-opencode` is publishable; `@bili/core` is private. +- **Non-Goals**: No source changes, no e2e harness implementation (skeleton only), + no commit/push/PR (files only — human reviews and commits). + +## 4. Acceptance Criteria (must be testable) + +- **Deliverables present**: + - [x] `AGENTS.md` with the required sections (overview, architecture incl. + dual-shape mechanism, configuration, dev standards, contributing incl. + PR-merge prohibition, release workflow, git safety rules, gh-guard note). + - [x] `.github/workflows/{ci,pr-checks,release}.yml`. + - [x] `devlog/{README,REQ.template,WORKLOG.template,DESIGN.template}.md`. + - [x] `scripts/ci/check-pr.sh`. + - [x] `scripts/e2e/{README.md,run-e2e.sh}` skeleton. + - [x] `README.md` Changelog section appended (existing content preserved). +- **Verification**: + - [x] `bash scripts/ci/check-pr.sh 2026-08-13_project-standards origin/master` PASSES. + - [x] `npm run typecheck --workspaces` still passes (no source touched). + - [x] `bash -n scripts/ci/check-pr.sh` syntax valid. + - [x] Workflow YAML files parse. diff --git a/devlog/2026-08-13_project-standards/WORKLOG.md b/devlog/2026-08-13_project-standards/WORKLOG.md new file mode 100644 index 0000000..b3db3db --- /dev/null +++ b/devlog/2026-08-13_project-standards/WORKLOG.md @@ -0,0 +1,75 @@ +# WORKLOG - Project standards (AGENTS.md, CI, devlog, e2e skeleton) + +- Task ID: `2026-08-13_project-standards` +- Home Repo: `billion-context-opencode` +- Status: Done +- Updated: 2026-08-13 + +## 1. Summary + +- **What was done**: Added top-level project standards for the monorepo — a focused + `AGENTS.md` dev spec, three GitHub Actions workflows (ci / pr-checks / release), + devlog templates + README, a `scripts/ci/check-pr.sh` PR validator, an e2e + harness skeleton, and a Changelog entry in README.md. +- **Why**: Establish contributing conventions, CI gates, and automated + release-from-PR-merge before the package is published more widely. +- **Behavior / compatibility changes**: No. No source under `packages/` was touched. +- **Risk level**: Low (standards/docs/CI only). + +## 2. Change Log + +### Key Files + +- `AGENTS.md` — the dev spec (~340 lines, 8 numbered sections). Documents the + dual-shape mechanism (`Object.assign(biliAcpPluginV1, {id, setup})` + why + `Plugin.define` being an identity function makes one entry serve opencode V1 and + V2), the monorepo module map, config options, dev workflow, the **absolute + PR-merge prohibition**, git safety rules, release workflow, and the gh-guard note. +- `.github/workflows/ci.yml` — matrix Node 22/24, single `build` job running + typecheck+build+test. +- `.github/workflows/pr-checks.yml` — `pr-validation` job running `check-pr.sh` + with `${{ github.head_ref }}` / `origin/${{ github.base_ref }}`. +- `.github/workflows/release.yml` — on push to master + `workflow_dispatch`; detects + release-branch merge, tags `v{VERSION}`, publishes + `--workspace billion-context-opencode`, creates GitHub Release; prerelease + (version contains `-`) → npm `dev` tag. +- `devlog/README.md` + `REQ.template.md` + `WORKLOG.template.md` + + `DESIGN.template.md` — devlog convention (default branch `master`). +- `scripts/ci/check-pr.sh` — branch-name regex, devlog presence, changelog-on-bump + (version read from `packages/billion-context-opencode/package.json`). +- `scripts/e2e/README.md` + `run-e2e.sh` — e2e harness PLAN (fake-LLM + + nudge-detection + state-asserting verify), marked "not yet functional". +- `README.md` — appended `## Changelog` with the `### v0.1.0` entry (existing + content preserved verbatim). + +## 3. Design & Implementation Notes + +- AGENTS.md adapted from `opencode-acp`'s structure but rewritten for this repo and + kept ~3.5x smaller. Key insight preserved verbatim in code: the dual-shape export + relies on `Object.assign` returning the same callable function and on + `@opencode-ai/plugin`'s `Plugin.define` being a no-op identity. +- `check-pr.sh` differs from the reference only in the version-source path + (`packages/billion-context-opencode/package.json`) and dropping the + `README.zh-CN.md` check (this repo has none). +- `release.yml` publishes ONLY the public workspace (`--workspace + billion-context-opencode`); `@bili/core` is private and never published. + +## 4. Testing & Verification + +- `bash scripts/ci/check-pr.sh 2026-08-13_project-standards origin/master` → PASS + (branch matches regex; devlog REQ.md + WORKLOG.md present; version unchanged). +- `npm run typecheck --workspaces` → PASS (no source touched). +- `bash -n scripts/ci/check-pr.sh` → syntax valid. +- Workflow YAML parses (validated with python yaml). + +## 5. Risk Assessment & Rollback + +- **Risk points**: None functional (standards/CI/docs only). +- **Rollback method**: `git revert` the standards commit. +- **Compatibility notes**: No data-format or config-schema changes. + +## 7. Follow-ups + +- [ ] Implement the e2e harness described in `scripts/e2e/README.md` and add an + `e2e` job to `ci.yml`. +- [ ] Configure `NPM_TOKEN` secret in GitHub repo settings before the first release. diff --git a/devlog/DESIGN.template.md b/devlog/DESIGN.template.md new file mode 100644 index 0000000..7f77a60 --- /dev/null +++ b/devlog/DESIGN.template.md @@ -0,0 +1,54 @@ +# DESIGN - + +- Task ID: `<YYYY-MM-DD_short-title>` +- Home Repo: `billion-context-opencode` +- Created: <YYYY-MM-DD> +- Status: Draft | Accepted | Superseded + +## 1. Problem Statement + +- **What problem are we solving?** +- **Why now?** (urgency / dependencies / strategic alignment) + +## 2. Goals & Non-Goals + +- **Goals**: + - +- **Non-Goals**: + - + +## 3. Current Architecture (if applicable) + +- **How it works today** (brief description + diagram if helpful): +- **Pain points**: + +## 4. Proposed Architecture + +- **Overview** (text diagram): +- **Key components**: + - +- **Data flow**: +- **API / interface changes**: + +## 5. Design Decisions & Rationale + +| Decision | Options Considered | Chosen | Why | +|----------|--------------------|--------|-----| +| | | | | + +## 6. Impact Analysis + +- **Backward compatibility** (dual-shape export, call↔result pairing invariant, persisted state): +- **Performance**: +- **Security**: +- **Dependencies** (new packages required): + +## 7. Migration Plan (if applicable) + +- **Steps**: + 1) +- **Feature flags / gradual rollout**: + +## 8. Open Questions + +- [ ] diff --git a/devlog/README.md b/devlog/README.md new file mode 100644 index 0000000..09a8ff7 --- /dev/null +++ b/devlog/README.md @@ -0,0 +1,61 @@ +# devlog/ + +Development iteration tracking for **billion-context-opencode**. + +## Purpose + +Every development iteration (bug fix, feature, refactor, infra) gets its own folder here. The devlog is a persistent, searchable record of what was done, why, and what was learned — complementing git history with structured context. + +## Naming Convention + +Folder name: `YYYY-MM-DD_short-title` + +- Must match the branch name (e.g., branch `2026-08-13_project-standards` → folder `2026-08-13_project-standards/`). +- Use lowercase, hyphens for spaces, no special characters. +- Date is the iteration start date. +- The default branch is `master` (release branches follow `YYYY-MM-DD_release-v{VERSION}`). + +## Required Files + +Every devlog entry MUST include at minimum: + +| File | Purpose | When to fill | +|------|---------|--------------| +| `REQ.md` | Problem statement, acceptance criteria, constraints | **BEFORE** implementation | +| `WORKLOG.md` | Commits, key files, test results, lessons learned | **DURING/AFTER** implementation | + +## Optional Files + +| File | When to include | +|------|----------------| +| `DESIGN.md` | **Required** for any change affecting architecture, data flow, or module boundaries — in particular changes to the dual-shape export (`packages/billion-context-opencode/src/index.ts`) or the V1/V2 message converters. | +| `NOTES.md` | Ad-hoc notes, investigation logs, debugging traces | + +## Rules + +1. **Every PR MUST have a corresponding devlog entry.** No exceptions. +2. The devlog folder name MUST match the branch name. +3. At minimum, `REQ.md` and `WORKLOG.md` MUST be present. +4. `DESIGN.md` is required for any change affecting architecture, data flow, or module boundaries. +5. Fill `REQ.md` **BEFORE** implementation (it functions like a ticket). +6. Fill `WORKLOG.md` **DURING/AFTER** implementation. +7. Commit devlog files alongside code changes — not as a separate afterthought. + +## Templates + +- [`REQ.template.md`](./REQ.template.md) — Copy to your entry folder as `REQ.md` +- [`WORKLOG.template.md`](./WORKLOG.template.md) — Copy to your entry folder as `WORKLOG.md` +- [`DESIGN.template.md`](./DESIGN.template.md) — Copy when architectural changes are involved + +## Directory Layout + +``` +devlog/ +├── README.md # This file +├── REQ.template.md # Template +├── WORKLOG.template.md # Template +├── DESIGN.template.md # Template +└── 2026-08-13_project-standards/ # Project standards (AGENTS.md, CI, devlog, e2e skeleton) + ├── REQ.md + └── WORKLOG.md +``` diff --git a/devlog/REQ.template.md b/devlog/REQ.template.md new file mode 100644 index 0000000..666fd65 --- /dev/null +++ b/devlog/REQ.template.md @@ -0,0 +1,50 @@ +# REQ - <Title> + +- Task ID: `<YYYY-MM-DD_short-title>` +- Home Repo: `billion-context-opencode` +- Created: <YYYY-MM-DD> +- Status: Draft | InProgress | Done | Rollback +- Priority: P0 | P1 | P2 +- Owner: <name> +- References: <issue/PR links> + +## 1. Background & Problem Statement + +- **Context**: +- **Current behavior (symptom)**: +- **Expected behavior**: +- **Impact**: + +## 2. Reproduction (if applicable) + +- **Environment**: + - Node: <version> + - OS/Arch: <linux-arm64 / darwin-arm64 / ...> +- **Minimal reproduction steps**: + 1) + 2) +- **Relevant configuration**: + +## 3. Constraints & Non-Goals + +- **Constraints**: + - Backward compatibility (dual-shape export, persisted state format): + - Performance requirements: + - Resource limits: +- **Non-Goals** (explicitly out of scope): + +## 4. Acceptance Criteria (must be testable) + +- **Correctness**: + - [ ] +- **Performance / Stability**: + - [ ] +- **Regression**: + - [ ] New/modified test cases added and passing (`npm run test`) + +## 5. Proposed Approach (optional) + +- **Affected modules & entry files**: + - +- **Risks**: +- **Rollback strategy**: diff --git a/devlog/WORKLOG.template.md b/devlog/WORKLOG.template.md new file mode 100644 index 0000000..ad18f92 --- /dev/null +++ b/devlog/WORKLOG.template.md @@ -0,0 +1,74 @@ +# WORKLOG - <Title> + +- Task ID: `<YYYY-MM-DD_short-title>` +- Home Repo: `billion-context-opencode` +- Status: InProgress | Done | Rollback +- Updated: <YYYY-MM-DD HH:mm> + +## 1. Summary + +- **What was done** (1–3 sentences): +- **Why** (1–3 sentences): +- **Behavior / compatibility changes**: <Yes/No, details> +- **Risk level**: Low | Medium | High + +## 2. Change Log + +### Commits + +| Commit | Description | +|--------|-------------| +| `<sha>` | <one-line summary> | +| ... | ... | + +### Key Files + +- `<path>` — <what changed and why> +- ... + +## 3. Design & Implementation Notes + +- **Entry point / key function**: +- **Key configuration items**: +- **Key logic explanation** (if non-trivial): + +## 4. Testing & Verification + +### Build & Test Commands + +```sh +# From repo root +npm run typecheck # all workspaces +npm run build # billion-context-opencode (tsup) +npm run test # billion-context-opencode tests +node smoke.mjs # e2e check against dist (after build) +``` + +### Test Coverage + +- New/modified test files: +- Test count: <N> total, <N> pass, <N> fail +- Key scenarios verified: + +### Results + +- **PASS/FAIL**: +- **Key logs/data** (optional): + +## 5. Risk Assessment & Rollback + +- **Risk points**: +- **Rollback method**: + - Revert commit(s): `<sha>` + - Rollback impact: +- **Compatibility notes** (dual-shape export, persisted state format, config schema): <Yes/No, details> + +## 6. Lessons Learned (optional) + +- What went well: +- What could be improved: +- Reusable conclusions: + +## 7. Follow-ups (optional) + +- [ ] diff --git a/scripts/ci/check-pr.sh b/scripts/ci/check-pr.sh new file mode 100644 index 0000000..d18917a --- /dev/null +++ b/scripts/ci/check-pr.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# PR validation script — enforces AGENTS.md contributing standards. +# +# Checks: +# 1. Branch name matches YYYY-MM-DD_short-title +# 2. devlog/{branch-name}/REQ.md exists +# 3. devlog/{branch-name}/WORKLOG.md exists +# 4. If packages/billion-context-opencode/package.json version changed, +# README.md must be modified AND contain "### v{VERSION}" in its changelog +# +# Usage: bash scripts/ci/check-pr.sh [branch-name] [base-ref] +# branch-name defaults to $GITHUB_HEAD_REF or the current branch +# base-ref defaults to "origin/master" +# +# Exit codes: 0 = all checks passed, 1 = one or more checks failed + +set -euo pipefail + +BRANCH="${1:-${GITHUB_HEAD_REF:-$(git branch --show-current)}}" +BASE="${2:-origin/master}" + +# The single published package's manifest — the only place a version bump is valid. +PKG="packages/billion-context-opencode/package.json" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +errors=0 +warn() { echo -e "${YELLOW}⚠ $1${NC}"; } +fail() { echo -e "${RED}✗ $1${NC}"; errors=$((errors + 1)); } +pass() { echo -e "${GREEN}✓ $1${NC}"; } + +echo "=== PR Validation ===" +echo "Branch: $BRANCH" +echo "Base: $BASE" +echo "" + +# ── Check 1: Branch name convention ────────────────────────── +echo "── Branch name convention ──" +if echo "$BRANCH" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[a-z0-9.-]+$'; then + pass "Branch name matches YYYY-MM-DD_short-title" +else + fail "Branch name '$BRANCH' does not match YYYY-MM-DD_short-title (e.g., 2026-08-13_compress-fix)" + echo " Required format: digits-digits-digits_lowercase-kebab-case" +fi +echo "" + +# ── Checks 2 & 3: Devlog exists ────────────────────────────── +echo "── Devlog entry ──" +DEVLOG_DIR="devlog/$BRANCH" +if [ -f "$DEVLOG_DIR/REQ.md" ]; then + pass "devlog/$BRANCH/REQ.md exists" +else + fail "devlog/$BRANCH/REQ.md is missing (required by AGENTS.md §5.1.2)" +fi + +if [ -f "$DEVLOG_DIR/WORKLOG.md" ]; then + pass "devlog/$BRANCH/WORKLOG.md exists" +else + fail "devlog/$BRANCH/WORKLOG.md is missing (required by AGENTS.md §5.1.2)" +fi +echo "" + +# ── Check 4: Changelog updated when version changes ────────── +echo "── Changelog check ──" +# Read version from the current (PR) tree's published package manifest. +CURRENT_VERSION=$(node -p "require('./$PKG').version" 2>/dev/null || echo "") +# Read version from the base ref's copy of the same manifest. +BASE_VERSION=$(git show "$BASE:$PKG" 2>/dev/null | node -e " +const chunks = []; +process.stdin.on('data', c => chunks.push(c)); +process.stdin.on('end', () => { + try { console.log(JSON.parse(Buffer.concat(chunks).toString()).version); } + catch { console.log(''); } +}); +" 2>/dev/null || echo "") + +if [ -z "$CURRENT_VERSION" ]; then + warn "Could not read version from $PKG — skipping changelog check" +elif [ -z "$BASE_VERSION" ]; then + warn "Could not read version from $BASE:$PKG — skipping changelog check" +elif [ "$CURRENT_VERSION" = "$BASE_VERSION" ]; then + pass "Version unchanged ($CURRENT_VERSION) — changelog check skipped" +else + echo " Version change: $BASE_VERSION → $CURRENT_VERSION" + + # Was README.md modified in this PR (diff against the three-dot merge base)? + README_CHANGED=$(git diff --name-only "$BASE"...HEAD -- README.md 2>/dev/null | wc -l) + + if [ "$README_CHANGED" -eq 0 ]; then + fail "Version bumped ($BASE_VERSION → $CURRENT_VERSION) but README.md not modified" + echo " AGENTS.md §5.4.2 requires a changelog entry in README.md for version changes" + else + pass "README.md modified — checking version string..." + + if grep -q "### v${CURRENT_VERSION}" README.md 2>/dev/null; then + pass "README.md changelog contains '### v$CURRENT_VERSION'" + else + fail "README.md changelog does not contain '### v$CURRENT_VERSION'" + fi + fi +fi +echo "" + +# ── Summary ────────────────────────────────────────────────── +echo "=== Summary ===" +if [ "$errors" -eq 0 ]; then + echo -e "${GREEN}All checks passed ✓${NC}" + exit 0 +else + echo -e "${RED}$errors check(s) failed${NC}" + exit 1 +fi diff --git a/scripts/e2e/README.md b/scripts/e2e/README.md new file mode 100644 index 0000000..e47a8a9 --- /dev/null +++ b/scripts/e2e/README.md @@ -0,0 +1,63 @@ +# scripts/e2e/ + +End-to-end test harness for **billion-context-opencode**. + +> ## ⚠️ Scaffold — not yet functional +> +> This directory is a **planned** harness. The `run-e2e.sh` stub exists only so CI +> and local scripts have a stable entrypoint. **Full e2e is deferred.** Nothing here +> runs a real opencode instance yet. Do not wire it into CI as a gating job until the +> harness described below is implemented. + +## Planned Design + +The goal is a Docker-isolated harness (modeled on the `opencode-acp` e2e philosophy) +that drives a **real opencode** process against a **fake LLM server** and asserts on +the resulting compression/nudge state. Rationale: unit tests cover the message +converters and config; e2e must cover the *integration* — opencode loading the +dual-shape entry, the V1/V2 hooks firing, the model emitting `bili_compress` calls, +and the persisted state on disk. + +### Components (to be built) + +| Component | Purpose | +|-----------|---------| +| `fake-llm-server.ts` | A tiny HTTP server impersonating an LLM provider. It echoes a scripted assistant turn OR — critically — **detects ACP nudge injection** in the incoming messages and responds with a `bili_compress` tool call. Reports realistic `prompt_tokens` derived from actual input sizes so the plugin sees real token counts for nudge math. | +| `scenarios/*.json` | Declarative scenarios: a scripted message list + a `"respond"` mode (`"static"` \| `"nudge-compress"` \| `"compress-and-continue"`) + expectations. | +| `verify.ts` | After a scenario runs, asserts on state: block count, nudge state fields, covered-message ids, persisted `~/.cache/opencode-bili-acp/<sid>.acp.json`. Checks **nudge state**, not just block count, so baseline/feedback-loop bugs surface. | +| `run-e2e.sh` | Orchestrator: builds the bundle, starts the fake LLM, boots opencode against an isolated `HOME`/`XDG_CONFIG_HOME`, feeds the scenario, runs `verify.ts`. | + +### Planned Scenarios + +These are deliberately modeled on the gaps that hid production bugs elsewhere, per +the AGENTS.md nudge/growth testing requirements: + +1. **Smoke** — a scripted `bili_compress` call produces one block and prunes the range. +2. **Nudge-triggered compression** — context grows past the threshold; the fake LLM + detects ACP's injected nudge via `detectNudge()` and emits a `bili_compress` call + in response. Tests the *real* nudge→compress flow, not just scripted compress. +3. **Growth accumulation** — context grows across multiple turns past the nudge + threshold, exercising the growth-gating logic (all-compress-in-one-turn does not). +4. **Nudge re-fire after compress** — after a compress resets the baseline, new growth + must trigger a fresh nudge. Catches baseline-corruption feedback loops. +5. **Decompress** — a `bili_decompress` call restores a block's content correctly. + +### Why verify nudge STATE, not just block count + +Block count alone cannot detect: +- a corrupted `lastPerMessageNudgeTokens` baseline that suppresses all nudges, +- a feedback loop that re-fires every turn, +- growth math that never crosses the threshold. + +`verify.ts` MUST assert on the nudge state fields in addition to block count. + +## Status + +- [x] Directory + README + `run-e2e.sh` stub +- [ ] `fake-llm-server.ts` +- [ ] `scenarios/` +- [ ] `verify.ts` +- [ ] Wired into CI (`ci.yml` e2e job) + +When the harness is implemented, add an `e2e` job to `.github/workflows/ci.yml` +(needs `build`) and document the scenario list in AGENTS.md §3.3. diff --git a/scripts/e2e/run-e2e.sh b/scripts/e2e/run-e2e.sh new file mode 100644 index 0000000..1d4de1a --- /dev/null +++ b/scripts/e2e/run-e2e.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# E2E orchestrator stub. +# +# Full e2e harness (fake LLM + opencode + verify.ts) is not yet implemented. +# See scripts/e2e/README.md for the planned design and scenario list. +# +# Exit 0 so this can be referenced from CI/local scripts without breaking builds. + +set -euo pipefail + +echo "e2e harness not yet implemented — see scripts/e2e/README.md" +exit 0