diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..753b72a --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-marketplace.json", + "name": "harness", + "owner": { + "name": "coderloganli", + "email": "coder_logan_li@outlook.com" + }, + "plugins": [ + { + "name": "harness", + "source": "./", + "description": "Governs a project's documents, a task's workspace, and the ten stages a task passes through.", + "version": "0.2.0", + "license": "MIT", + "keywords": ["workflow", "worktree", "adr", "documentation", "code-review"] + } + ] +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c3d08ff --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: test + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + # The harness resolves workspaces, worktrees and repository paths itself, and + # the one refusal is a path comparison. Windows is in the matrix because that + # is where a path bug would appear first. + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + node: ['20', '22'] + + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node }} + + - run: npm test diff --git a/README.md b/README.md index c10bfe8..b1930c1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Claude Code Harness +[![test](https://github.com/coderloganli/dev-harness/actions/workflows/test.yml/badge.svg)](https://github.com/coderloganli/dev-harness/actions/workflows/test.yml) + A harness for Claude Code: it makes Claude build software the way a careful engineer does — read the design first, agree it before writing code, write the failing test before the feature, work in a scratch space that cannot damage anything, and keep @@ -16,7 +18,7 @@ It governs three things, and they are independent of each other: > **Status: early.** The harness has been through one real task — its own, fixing > five defects that its first run exposed — and the ten stages, the refusal and the -> approval dialogs all held. There is no marketplace entry yet. +> approval dialogs all held. ## Why @@ -112,6 +114,19 @@ session it was worked in. Weeks later, ask for "that task about search ranking" one was recorded — a task whose session was not says so, rather than offering a resume that goes nowhere. +## Installing + +The repository is its own marketplace, so adding it and installing from it are two +lines in Claude Code: + +``` +/plugin marketplace add coderloganli/dev-harness +/plugin install harness@harness +``` + +If the install summary says `Run /reload-plugins to activate.`, run that too. To pick +up a later version, `/plugin marketplace update harness`. + ## Getting started ``` diff --git a/docs/architecture.md b/docs/architecture.md index ba016a1..70b1257 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -26,6 +26,12 @@ something a hook can do. | **Templates** | Skeletons for the documents `init` creates | | **Project config** | A few facts about one project | +The repository is also its own marketplace: `.claude-plugin/marketplace.json` lists +one plugin whose source is the repository root, so `/plugin marketplace add` and +`/plugin install` reach it without a second repository to keep in step. Its `version` +is what tells an installed copy that a newer one exists, so it moves with +`plugin.json`'s. + The line that matters: **a skill is text Claude may or may not follow; the server and the hooks are programs that run either way.** Every rule below is placed on one side of that line on purpose. diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f64605 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "claude-code-harness", + "version": "0.2.0", + "private": true, + "description": "Governs a project's documents, a task's workspace, and the ten stages a task passes through.", + "license": "MIT", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "test": "node --test" + } +} diff --git a/test/conventions.test.mjs b/test/conventions.test.mjs index 7ba6d5f..0185724 100644 --- a/test/conventions.test.mjs +++ b/test/conventions.test.mjs @@ -1,4 +1,4 @@ -// Test cases 9-11: conventions the plugin states about itself, asserted against the +// Test cases 9-11 and 38: conventions the plugin states about itself, asserted against the // repository so that a fix cannot look done while a stale copy survives somewhere. import assert from 'node:assert/strict'; @@ -65,3 +65,24 @@ test('11 — one product document, under 200 lines', () => { 'nothing should still link to docs/prd.md', ); }); + +test('38 — one version, declared in three files, and an install line that reaches it', () => { + const plugin = JSON.parse(read('.claude-plugin/plugin.json')); + const marketplace = JSON.parse(read('.claude-plugin/marketplace.json')); + const pkg = JSON.parse(read('package.json')); + + // The marketplace version is what tells an installed copy a newer one exists. A + // release that bumps plugin.json alone ships to nobody, and nothing at runtime + // would say so. + const entry = marketplace.plugins.find((p) => p.name === plugin.name); + assert.ok(entry, `marketplace.json must list a plugin named ${plugin.name}`); + assert.equal(entry.version, plugin.version, 'the marketplace entry must carry the plugin.json version'); + assert.equal(pkg.version, plugin.version, 'package.json must carry the plugin.json version'); + + // README tells the user to type `/plugin install @`. + assert.match( + read('README.md'), + new RegExp(`/plugin install ${plugin.name}@${marketplace.name}\\b`), + 'the README install line must name the plugin and the marketplace as they are declared', + ); +});