mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(skill): ship the codeman agent skill as a Claude Code plugin from the repo's own marketplace
`.claude-plugin/marketplace.json` at the repo root makes `/plugin marketplace add Ark0N/Codeman` work, and the one plugin it lists is the repo itself (`source: "./"`), whose one component is `skills/codeman/`. So `/plugin install codeman@codeman` is a third install route next to `npx skills add` and `codeman skill install`, and the skill shows up in the plugin directories that index Claude Code marketplaces. Both manifests carry package.json's version: `scripts/sync-plugin-version.mjs` rewrites them inside `version-packages`, right after `changeset version`, and `test/plugin-manifest.test.ts` pins the equality, the skill's frontmatter name (without it the installed skill would be named after a versioned cache dir), and that no other plugin component (`commands/`, `agents/`, `hooks/`, `.mcp.json`, `settings.json`) appears at the repo root, since an install would silently ship it. Verified with `claude plugin validate` (one expected warning: CLAUDE.md at a plugin root is not plugin context) and a local marketplace add, install, details, uninstall cycle against a clean checkout of this commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "codeman",
|
||||||
|
"owner": {
|
||||||
|
"name": "Ark0N",
|
||||||
|
"url": "https://github.com/Ark0N"
|
||||||
|
},
|
||||||
|
"description": "Codeman, self-hosted mission control for AI coding agents. Ships the codeman agent skill: let one Claude Code session spawn, prompt, wait on and read other sessions.",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "codeman",
|
||||||
|
"source": "./",
|
||||||
|
"description": "Drive Codeman from inside a Claude Code session: spawn worker sessions, prompt them, wait for them, read their answers, clean up. Acts only inside a Codeman-managed session.",
|
||||||
|
"version": "1.28.1",
|
||||||
|
"author": {
|
||||||
|
"name": "Ark0N",
|
||||||
|
"url": "https://github.com/Ark0N"
|
||||||
|
},
|
||||||
|
"homepage": "https://getcodeman.com",
|
||||||
|
"category": "productivity",
|
||||||
|
"keywords": [
|
||||||
|
"codeman",
|
||||||
|
"orchestration",
|
||||||
|
"multi-agent",
|
||||||
|
"session-manager",
|
||||||
|
"tmux",
|
||||||
|
"claude-code"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "codeman",
|
||||||
|
"description": "Drive Codeman, the self-hosted session manager for AI coding agents, from inside a Claude Code session: spawn worker sessions, prompt them, wait for them, read their answers, clean up. Acts only inside a Codeman-managed session.",
|
||||||
|
"version": "1.28.1",
|
||||||
|
"author": {
|
||||||
|
"name": "Ark0N",
|
||||||
|
"url": "https://github.com/Ark0N"
|
||||||
|
},
|
||||||
|
"homepage": "https://getcodeman.com",
|
||||||
|
"repository": "https://github.com/Ark0N/Codeman",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"codeman",
|
||||||
|
"orchestration",
|
||||||
|
"multi-agent",
|
||||||
|
"session-manager",
|
||||||
|
"tmux",
|
||||||
|
"claude-code",
|
||||||
|
"codex",
|
||||||
|
"deepseek"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
>
|
>
|
||||||
> **This file is in `.prettierignore` on purpose.** Prettier's markdown printer escapes underscores inside the glob-heavy paths used throughout (`agent-*.jsonl` became `agent-\_.jsonl`, collapsing backtick spans and corrupting a whole paragraph). Do not remove the ignore entry, and do not run `prettier --write` on it.
|
> **This file is in `.prettierignore` on purpose.** Prettier's markdown printer escapes underscores inside the glob-heavy paths used throughout (`agent-*.jsonl` became `agent-\_.jsonl`, collapsing backtick spans and corrupting a whole paragraph). Do not remove the ignore entry, and do not run `prettier --write` on it.
|
||||||
>
|
>
|
||||||
> **Repo root is kept short on purpose** (the README sits below the file listing on GitHub). Config lives in `config/` (`eslint.config.js`, `knip.json`, the vitest configs), Prettier's config is the `"prettier"` key in `package.json`, and `SECURITY.md` is under `.github/`. Root-only files are the ones tools genuinely require there: `CLAUDE.md` + `AGENTS.md` (loaded from the root by Claude Code / Codex), `CHANGELOG.md` (changesets writes it next to `package.json`), `tsconfig.json`, `.editorconfig`, `.nvmrc`/`.npmrc`, `.prettierignore` (resolved relative to cwd), `LICENSE` (GitHub detection), `.dockerignore` (the build context is the repo root, so Docker resolves it there and nowhere else) and `install.sh` (its raw URL is the published install one-liner). Don't relocate those.
|
> **Repo root is kept short on purpose** (the README sits below the file listing on GitHub). Config lives in `config/` (`eslint.config.js`, `knip.json`, the vitest configs), Prettier's config is the `"prettier"` key in `package.json`, and `SECURITY.md` is under `.github/`. Root-only files are the ones tools genuinely require there: `CLAUDE.md` + `AGENTS.md` (loaded from the root by Claude Code / Codex), `CHANGELOG.md` (changesets writes it next to `package.json`), `tsconfig.json`, `.editorconfig`, `.nvmrc`/`.npmrc`, `.prettierignore` (resolved relative to cwd), `LICENSE` (GitHub detection), `.dockerignore` (the build context is the repo root, so Docker resolves it there and nowhere else) and `install.sh` (its raw URL is the published install one-liner). `.claude-plugin/` (`plugin.json` + `marketplace.json`) is root-only for the same reason: `/plugin marketplace add Ark0N/Codeman` reads it from the repo root and nowhere else, which makes the repo its own plugin marketplace with the repo itself (`source: "./"`) as the one plugin, whose one component is `skills/codeman/`. Both manifests carry `package.json`'s version, synced by `scripts/sync-plugin-version.mjs` inside `version-packages` and pinned by `test/plugin-manifest.test.ts`, which also refuses any other plugin component (`commands/`, `agents/`, `hooks/`, `.mcp.json`, `settings.json`) appearing at the root, since an install would silently ship it. Don't relocate those.
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
|||||||
|
|
||||||
**Input**: `session.writeViaMux()` for programmatic/curl input via tmux `send-keys -l` + `send-keys Enter`, single-line only. Interactive **browser** input goes through a durable **exactly-once** layer: a stable `clientId` + monotonic per-session `seq` persisted to localStorage until the server ACKs, so a dropped link cannot lose or double-deliver a prompt. `ws-connection-registry.ts` supersedes only same-TAB reconnects, so two tabs on one session coexist. → [architecture-invariants#input-delivery-and-ws-resilience](docs/architecture-invariants.md#input-delivery-and-ws-resilience)
|
**Input**: `session.writeViaMux()` for programmatic/curl input via tmux `send-keys -l` + `send-keys Enter`, single-line only. Interactive **browser** input goes through a durable **exactly-once** layer: a stable `clientId` + monotonic per-session `seq` persisted to localStorage until the server ACKs, so a dropped link cannot lose or double-deliver a prompt. `ws-connection-registry.ts` supersedes only same-TAB reconnects, so two tabs on one session coexist. → [architecture-invariants#input-delivery-and-ws-resilience](docs/architecture-invariants.md#input-delivery-and-ws-resilience)
|
||||||
|
|
||||||
**Agent wait primitives**: bounded long-polls so an agent driving Codeman from a shell can block instead of poll: `GET /api/sessions/:id/wait` (lifecycle signal), `GET /api/sessions/:id/wait-output` (literal substring, **never** regex) and `wait`/`waitTimeout` on `POST /api/sessions/:id/input`. Registry in `session-wait-registry.ts` (pure, no `Session` reference), bounds in `config/agent-wait.ts`. ⚠️ **A timeout is a 200** (`wait.timedOut`), never an error, so callers loop over short waits. ⚠️ `stop`/`blocked` are hook-driven and fire for **`claude` and `deepseek` ONLY** (`shell` installs none either); asking for one explicitly on any other mode is a 400, the default set silently drops them. `deepseek` qualifies because the DeepSeek Harness TUI REPORTS idle/working/blocked to its supervisor and Codeman is that supervisor (`deepseek-status-shim.ts`), so its signals are definitive rather than inferred — `hooksAvailableForMode()` in `session-wait-registry.ts` is the one place that rule lives. ⚠️ Send-and-wait registers the waiter BEFORE the write (a separate POST-then-wait races and reports the PREVIOUS turn), and both teardown paths must `notifySignal('exit')` BEFORE `cancelAll()`. ⚠️ Client-hangup abort listens on **`reply.raw`** guarded by `writableFinished`: on `req.raw`, `close` fires when the request BODY ends, which on a POST killed every send-and-wait instantly and no `app.inject()` test could see it. ⚠️ Worker liveness cannot come from `session.pid` — for a tmux session that is the local attach client, which outlives a worker dying inside its pane — so it is probed at the mux layer (`isPaneDead`, ~750 ms cache) on blocking waits only, never on the input hot path. ⚠️ Signals are edge-triggered with no history: one that fires with no waiter registered is unobservable afterwards, so gather fan-outs with send-and-wait or latched `wait-output` markers, never fire-and-forget-then-sequential-signal-waits. ⚠️ **`deepseek` is therefore the one non-claude mode the skill drives like claude** — `spawn_workers alpha beta:deepseek` is a mixed fleet in one call, and `sendwait`/`last_text` need no variant. Two traps are baked into the preamble rather than left to the agent: the harness's boot `idle` report lands ~300 ms BEFORE its composer paints (2.26 s vs 2.56 s, measured), so readiness must come from the composer and never from the signal, or a send-and-wait resolves on the boot edge and reports a turn that never ran; and `sendwait` asks for `wait:"stop,exit"` rather than the default set, because that set also carries `idle`, which for an external CLI is inferred from output stabilization — on a dsh worker whose TUI repaints rarely, a re-wait resolved in 0 ms with `signal:"idle"` on a turn with minutes left to run. The primitives are packaged as the **`skills/codeman` agent skill**: installable via `codeman skill install [--case <name>]` / `skill uninstall`, or auto-injected into a case's `.claude/skills/` on Claude session create behind `agentSkillEnabled` (SYNCED, default OFF). Injection is ADD-ONLY at create, marker-owned (`applyAgentSkill` in `hooks-config.ts` never touches an unmarked user copy) and refuses symlinks (this repo's own `.claude/skills/codeman` is a symlink to the source, which the injector must never write through). ⚠️ Claude Code loads a same-named USER-LEVEL skill (`~/.claude/skills/codeman`, written once by `codeman skill install` with no `--case`) over the per-case copy, and nothing used to refresh it: a stale Aug-9 user copy shadowed every fresh injection (2026-08-14: agents ran the old recipes, spawned workers serially and lost their lineage arcs), so session create now also refreshes a marker-owned user copy (`refreshUserAgentSkill`; refresh-only, never installs, foreign/symlink refused). Session create additionally pre-seeds the skill's §0 preamble cache (`seedAgentSessionPreamble` → `${XDG_CACHE_HOME:-~/.cache}/codeman-agent-<id>.sh`, local claude sessions only), single-sourced from `skills/codeman/preamble.sh` and pinned byte-identical to SKILL.md's §0 heredoc by `test/agent-skill.test.ts`, so the skill's bootstrap is a two-line loader instead of a ~150-line paste the model types out (~47 s of generation, measured live). → [architecture-invariants#agent-wait-primitives](docs/architecture-invariants.md#agent-wait-primitives), `docs/api-reference.md`
|
**Agent wait primitives**: bounded long-polls so an agent driving Codeman from a shell can block instead of poll: `GET /api/sessions/:id/wait` (lifecycle signal), `GET /api/sessions/:id/wait-output` (literal substring, **never** regex) and `wait`/`waitTimeout` on `POST /api/sessions/:id/input`. Registry in `session-wait-registry.ts` (pure, no `Session` reference), bounds in `config/agent-wait.ts`. ⚠️ **A timeout is a 200** (`wait.timedOut`), never an error, so callers loop over short waits. ⚠️ `stop`/`blocked` are hook-driven and fire for **`claude` and `deepseek` ONLY** (`shell` installs none either); asking for one explicitly on any other mode is a 400, the default set silently drops them. `deepseek` qualifies because the DeepSeek Harness TUI REPORTS idle/working/blocked to its supervisor and Codeman is that supervisor (`deepseek-status-shim.ts`), so its signals are definitive rather than inferred — `hooksAvailableForMode()` in `session-wait-registry.ts` is the one place that rule lives. ⚠️ Send-and-wait registers the waiter BEFORE the write (a separate POST-then-wait races and reports the PREVIOUS turn), and both teardown paths must `notifySignal('exit')` BEFORE `cancelAll()`. ⚠️ Client-hangup abort listens on **`reply.raw`** guarded by `writableFinished`: on `req.raw`, `close` fires when the request BODY ends, which on a POST killed every send-and-wait instantly and no `app.inject()` test could see it. ⚠️ Worker liveness cannot come from `session.pid` — for a tmux session that is the local attach client, which outlives a worker dying inside its pane — so it is probed at the mux layer (`isPaneDead`, ~750 ms cache) on blocking waits only, never on the input hot path. ⚠️ Signals are edge-triggered with no history: one that fires with no waiter registered is unobservable afterwards, so gather fan-outs with send-and-wait or latched `wait-output` markers, never fire-and-forget-then-sequential-signal-waits. ⚠️ **`deepseek` is therefore the one non-claude mode the skill drives like claude** — `spawn_workers alpha beta:deepseek` is a mixed fleet in one call, and `sendwait`/`last_text` need no variant. Two traps are baked into the preamble rather than left to the agent: the harness's boot `idle` report lands ~300 ms BEFORE its composer paints (2.26 s vs 2.56 s, measured), so readiness must come from the composer and never from the signal, or a send-and-wait resolves on the boot edge and reports a turn that never ran; and `sendwait` asks for `wait:"stop,exit"` rather than the default set, because that set also carries `idle`, which for an external CLI is inferred from output stabilization — on a dsh worker whose TUI repaints rarely, a re-wait resolved in 0 ms with `signal:"idle"` on a turn with minutes left to run. The primitives are packaged as the **`skills/codeman` agent skill**: installable via `codeman skill install [--case <name>]` / `skill uninstall`, as a Claude Code plugin from the repo's own marketplace (`/plugin marketplace add Ark0N/Codeman` + `/plugin install codeman@codeman`, see the root-files note above), or auto-injected into a case's `.claude/skills/` on Claude session create behind `agentSkillEnabled` (SYNCED, default OFF). Injection is ADD-ONLY at create, marker-owned (`applyAgentSkill` in `hooks-config.ts` never touches an unmarked user copy) and refuses symlinks (this repo's own `.claude/skills/codeman` is a symlink to the source, which the injector must never write through). ⚠️ Claude Code loads a same-named USER-LEVEL skill (`~/.claude/skills/codeman`, written once by `codeman skill install` with no `--case`) over the per-case copy, and nothing used to refresh it: a stale Aug-9 user copy shadowed every fresh injection (2026-08-14: agents ran the old recipes, spawned workers serially and lost their lineage arcs), so session create now also refreshes a marker-owned user copy (`refreshUserAgentSkill`; refresh-only, never installs, foreign/symlink refused). Session create additionally pre-seeds the skill's §0 preamble cache (`seedAgentSessionPreamble` → `${XDG_CACHE_HOME:-~/.cache}/codeman-agent-<id>.sh`, local claude sessions only), single-sourced from `skills/codeman/preamble.sh` and pinned byte-identical to SKILL.md's §0 heredoc by `test/agent-skill.test.ts`, so the skill's bootstrap is a two-line loader instead of a ~150-line paste the model types out (~47 s of generation, measured live). → [architecture-invariants#agent-wait-primitives](docs/architecture-invariants.md#agent-wait-primitives), `docs/api-reference.md`
|
||||||
|
|
||||||
**Agent-created case marker** (`src/agent-case-marker.ts`): a case directory `POST /api/quick-start` **creates** for an agent-driven spawn gets a `.codeman-agent-case.json` marker, so the scratch workspaces a long orchestration leaves behind (one per worker, and deleting the session does not remove them) can still be told apart from the user's real projects months later. `GET /api/cases` publishes it as `agentCreated`; `GET /api/cases/agent-created` is the read-only cleanup listing, adding `inUse` (a live session's `workingDir` is that case) and `modifiedAt`; Add Case → Manage badges each one and offers a review-then-delete sweep. The signal is the skill preamble's `X-Codeman-Agent-Origin` header (or an `agentOrigin` body field), falling back to a RESOLVED `parentSessionId` — nothing in the browser UI sets lineage, so a create request naming its spawning session came from an agent by construction, and that fallback is what still labels workers spawned by a stale skill copy. ⚠️ **Only the branch that CREATES the directory may write it.** A linked case, a cloned repo or any pre-existing path must never be labelled: the label drives a recursive-delete affordance, and mislabelling someone's repo there is the one failure mode that costs real work. `POST /api/sessions` takes an existing `workingDir`, so it writes no marker at all, by construction. ⚠️ Reading is TOTAL: anything that is not a well-formed version-1 marker (truncated write, hand-edited junk) reads as *not* agent-created rather than as a half-trusted entry, and deleting the file is the supported way to adopt a scratch case as a real one — which is what the `note` written into it tells whoever finds it. ⚠️ Removal stays on the existing `DELETE /api/cases/:name`, one name at a time, so there is exactly ONE recursive-delete path; the UI's sweep names every directory in its confirm and EXCLUDES an `inUse` case outright rather than confirming it away. ⚠️ Marker in the case dir rather than a registry under `~/.codeman`: it survives a wiped data dir or a different instance, is removed by the same `rm -rf` that removes the case (so no stale-entry pruning), and a user who runs `ls -a` can see what labelled their directory. Adding the header changed the preamble, so `CODEMAN_PREAMBLE` was bumped (1.22.0) — a cached copy is version-checked, and forgetting the bump leaves every already-seeded agent sending the old headers. Tests: `test/agent-case-marker.test.ts`, `test/routes/agent-case-marker-routes.test.ts`.
|
**Agent-created case marker** (`src/agent-case-marker.ts`): a case directory `POST /api/quick-start` **creates** for an agent-driven spawn gets a `.codeman-agent-case.json` marker, so the scratch workspaces a long orchestration leaves behind (one per worker, and deleting the session does not remove them) can still be told apart from the user's real projects months later. `GET /api/cases` publishes it as `agentCreated`; `GET /api/cases/agent-created` is the read-only cleanup listing, adding `inUse` (a live session's `workingDir` is that case) and `modifiedAt`; Add Case → Manage badges each one and offers a review-then-delete sweep. The signal is the skill preamble's `X-Codeman-Agent-Origin` header (or an `agentOrigin` body field), falling back to a RESOLVED `parentSessionId` — nothing in the browser UI sets lineage, so a create request naming its spawning session came from an agent by construction, and that fallback is what still labels workers spawned by a stale skill copy. ⚠️ **Only the branch that CREATES the directory may write it.** A linked case, a cloned repo or any pre-existing path must never be labelled: the label drives a recursive-delete affordance, and mislabelling someone's repo there is the one failure mode that costs real work. `POST /api/sessions` takes an existing `workingDir`, so it writes no marker at all, by construction. ⚠️ Reading is TOTAL: anything that is not a well-formed version-1 marker (truncated write, hand-edited junk) reads as *not* agent-created rather than as a half-trusted entry, and deleting the file is the supported way to adopt a scratch case as a real one — which is what the `note` written into it tells whoever finds it. ⚠️ Removal stays on the existing `DELETE /api/cases/:name`, one name at a time, so there is exactly ONE recursive-delete path; the UI's sweep names every directory in its confirm and EXCLUDES an `inUse` case outright rather than confirming it away. ⚠️ Marker in the case dir rather than a registry under `~/.codeman`: it survives a wiped data dir or a different instance, is removed by the same `rm -rf` that removes the case (so no stale-entry pruning), and a user who runs `ls -a` can see what labelled their directory. Adding the header changed the preamble, so `CODEMAN_PREAMBLE` was bumped (1.22.0) — a cached copy is version-checked, and forgetting the bump leaves every already-seeded agent sending the old headers. Tests: `test/agent-case-marker.test.ts`, `test/routes/agent-case-marker-routes.test.ts`.
|
||||||
|
|
||||||
|
|||||||
@@ -715,6 +715,7 @@ Everything in this section also ships as a **Claude Code skill** in [`skills/cod
|
|||||||
| How | Command | Scope |
|
| How | Command | Scope |
|
||||||
| -------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
| -------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| Skills CLI | `npx skills add Ark0N/Codeman --skill codeman -g` | Global, works for any skills-aware agent |
|
| Skills CLI | `npx skills add Ark0N/Codeman --skill codeman -g` | Global, works for any skills-aware agent |
|
||||||
|
| Claude Code plugin | `/plugin marketplace add Ark0N/Codeman` then `/plugin install codeman@codeman` | Global, through Claude Code's plugin manager; `/plugin update codeman` follows releases |
|
||||||
| Bundled CLI | `codeman skill install` | Global (`~/.claude/skills/codeman`), for npm installs that never cloned the repo |
|
| Bundled CLI | `codeman skill install` | Global (`~/.claude/skills/codeman`), for npm installs that never cloned the repo |
|
||||||
| Bundled CLI | `codeman skill install --case <name>` | One case only |
|
| Bundled CLI | `codeman skill install --case <name>` | One case only |
|
||||||
| Web UI | App Settings → Agents & CLIs → Claude → **Agent Skill** | Auto-injects into each case on Claude session create (`agentSkillEnabled`, SYNCED, default off) |
|
| Web UI | App Settings → Agents & CLIs → Claude → **Agent Skill** | Auto-injects into each case on Claude session create (`agentSkillEnabled`, SYNCED, default off) |
|
||||||
|
|||||||
@@ -646,6 +646,7 @@ Codeman 默认用 `--dangerously-skip-permissions` 启动会话,因此 Web UI
|
|||||||
> **捷径:装上打包好的智能体技能。** 下面这一整套(外加多工作会话的实战配方)已经作为 Claude Code 技能随仓库发布在 [`skills/codeman`](skills/codeman/SKILL.md),会话内部的智能体不必等你把文档粘进提示词就能驱动 Codeman。三种获取方式:
|
> **捷径:装上打包好的智能体技能。** 下面这一整套(外加多工作会话的实战配方)已经作为 Claude Code 技能随仓库发布在 [`skills/codeman`](skills/codeman/SKILL.md),会话内部的智能体不必等你把文档粘进提示词就能驱动 Codeman。三种获取方式:
|
||||||
>
|
>
|
||||||
> - `npx skills add Ark0N/Codeman --skill codeman -g`:全局安装,任何支持技能的智能体都能用
|
> - `npx skills add Ark0N/Codeman --skill codeman -g`:全局安装,任何支持技能的智能体都能用
|
||||||
|
> - Claude Code 插件:`/plugin marketplace add Ark0N/Codeman`,然后 `/plugin install codeman@codeman`:通过 Claude Code 自带的插件管理器全局安装,`/plugin update codeman` 跟随新版本
|
||||||
> - `codeman skill install`(全局)或 `codeman skill install --case <name>`:给那些从 npm 安装、从未克隆过仓库的用户;`codeman skill uninstall` 可撤销
|
> - `codeman skill install`(全局)或 `codeman skill install --case <name>`:给那些从 npm 安装、从未克隆过仓库的用户;`codeman skill uninstall` 可撤销
|
||||||
> - **App Settings → Agent Skill**(`agentSkillEnabled`,默认关闭):开启后,Codeman 会在每次于某个 case 中创建 Claude 会话时把技能注入该 case;case 里用户自己写的 `skills/codeman` 永远不会被覆盖
|
> - **App Settings → Agent Skill**(`agentSkillEnabled`,默认关闭):开启后,Codeman 会在每次于某个 case 中创建 Claude 会话时把技能注入该 case;case 里用户自己写的 `skills/codeman` 永远不会被覆盖
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ instead of pasting endpoint documentation into prompts.
|
|||||||
| How | Command | Scope |
|
| How | Command | Scope |
|
||||||
| ------------ | ----------------------------------------------------------- | ----------------------------------------------------------- |
|
| ------------ | ----------------------------------------------------------- | ----------------------------------------------------------- |
|
||||||
| Skills CLI | `npx skills add Ark0N/Codeman --skill codeman -g` | Global, any skills-aware agent. |
|
| Skills CLI | `npx skills add Ark0N/Codeman --skill codeman -g` | Global, any skills-aware agent. |
|
||||||
|
| Claude Code plugin | `/plugin marketplace add Ark0N/Codeman`, then `/plugin install codeman@codeman` | Global, through Claude Code's plugin manager. `/plugin update codeman` follows releases. |
|
||||||
| Bundled CLI | `codeman skill install` | Global, at `~/.claude/skills/codeman`. |
|
| Bundled CLI | `codeman skill install` | Global, at `~/.claude/skills/codeman`. |
|
||||||
| Bundled CLI | `codeman skill install --case <name>` | One case. |
|
| Bundled CLI | `codeman skill install --case <name>` | One case. |
|
||||||
| Web UI | **App Settings → Agents & CLIs → Claude → Agent Skill** | Injects into each case when a Claude session is created. Off by default. |
|
| Web UI | **App Settings → Agents & CLIs → Claude → Agent Skill** | Injects into each case when a Claude session is created. Off by default. |
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
"check:public-assets": "node scripts/check-public-assets.mjs",
|
"check:public-assets": "node scripts/check-public-assets.mjs",
|
||||||
"capture:subagents": "node scripts/capture-subagent-screenshots.mjs",
|
"capture:subagents": "node scripts/capture-subagent-screenshots.mjs",
|
||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"version-packages": "changeset version && npm install --package-lock-only && node scripts/check-lockfile-sync.mjs",
|
"version-packages": "changeset version && node scripts/sync-plugin-version.mjs && npm install --package-lock-only && node scripts/check-lockfile-sync.mjs",
|
||||||
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
||||||
"knip": "npx --yes knip@latest --config config/knip.json",
|
"knip": "npx --yes knip@latest --config config/knip.json",
|
||||||
"release": "changeset publish",
|
"release": "changeset publish",
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* @fileoverview Keep the Claude Code plugin manifests in step with package.json.
|
||||||
|
*
|
||||||
|
* The repo is its own plugin marketplace (`/plugin marketplace add Ark0N/Codeman`
|
||||||
|
* reads `.claude-plugin/marketplace.json` from the repo root, and the `codeman`
|
||||||
|
* plugin it lists is the repo itself, `source: "./"`, whose one skill is
|
||||||
|
* `skills/codeman/`). Claude Code's `plugin update` only sees a new release when
|
||||||
|
* the manifest version number changes, so both manifests must carry the version
|
||||||
|
* `package.json` carries. This runs inside `npm run version-packages`, right after
|
||||||
|
* `changeset version` bumps package.json, and `test/plugin-manifest.test.ts` pins
|
||||||
|
* the result so drift fails the gate.
|
||||||
|
*
|
||||||
|
* node scripts/sync-plugin-version.mjs rewrite both manifests
|
||||||
|
* node scripts/sync-plugin-version.mjs --check exit 1 on drift, change nothing
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
const PLUGIN_NAME = 'codeman';
|
||||||
|
const MANIFESTS = ['.claude-plugin/plugin.json', '.claude-plugin/marketplace.json'];
|
||||||
|
|
||||||
|
const check = process.argv.includes('--check');
|
||||||
|
const { version } = JSON.parse(readFileSync('package.json', 'utf8'));
|
||||||
|
let drift = [];
|
||||||
|
|
||||||
|
for (const file of MANIFESTS) {
|
||||||
|
const json = JSON.parse(readFileSync(file, 'utf8'));
|
||||||
|
const targets = file.endsWith('marketplace.json')
|
||||||
|
? json.plugins.filter((p) => p.name === PLUGIN_NAME)
|
||||||
|
: [json];
|
||||||
|
if (targets.length === 0) {
|
||||||
|
console.error(`${file}: no plugin entry named "${PLUGIN_NAME}"`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
for (const target of targets) {
|
||||||
|
if (target.version !== version) {
|
||||||
|
drift.push(`${file}: ${target.version} -> ${version}`);
|
||||||
|
target.version = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!check) writeFileSync(file, JSON.stringify(json, null, 2) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drift.length === 0) {
|
||||||
|
console.log(`plugin manifests already at ${version}`);
|
||||||
|
} else if (check) {
|
||||||
|
console.error(`plugin manifest version drift (run: node scripts/sync-plugin-version.mjs):\n ${drift.join('\n ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log(`plugin manifests synced to ${version}:\n ${drift.join('\n ')}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Static guard for the Claude Code plugin the repo publishes about itself.
|
||||||
|
*
|
||||||
|
* `.claude-plugin/marketplace.json` at the repo root makes `/plugin marketplace add
|
||||||
|
* Ark0N/Codeman` work, and the one plugin it lists is the repo itself (`source: "./"`),
|
||||||
|
* so the plugin's component roots ARE the repo root. Two things follow and both are
|
||||||
|
* pinned here: the manifests must carry package.json's version (Claude Code's
|
||||||
|
* `plugin update` only sees a release when that number changes; `version-packages`
|
||||||
|
* runs `scripts/sync-plugin-version.mjs` to keep them in step), and the repo root
|
||||||
|
* must not grow any other plugin component (`commands/`, `agents/`, `hooks/`,
|
||||||
|
* `.mcp.json`, `.lsp.json`, `settings.json`), or every plugin install would silently
|
||||||
|
* ship it. The skill's frontmatter `name` is pinned too: without it the installed
|
||||||
|
* skill would be named after the cache directory, which is a version string.
|
||||||
|
*
|
||||||
|
* `claude plugin validate .claude-plugin/plugin.json` passes with one warning, that CLAUDE.md at
|
||||||
|
* the plugin root is not loaded as plugin context. That is what a repo-root plugin looks like,
|
||||||
|
* not a defect; `--strict` is therefore not the right mode for this repo.
|
||||||
|
*
|
||||||
|
* Pure filesystem reads against the real tree. Port: N/A.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||||
|
const readJson = (rel: string) => JSON.parse(readFileSync(join(ROOT, rel), 'utf8'));
|
||||||
|
|
||||||
|
const pkg = readJson('package.json');
|
||||||
|
const plugin = readJson('.claude-plugin/plugin.json');
|
||||||
|
const marketplace = readJson('.claude-plugin/marketplace.json');
|
||||||
|
|
||||||
|
describe('Claude Code plugin manifests', () => {
|
||||||
|
it('plugin.json names the codeman plugin at the package version', () => {
|
||||||
|
expect(plugin.name).toBe('codeman');
|
||||||
|
expect(plugin.version).toBe(pkg.version);
|
||||||
|
expect(plugin.license).toBe('MIT');
|
||||||
|
expect(plugin.repository).toBe('https://github.com/Ark0N/Codeman');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marketplace.json lists exactly that plugin, sourced from the repo root, at the same version', () => {
|
||||||
|
expect(marketplace.name).toBe('codeman');
|
||||||
|
expect(marketplace.owner?.name).toBeTruthy();
|
||||||
|
expect(marketplace.plugins).toHaveLength(1);
|
||||||
|
const [entry] = marketplace.plugins;
|
||||||
|
expect(entry.name).toBe(plugin.name);
|
||||||
|
expect(entry.source).toBe('./');
|
||||||
|
expect(entry.version).toBe(pkg.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the skill declares its own name, so the installed skill is codeman:codeman and not a cache-dir version string', () => {
|
||||||
|
const skill = readFileSync(join(ROOT, 'skills/codeman/SKILL.md'), 'utf8');
|
||||||
|
const frontmatter = skill.split('---')[1] ?? '';
|
||||||
|
expect(frontmatter).toMatch(/^name: codeman$/m);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the repo root carries no other plugin component the install would ship', () => {
|
||||||
|
for (const rel of ['commands', 'agents', 'hooks', '.mcp.json', '.lsp.json', 'settings.json', 'monitors']) {
|
||||||
|
expect(existsSync(join(ROOT, rel)), `${rel} at the repo root would become part of the plugin`).toBe(false);
|
||||||
|
}
|
||||||
|
// The manifest must not redirect component discovery either; the defaults are the contract.
|
||||||
|
for (const key of ['skills', 'commands', 'agents', 'hooks', 'mcpServers', 'lspServers']) {
|
||||||
|
expect(plugin[key], `plugin.json "${key}" override`).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user