cohorte 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +264 -0
- package/LICENSE +661 -0
- package/README.md +269 -0
- package/bin/cli.js +339 -0
- package/core/agents/implementer.template.md +74 -0
- package/core/agents/release.md +51 -0
- package/core/agents/review.md +85 -0
- package/core/commands/align-ds.md +32 -0
- package/core/commands/audit.md +31 -0
- package/core/commands/brainstorm.md +48 -0
- package/core/commands/build.md +91 -0
- package/core/commands/doctor.md +50 -0
- package/core/commands/fix.md +62 -0
- package/core/commands/init-pipeline.md +32 -0
- package/core/commands/refactor.md +38 -0
- package/core/commands/review.md +68 -0
- package/core/commands/ship.md +68 -0
- package/core/commands/smoke.md +55 -0
- package/core/commands/spec.md +67 -0
- package/core/commands/update-pipeline.md +96 -0
- package/core/hooks/__pycache__/gate.cpython-312.pyc +0 -0
- package/core/hooks/gate.py +129 -0
- package/core/templates/agent-handoff.md +34 -0
- package/core/templates/brainstorm-return.md +36 -0
- package/core/templates/design-brief.md +35 -0
- package/core/templates/pr-body.md +29 -0
- package/core/templates/review-feedback.md +36 -0
- package/core/templates/spec.template.md +84 -0
- package/core/templates/steps/init-pipeline/01-detect-stack.md +40 -0
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +41 -0
- package/core/templates/steps/init-pipeline/03-draft-profile.md +10 -0
- package/core/templates/steps/init-pipeline/04-write-render.md +88 -0
- package/core/templates/steps/init-pipeline/05-report.md +12 -0
- package/dashboard/README.md +54 -0
- package/dashboard/dist/apple-touch-icon-180.png +0 -0
- package/dashboard/dist/assets/index-CoBuEdy-.js +42 -0
- package/dashboard/dist/assets/index-DN5OGW9g.css +1 -0
- package/dashboard/dist/favicon-16.png +0 -0
- package/dashboard/dist/favicon-32.png +0 -0
- package/dashboard/dist/favicon-48.png +0 -0
- package/dashboard/dist/icon-192.png +0 -0
- package/dashboard/dist/icon-512.png +0 -0
- package/dashboard/dist/index.html +16 -0
- package/dashboard/server/doctor.js +266 -0
- package/dashboard/server/fleet.js +119 -0
- package/dashboard/server/index.js +306 -0
- package/dashboard/server/kanban.js +158 -0
- package/dashboard/server/versions.js +111 -0
- package/dashboard/server/yaml.js +126 -0
- package/install.ps1 +359 -0
- package/install.sh +301 -0
- package/package.json +40 -0
- package/profile/PIPELINE.template.md +208 -0
- package/profile/SCHEMA.md +303 -0
- package/profile/cohorte.config.template.yaml +43 -0
- package/scripts/new-feature.sh.template +89 -0
- package/scripts/remove-feature.sh.template +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/TheBidouilleAgency/cohorte/main/assets/cohorte-banner.png" alt="Cohorte — portable multi-agent pipeline for Claude Code" width="720">
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/cohorte)
|
|
6
|
+
[](https://www.npmjs.com/package/cohorte)
|
|
7
|
+
[](https://github.com/TheBidouilleAgency/cohorte/actions/workflows/publish.yml)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
A **portable, stack-agnostic multi-agent pipeline** for Claude Code. Install it once globally,
|
|
14
|
+
then one command per project (`/init-pipeline`) adapts it to that project's stack.
|
|
15
|
+
|
|
16
|
+
- **The dev pipeline** — a human **lead** drives feature work through gated commands, dispatching
|
|
17
|
+
**stateless agents** that only communicate through a frozen contract:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/brainstorm → /spec → (design) → /build <id> → /smoke → /review → (/fix) → /ship
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How it works — three layers
|
|
24
|
+
|
|
25
|
+
| Layer | What it holds | Lives in | Scope |
|
|
26
|
+
| --- | --- | --- | --- |
|
|
27
|
+
| **Generic core** | the workflow doctrine: commands, fixed agents, templates, hooks — zero project facts | `~/.claude` (global) — or vendored in a repo's `.claude/` (bundled) | identical everywhere, installed once |
|
|
28
|
+
| **Project profile** | stack, surfaces, commands, conventions, gates | `PIPELINE.md` + rendered surface agents + `gate-config.json`, **committed in each repo** | generated per project by `/init-pipeline` |
|
|
29
|
+
| **User config** | kanban board links + shared Obsidian vault path | `~/.claude/cohorte.config.yaml` | personal, project-independent |
|
|
30
|
+
|
|
31
|
+
The core never hardcodes stack facts. Two mechanisms keep it generic:
|
|
32
|
+
|
|
33
|
+
1. **Runtime indirection** — commands/agents read project facts from `PIPELINE.md` (dev pipeline) or
|
|
34
|
+
`~/.claude/cohorte.config.yaml` (kanban board links + shared vault) at run time — an agent's
|
|
35
|
+
_first action_ is to read its config.
|
|
36
|
+
2. **Render-at-init** — things that must be in agent frontmatter (name, `tools:`, surface ownership)
|
|
37
|
+
are rendered per **surface** by `/init-pipeline` from `implementer.template.md`.
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
Only one hard requirement — the rest is optional and independent:
|
|
42
|
+
|
|
43
|
+
- **Node ≥ 18 + npm** — _required_, for the `npx` installer that lays down the core. Nothing else needs it.
|
|
44
|
+
- **[`uv`](https://docs.astral.sh/uv/) + the Serena CLI** — _optional_, the default code-retrieval
|
|
45
|
+
provider. Install it separately (`uv tool install -p 3.13 serena-agent && uv tool update-shell`); the
|
|
46
|
+
`npx` install neither needs nor touches it, so the order between the two is irrelevant. Without Serena
|
|
47
|
+
the pipeline still runs — agents just fall back to Grep/Read. Having it installed **before**
|
|
48
|
+
`/init-pipeline` lets init wire it in one pass (otherwise `/update-pipeline` wires it later).
|
|
49
|
+
- **On a new machine cloning a repo that's already pipeline-ised:** the Serena registration is committed
|
|
50
|
+
in the repo's `.mcp.json` (project scope, portable) — you don't re-wire. Just install the Serena CLI,
|
|
51
|
+
restart the session, and run `/doctor` to confirm it connects.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
The pipeline ships as an npm package (`cohorte`), so releases are semver-tagged and
|
|
56
|
+
`npx` always fetches the latest published version — no clone needed, works on macOS/Linux/Windows.
|
|
57
|
+
|
|
58
|
+
**Global (recommended)** — install the generic core ONCE into `~/.claude`; it serves every repo on
|
|
59
|
+
your machine. Nothing is copied per project; the gate hook is registered once and reads each repo's
|
|
60
|
+
own `gate-config.json`:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npx cohorte install --global
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The per-project part is NOT the core — it's the **profile** `/init-pipeline` generates and you
|
|
67
|
+
commit: `PIPELINE.md`, the rendered surface agents, `gate-config.json`, `settings.json`, `specs/`.
|
|
68
|
+
**That's what makes team work possible in global mode**: everything project-specific travels with the
|
|
69
|
+
repo; each teammate just runs the same global one-liner once, guided by the committed
|
|
70
|
+
`.claude/pipeline.json` pointer (core version + install command) that `/init-pipeline` writes.
|
|
71
|
+
|
|
72
|
+
<details>
|
|
73
|
+
<summary><strong>Alternative: per-project (bundled)</strong> — vendor the core into the repo itself.</summary>
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
# inside your project (or pass its path as an argument)
|
|
77
|
+
npx cohorte install
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Copies the core into `<project>/.claude`, committed with the repo. Choose this when you want
|
|
81
|
+
**zero-setup onboarding** (teammates get the core with `git clone`, no install step at all) and a
|
|
82
|
+
core version **pinned per repo** (no drift between projects or teammates). Cost: the core is
|
|
83
|
+
duplicated in every repo and each repo updates separately.
|
|
84
|
+
|
|
85
|
+
</details>
|
|
86
|
+
|
|
87
|
+
<details>
|
|
88
|
+
<summary><strong>No Node/npm?</strong> The original script installers still work.</summary>
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
# global (recommended) # per-project (bundled)
|
|
92
|
+
sh install.sh --global sh install.sh
|
|
93
|
+
# or piped:
|
|
94
|
+
curl -fsSL https://raw.githubusercontent.com/TheBidouilleAgency/cohorte/main/install.sh | sh -s -- --global
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
# Windows (PowerShell 5.1+)
|
|
99
|
+
.\install.ps1 -Global # or without -Global for per-project
|
|
100
|
+
# or: & ([scriptblock]::Create((irm https://raw.githubusercontent.com/TheBidouilleAgency/cohorte/main/install.ps1))) -Global
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Script installs from a git checkout stamp the version as `<semver> (<sha>)`; the npm CLI stamps the
|
|
104
|
+
published semver. Both land in `.claude/pipeline/VERSION` and the `pipeline.json` pointer.
|
|
105
|
+
|
|
106
|
+
</details>
|
|
107
|
+
|
|
108
|
+
> **After installing (or updating): restart Claude Code / start a new session.** Slash commands and
|
|
109
|
+
> agents are scanned at session start — in an already-open session the new `/init-pipeline`,
|
|
110
|
+
> `/build`, etc. won't appear until you reload. This is the #1 "the install didn't work" trap.
|
|
111
|
+
|
|
112
|
+
Then, in Claude Code (from any repo, once the core is installed either way):
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
/init-pipeline
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It **detects** your stack (package manager, workspaces, frameworks, test runners, linters, git remote,
|
|
119
|
+
design system), **interviews** you for the gaps, and **generates**:
|
|
120
|
+
|
|
121
|
+
- `PIPELINE.md` — the project profile (a machine-readable `pipeline-profile` YAML block + prose conventions)
|
|
122
|
+
- one implementer agent per **surface** (e.g. `backend.md`, `frontend.md`) with strict tree ownership
|
|
123
|
+
and a per-surface `model:` tier (Haiku for mechanical surfaces, bigger models where design decisions live)
|
|
124
|
+
- `.claude/gate-config.json` + `.claude/settings.json` — the destructive-command gate, plus an
|
|
125
|
+
`allow` list of the project's read-only commands so agents don't stall on permission prompts
|
|
126
|
+
- a **code-retrieval provider** wired as a committed project-scope MCP server —
|
|
127
|
+
[Serena](https://github.com/oraios/serena) by default (live LSP symbol navigation: agents query
|
|
128
|
+
symbols instead of grep-and-reading whole files; `graphify` or `none` also available via the
|
|
129
|
+
profile's `retrieval.provider`)
|
|
130
|
+
- `scripts/new-feature.sh` + `remove-feature.sh` — parallel worktree isolation (if you enable it)
|
|
131
|
+
- `specs/_template.md`
|
|
132
|
+
|
|
133
|
+
Sanity-check `PIPELINE.md`, commit it, and run `/brainstorm`.
|
|
134
|
+
|
|
135
|
+
## Update
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npx cohorte@latest update --global # the shared core in ~/.claude (recommended setup)
|
|
139
|
+
npx cohorte@latest update # a repo's bundled core in <project>/.claude
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
(Script equivalents: `sh install.sh --update [--global]` / `.\install.ps1 -Update [-Global]`.)
|
|
143
|
+
|
|
144
|
+
The installer refreshes the generic core (commands, hook, templates) **without** touching your
|
|
145
|
+
`PIPELINE.md`, rendered agents, `gate-config.json`, `settings.json`, or your filled
|
|
146
|
+
`~/.claude/cohorte.config.yaml`.
|
|
147
|
+
|
|
148
|
+
From inside Claude Code, prefer **`/update-pipeline`**: it runs the right update invocation for your
|
|
149
|
+
install scope, reports `old → new` — and then **reconciles the repo's generated files to the new
|
|
150
|
+
core**: new profile fields are added at their defaults (you're only asked for genuinely new
|
|
151
|
+
decisions), surface agents are re-rendered, settings are patched additively, new capabilities get
|
|
152
|
+
wired. **`/init-pipeline` is one-time per project** — after init, `/update-pipeline` is the only
|
|
153
|
+
maintenance command you ever run (`/build` auto-grows surfaces as specs need them).
|
|
154
|
+
|
|
155
|
+
## Dashboard — a local web cockpit
|
|
156
|
+
|
|
157
|
+
A browser view of pipeline state, for when a checklist beats scanning files:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
npx cohorte dashboard # serves http://localhost:4317 (Ctrl-C to stop)
|
|
161
|
+
npx cohorte dashboard <path> # start focused on another project
|
|
162
|
+
npx cohorte dashboard --port=4400 --open # custom port, open the browser
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Bound to `127.0.0.1` by default** — the dashboard's actions execute code (install/update/reset,
|
|
166
|
+
and `/init-pipeline`·`/update-pipeline` via headless Claude), so it must stay on loopback. Each user
|
|
167
|
+
runs their own agent and drives only their own machine. `--host=0.0.0.0` exposes it to the network
|
|
168
|
+
(it prints a security warning) — only on a trusted network, since anyone who reaches the port can run
|
|
169
|
+
those actions.
|
|
170
|
+
|
|
171
|
+
- **Fleet overview** — the global core version vs npm latest, plus every tracked project's freshness
|
|
172
|
+
and health at a glance. Add a project by absolute path or with the **folder picker** (Browse…); the
|
|
173
|
+
set is remembered in `~/.claude/cohorte-dashboard.json`.
|
|
174
|
+
- **Per-project drill-down** — Freshness (installed core vs npm), `/doctor` rendered as a live
|
|
175
|
+
✅/⚠️/❌ checklist (each failure with its fix), the **Surfaces ↔ agents** map from `PIPELINE.md`,
|
|
176
|
+
and one board: a **Kanban** if the project has a linked Obsidian board (columns + cards from the
|
|
177
|
+
vault, with clickable PR links + live open/merged/closed status and a ship-date-sorted Shipped
|
|
178
|
+
column, via `gh`), otherwise a **Specs board** from `specs/*.md` (by `draft · frozen · in-review ·
|
|
179
|
+
shipped`). The Kanban supersedes the Specs board when both would apply.
|
|
180
|
+
- **Actions** (stream their output live) — **Update / Install core** (the shared global core, or a
|
|
181
|
+
repo's bundled core); **Init-pipeline / Update-pipeline**, which run those Claude Code commands
|
|
182
|
+
**headless** (`claude -p`, autonomous — Init skips the interactive interview, so review the result);
|
|
183
|
+
and **Reset pipeline**, which backs up then wipes a project's pipeline footprint and reinstalls a
|
|
184
|
+
fresh core. Buttons render only when they apply (e.g. Init only when there's no profile).
|
|
185
|
+
|
|
186
|
+
Runtime is **dependency-free** — node's built-in `http` server serves a prebuilt React app (the app
|
|
187
|
+
source lives in `dashboard/app/`, built to `dashboard/dist/` at publish time). The `/doctor` checks
|
|
188
|
+
are reimplemented in JS, so the dashboard needs no Claude session to compute state. See
|
|
189
|
+
[`dashboard/README.md`](dashboard/README.md) for the architecture.
|
|
190
|
+
|
|
191
|
+
## Releasing (maintainers)
|
|
192
|
+
|
|
193
|
+
Versions are tracked with npm semver — the published package is the release artifact.
|
|
194
|
+
Publishing is fully automated: [`publish.yml`](.github/workflows/publish.yml) runs on every push
|
|
195
|
+
to `main`; when `package.json`'s version isn't on the registry yet it publishes to npm (trusted
|
|
196
|
+
publishing / provenance), pushes the `vX.Y.Z` tag, and creates the GitHub release. Pushes without
|
|
197
|
+
a version bump just run the sanity checks.
|
|
198
|
+
|
|
199
|
+
**Releasing = editing one line.** Bump `"version"` in `package.json` (by hand, or
|
|
200
|
+
`npm version patch --no-git-tag-version`), commit, push — CI does the rest (publish + tag +
|
|
201
|
+
release). No local tagging needed.
|
|
202
|
+
|
|
203
|
+
`npx cohorte@latest …` then serves the new version everywhere; installed cores record
|
|
204
|
+
it in `.claude/pipeline/VERSION` and bundled repos in their committed `pipeline.json` pointer.
|
|
205
|
+
|
|
206
|
+
## The commands
|
|
207
|
+
|
|
208
|
+
| Command | Role |
|
|
209
|
+
| -------------------- | ------------------------------------------------------------------------------------- |
|
|
210
|
+
| `/init-pipeline` | Detect stack → interview → generate the profile + agents. Run once per project. |
|
|
211
|
+
| `/brainstorm` | Interactive persona panel that pressure-tests a feature idea. |
|
|
212
|
+
| `/spec` | Freeze the feature spec + contract into `specs/<id>.md` (UI features also get a standalone design brief at `specs/design/<id>.md`). Also applies review returns. |
|
|
213
|
+
| `/build <id>` | Lead authors the contract, then dispatches one implementer per surface in parallel. |
|
|
214
|
+
| `/smoke <id>` | Run the feature for real: infra up, contract endpoints, UI flows, design conformance. |
|
|
215
|
+
| `/review <id>` | Read-only review agents (one per touched surface, parallel) audit the diff vs the spec. |
|
|
216
|
+
| `/fix <id>` | Apply a review/smoke report: remediation into the spec, re-dispatch only the surfaces with findings. |
|
|
217
|
+
| `/ship <id>` | Release agent commits, pushes, opens the PR; watches CI; proposes worktree teardown. |
|
|
218
|
+
| `/audit [path]` | Prioritized refactor backlog for existing code. |
|
|
219
|
+
| `/refactor <domain>` | Apply the backlog for one surface, TDD-first. |
|
|
220
|
+
| `/align-ds` | Align the code UI kit to the design system (no-op if none configured). |
|
|
221
|
+
| `/update-pipeline` | Refresh the installed core (global or bundled) to the latest published version. |
|
|
222
|
+
| `/doctor` | Diagnose the installation (core, agents↔surfaces, hooks, gate, retrieval, worktrees). |
|
|
223
|
+
|
|
224
|
+
### Run the loop cheaply — `/clear` between stages
|
|
225
|
+
|
|
226
|
+
Every command reloads all the state it needs **from disk** — the frozen spec, the contract, the diff, the
|
|
227
|
+
Remediation checkboxes, the freshness stamp, and the last `/review`·`/smoke` report (staged to a gitignored
|
|
228
|
+
`specs/reports/<id>.md`). Nothing essential lives in the conversation. So the loop is **`/clear`-safe at
|
|
229
|
+
every boundary**:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
/spec → /clear → /build → /clear → /smoke → /clear → /review → /clear → /fix → /clear → /review → /ship
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`/clear`-ing between stages sheds the accumulated main-thread context, which is the single biggest token
|
|
236
|
+
lever: long sessions (>150k) are expensive even when cached. Each command tells you when its handoff is
|
|
237
|
+
safe to clear. If you'd rather stay in one session, `/compact` mid-task does the lighter version. (Claude
|
|
238
|
+
can't fire `/clear` itself — it's a client-side command; the pipeline just makes it always safe to type.)
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
[AGPL-3.0](LICENSE). Free to use, including commercially — but if you modify it and distribute it
|
|
243
|
+
or offer it as a network service, you must publish your modifications under the same license.
|
|
244
|
+
|
|
245
|
+
## Profile reference
|
|
246
|
+
|
|
247
|
+
See `profile/SCHEMA.md` for every field in `PIPELINE.md` and how the pipeline uses it.
|
|
248
|
+
|
|
249
|
+
## Layout of this repo
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
package.json # npm package (cohorte) — semver source of truth
|
|
253
|
+
bin/cli.js # the npm CLI: install / update / dashboard / version (cross-platform, no deps)
|
|
254
|
+
install.sh # script installer (fresh + --update) for no-Node environments
|
|
255
|
+
install.ps1 # same installer for Windows PowerShell (fresh + -Update)
|
|
256
|
+
core/ # copied verbatim into ~/.claude (global) or <project>/.claude (bundled)
|
|
257
|
+
agents/ # implementer.template.md (rendered per surface) + review.md + release.md
|
|
258
|
+
commands/ # init-pipeline + the workflow commands + /update-pipeline
|
|
259
|
+
hooks/ # gate.py (destructive-command gate; branch-aware — git/docker free off the default branch)
|
|
260
|
+
templates/ # handoff / brainstorm-return / design-brief / review-feedback / pr-body / spec
|
|
261
|
+
profile/
|
|
262
|
+
PIPELINE.template.md # the profile skeleton /init-pipeline fills
|
|
263
|
+
SCHEMA.md # field reference
|
|
264
|
+
cohorte.config.template.yaml # seeds ~/.claude/cohorte.config.yaml (kanban)
|
|
265
|
+
scripts/ # new-feature / remove-feature worktree-isolation templates
|
|
266
|
+
dashboard/ # local web cockpit (npx … dashboard) — see dashboard/README.md
|
|
267
|
+
server/ # dependency-free node runtime (serves the built app + JSON/stream API)
|
|
268
|
+
app/ # Vite + React source (built to dashboard/dist/ at publish time)
|
|
269
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cohorte — installer CLI for the portable multi-agent pipeline.
|
|
3
|
+
// Cross-platform, dependency-free port of install.sh / install.ps1.
|
|
4
|
+
//
|
|
5
|
+
// npx cohorte install # bundle the core into <cwd>/.claude (committable)
|
|
6
|
+
// npx cohorte install [target] # same, into another project
|
|
7
|
+
// npx cohorte install --global # one shared core in ~/.claude
|
|
8
|
+
// npx cohorte update [--global] # refresh the core, keep every generated file
|
|
9
|
+
// npx cohorte version
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const pkgRoot = path.resolve(__dirname, '..');
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
|
|
20
|
+
const VERSION = pkg.version;
|
|
21
|
+
|
|
22
|
+
const REPO_URL = 'https://github.com/TheBidouilleAgency/cohorte';
|
|
23
|
+
|
|
24
|
+
function usage(code) {
|
|
25
|
+
console.log(`cohorte v${VERSION}
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
cohorte install [target] [--global]
|
|
29
|
+
cohorte update [target] [--global]
|
|
30
|
+
cohorte dashboard [target] [--port=N] [--host=ADDR] [--open]
|
|
31
|
+
cohorte version
|
|
32
|
+
|
|
33
|
+
Commands:
|
|
34
|
+
install Fresh install. Default: bundle the core into <target>/.claude
|
|
35
|
+
(committed with the repo). --global: one shared core in ~/.claude,
|
|
36
|
+
available to every project on this machine.
|
|
37
|
+
update Refresh the stack-agnostic core only. PIPELINE.md, rendered surface
|
|
38
|
+
agents, gate-config.json, settings.json and your filled
|
|
39
|
+
~/.claude/cohorte.config.yaml are never touched.
|
|
40
|
+
dashboard Serve a local web cockpit for the pipeline (freshness, /doctor
|
|
41
|
+
health, specs board, install/update actions). Binds 127.0.0.1:4317
|
|
42
|
+
by default (loopback only — its actions execute code). --host=ADDR
|
|
43
|
+
to expose (e.g. --host=0.0.0.0, prints a security warning). --open
|
|
44
|
+
to launch the browser.
|
|
45
|
+
version Print the installed CLI version.`);
|
|
46
|
+
process.exit(code);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- arg parsing -------------------------------------------------------------
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
let mode = null;
|
|
52
|
+
let scope = 'project';
|
|
53
|
+
let target = process.cwd();
|
|
54
|
+
let port = parseInt(process.env.COHORTE_DASHBOARD_PORT, 10) || 4317;
|
|
55
|
+
// Bind to loopback by default — the dashboard's action endpoints execute code (install/update/
|
|
56
|
+
// reset/claude), so it must NOT be reachable from the network unless the user explicitly opts in.
|
|
57
|
+
let host = process.env.COHORTE_DASHBOARD_HOST || '127.0.0.1';
|
|
58
|
+
let openBrowser = false;
|
|
59
|
+
|
|
60
|
+
for (const a of args) {
|
|
61
|
+
if (a === 'install' || a === 'update' || a === 'dashboard') mode = a;
|
|
62
|
+
else if (a === 'version' || a === '--version' || a === '-v') { console.log(VERSION); process.exit(0); }
|
|
63
|
+
else if (a === '--global' || a === '-g') scope = 'global';
|
|
64
|
+
else if (a.startsWith('--port=')) { port = parseInt(a.slice(7), 10); }
|
|
65
|
+
else if (a.startsWith('--host=')) { host = a.slice(7); }
|
|
66
|
+
else if (a === '--open') { openBrowser = true; }
|
|
67
|
+
else if (a === 'help' || a === '--help' || a === '-h') usage(0);
|
|
68
|
+
else if (a.startsWith('-')) { console.error(`error: unknown flag: ${a}`); usage(2); }
|
|
69
|
+
else target = path.resolve(a);
|
|
70
|
+
}
|
|
71
|
+
if (!mode) usage(args.length ? 2 : 0);
|
|
72
|
+
|
|
73
|
+
// --- dashboard: local web cockpit -------------------------------------------
|
|
74
|
+
// Short-circuits before the install/update machinery (CommonJS wraps the module,
|
|
75
|
+
// so a top-level return is valid here). Runtime is dependency-free node `http`.
|
|
76
|
+
if (mode === 'dashboard') {
|
|
77
|
+
const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
78
|
+
require('../dashboard/server')({ projectRoot: target, globalDir, port, host, openBrowser, pkgRoot, version: VERSION });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- paths -------------------------------------------------------------------
|
|
83
|
+
const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
84
|
+
const dest = scope === 'global' ? globalDir : path.join(target, '.claude');
|
|
85
|
+
const src = pkgRoot;
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(path.join(src, 'core'))) {
|
|
88
|
+
console.error(`error: pipeline source not found (no core/ in ${src})`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
92
|
+
|
|
93
|
+
// --- helpers (mirror install.sh) --------------------------------------------
|
|
94
|
+
function copyCore() {
|
|
95
|
+
for (const d of ['commands', 'hooks', 'templates']) {
|
|
96
|
+
fs.cpSync(path.join(src, 'core', d), path.join(dest, d), { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
// 0.1.19 renamed questionnaire-domain-brief.md → research-brief.md; drop the stale copy.
|
|
99
|
+
fs.rmSync(path.join(dest, 'templates', 'questionnaire-domain-brief.md'), { force: true });
|
|
100
|
+
const pipelineDir = path.join(dest, 'pipeline');
|
|
101
|
+
fs.mkdirSync(path.join(pipelineDir, 'scripts'), { recursive: true });
|
|
102
|
+
for (const f of ['PIPELINE.template.md', 'SCHEMA.md', 'cohorte.config.template.yaml']) {
|
|
103
|
+
fs.copyFileSync(path.join(src, 'profile', f), path.join(pipelineDir, f));
|
|
104
|
+
}
|
|
105
|
+
for (const f of fs.readdirSync(path.join(src, 'scripts'))) {
|
|
106
|
+
if (f.endsWith('.template')) {
|
|
107
|
+
fs.copyFileSync(path.join(src, 'scripts', f), path.join(pipelineDir, 'scripts', f));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
fs.copyFileSync(path.join(src, 'core', 'agents', 'implementer.template.md'),
|
|
111
|
+
path.join(pipelineDir, 'implementer.template.md'));
|
|
112
|
+
fs.writeFileSync(path.join(pipelineDir, 'VERSION'), VERSION + '\n');
|
|
113
|
+
if (process.platform !== 'win32') {
|
|
114
|
+
try { fs.chmodSync(path.join(dest, 'hooks', 'gate.py'), 0o755); } catch { /* optional */ }
|
|
115
|
+
}
|
|
116
|
+
scrubTddGate();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The TDD gate was removed in 0.1.6. Older installs have hooks/tdd_gate.py on disk and
|
|
120
|
+
// registered in settings.json — copy-over never deletes, and a registered hook whose file
|
|
121
|
+
// is gone errors on every Write/Edit, so scrub both.
|
|
122
|
+
function scrubTddGate() {
|
|
123
|
+
fs.rmSync(path.join(dest, 'hooks', 'tdd_gate.py'), { force: true });
|
|
124
|
+
const settingsPath = path.join(dest, 'settings.json');
|
|
125
|
+
let data;
|
|
126
|
+
try { data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { return; }
|
|
127
|
+
const pre = data && data.hooks && Array.isArray(data.hooks.PreToolUse) ? data.hooks.PreToolUse : null;
|
|
128
|
+
if (!pre) return;
|
|
129
|
+
const kept = pre.filter(entry => !(entry.hooks || []).some(
|
|
130
|
+
h => typeof h.command === 'string' && h.command.trim().endsWith('tdd_gate.py')));
|
|
131
|
+
if (kept.length !== pre.length) {
|
|
132
|
+
data.hooks.PreToolUse = kept;
|
|
133
|
+
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n');
|
|
134
|
+
console.log(' · removed the retired tdd_gate.py hook (file + settings registration)');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// the fixed (non-rendered) agents: the dev review/release pipeline agents
|
|
139
|
+
function copyFixedAgents() {
|
|
140
|
+
fs.mkdirSync(path.join(dest, 'agents'), { recursive: true });
|
|
141
|
+
for (const f of ['review.md', 'release.md']) {
|
|
142
|
+
fs.copyFileSync(path.join(src, 'core', 'agents', f), path.join(dest, 'agents', f));
|
|
143
|
+
}
|
|
144
|
+
// 0.1.19 split the bi-mode questionnaire-researcher into research-agent + questionnaire-architect;
|
|
145
|
+
// copy-over never deletes, so scrub the retired agent lest a dead subagent_type linger.
|
|
146
|
+
fs.rmSync(path.join(dest, 'agents', 'questionnaire-researcher.md'), { force: true });
|
|
147
|
+
scrubResearchQuestionnaire();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The research + questionnaire capability was removed. Older installs have its agents, commands,
|
|
151
|
+
// templates and template-step dirs on disk; copy-over never deletes, so scrub every orphan.
|
|
152
|
+
function scrubResearchQuestionnaire() {
|
|
153
|
+
for (const f of ['research-agent.md', 'questionnaire-architect.md',
|
|
154
|
+
'questionnaire-writer.md', 'questionnaire-validator.md']) {
|
|
155
|
+
fs.rmSync(path.join(dest, 'agents', f), { force: true });
|
|
156
|
+
}
|
|
157
|
+
for (const f of ['research.md', 'questionnaire.md']) {
|
|
158
|
+
fs.rmSync(path.join(dest, 'commands', f), { force: true });
|
|
159
|
+
}
|
|
160
|
+
for (const f of ['research-brief.md', 'questionnaire-blueprint.md',
|
|
161
|
+
'questionnaire-declaration.md', 'questionnaire-verdict.md']) {
|
|
162
|
+
fs.rmSync(path.join(dest, 'templates', f), { force: true });
|
|
163
|
+
}
|
|
164
|
+
for (const d of ['research', 'questionnaire']) {
|
|
165
|
+
fs.rmSync(path.join(dest, 'templates', 'steps', d), { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- interactive config helpers ---------------------------------------------
|
|
170
|
+
// Ask one question on the TTY. Resolves to the trimmed answer (or '' on EOF).
|
|
171
|
+
function ask(question) {
|
|
172
|
+
const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
173
|
+
return new Promise(res => rl.question(question, a => { rl.close(); res((a || '').trim()); }));
|
|
174
|
+
}
|
|
175
|
+
function yes(a) { return /^(y|yes|o|oui)$/i.test(a); }
|
|
176
|
+
|
|
177
|
+
// Set the value on the line carrying `# cfg:<cfgKey>`, preserving the yaml key + the comment.
|
|
178
|
+
// The config template anchors every interactive field this way, so we never parse YAML.
|
|
179
|
+
// Line-scoped on purpose (a multiline regex would let \s span newlines and mangle keys).
|
|
180
|
+
function setCfg(text, cfgKey, value) {
|
|
181
|
+
const marker = `# cfg:${cfgKey}`;
|
|
182
|
+
return text.split('\n').map(line => {
|
|
183
|
+
const idx = line.indexOf(marker);
|
|
184
|
+
if (idx === -1) return line;
|
|
185
|
+
const m = line.slice(0, idx).match(/^(\s*[\w.]+:\s*)/); // " key: "
|
|
186
|
+
return m ? `${m[1]}${value} ${line.slice(idx)}` : line;
|
|
187
|
+
}).join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Fill the seeded config from a short TTY interview (shared Obsidian vault for the kanban mirror).
|
|
191
|
+
// Kanban is per-project, so it is wired later by /init-pipeline — not asked here.
|
|
192
|
+
async function promptConfig(text) {
|
|
193
|
+
console.log('\n Quick setup (Enter to skip — you can also wire this later via');
|
|
194
|
+
console.log(' /init-pipeline or /update-pipeline):');
|
|
195
|
+
const vault = await ask(' · absolute path to your shared Obsidian vault (for the kanban mirror): ');
|
|
196
|
+
if (vault) text = setCfg(text, 'vault_path', `"${vault}"`);
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// The pipeline capability config is USER-level (vault, Notion DB, kanban boards) — it lives in
|
|
201
|
+
// ~/.claude regardless of install scope. Seed it only if the user has no copy (consolidated OR
|
|
202
|
+
// legacy). On a TTY, offer a quick interview to fill it; otherwise seed disabled defaults.
|
|
203
|
+
async function seedConfig() {
|
|
204
|
+
const cfg = path.join(globalDir, 'cohorte.config.yaml');
|
|
205
|
+
// Pre-rename names, newest first — read as a fallback so upgrades don't lose the config.
|
|
206
|
+
const legacy = ['thebidouille.config.yaml']
|
|
207
|
+
.map((n) => path.join(globalDir, n)).find(fs.existsSync);
|
|
208
|
+
if (fs.existsSync(cfg)) { console.log(` · kept your existing ${cfg}`); return; }
|
|
209
|
+
if (legacy) {
|
|
210
|
+
console.log(` · found legacy ${legacy} — kept as-is (still read as a fallback).`);
|
|
211
|
+
console.log(' Run /update-pipeline to migrate it into cohorte.config.yaml + wire the kanban.');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
fs.mkdirSync(path.dirname(cfg), { recursive: true });
|
|
215
|
+
let text = fs.readFileSync(path.join(src, 'profile', 'cohorte.config.template.yaml'), 'utf8');
|
|
216
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
217
|
+
text = await promptConfig(text);
|
|
218
|
+
fs.writeFileSync(cfg, text);
|
|
219
|
+
console.log(` · seeded ${cfg} from your answers`);
|
|
220
|
+
} else {
|
|
221
|
+
fs.writeFileSync(cfg, text);
|
|
222
|
+
console.log(` · seeded ${cfg} (disabled defaults — enable via /init-pipeline or /update-pipeline)`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function findPython() {
|
|
227
|
+
const candidates = process.platform === 'win32' ? ['py', 'python', 'python3'] : ['python3', 'python'];
|
|
228
|
+
for (const c of candidates) {
|
|
229
|
+
const r = spawnSync(c, ['--version'], { stdio: 'ignore', shell: false });
|
|
230
|
+
if (r.status === 0) return c;
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Register the profile-driven gate hook in the GLOBAL settings.json. Idempotent: the
|
|
236
|
+
// hook reads each repo's own .claude/gate-config.json (and no-ops where absent),
|
|
237
|
+
// so one registration serves every project.
|
|
238
|
+
function registerGlobalHook() {
|
|
239
|
+
const python = findPython();
|
|
240
|
+
if (!python) return 'skipped (no python found — register the gate hook manually)';
|
|
241
|
+
const settingsPath = path.join(dest, 'settings.json');
|
|
242
|
+
let data = {};
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
245
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) data = parsed;
|
|
246
|
+
} catch { /* absent or invalid → start fresh */ }
|
|
247
|
+
if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {};
|
|
248
|
+
if (!Array.isArray(data.hooks.PreToolUse)) data.hooks.PreToolUse = [];
|
|
249
|
+
const pre = data.hooks.PreToolUse;
|
|
250
|
+
const hooks = [
|
|
251
|
+
{ file: path.join(dest, 'hooks', 'gate.py'), matcher: 'Bash' },
|
|
252
|
+
];
|
|
253
|
+
for (const { file, matcher } of hooks) {
|
|
254
|
+
const base = path.basename(file);
|
|
255
|
+
const already = pre.some(entry => (entry.hooks || []).some(
|
|
256
|
+
h => typeof h.command === 'string' && h.command.trim().endsWith(base)));
|
|
257
|
+
if (!already) {
|
|
258
|
+
const cmd = process.platform === 'win32' ? `${python} "${file}"` : `${python} ${file}`;
|
|
259
|
+
pre.push({ matcher, hooks: [{ type: 'command', command: cmd }] });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n');
|
|
263
|
+
return 'ok';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Bump only the core_version in a repo's committed .claude/pipeline.json (bundled mode).
|
|
267
|
+
// Leaves every other field intact; no-ops if the pointer is absent or has no core_version.
|
|
268
|
+
function bumpPointerVersion(ptr) {
|
|
269
|
+
if (!fs.existsSync(ptr)) return;
|
|
270
|
+
let data;
|
|
271
|
+
try { data = JSON.parse(fs.readFileSync(ptr, 'utf8')); } catch { return; }
|
|
272
|
+
if (data && typeof data === 'object' && 'core_version' in data) {
|
|
273
|
+
data.core_version = VERSION;
|
|
274
|
+
fs.writeFileSync(ptr, JSON.stringify(data, null, 2) + '\n');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- run ---------------------------------------------------------------------
|
|
279
|
+
(async () => {
|
|
280
|
+
if (scope === 'global') {
|
|
281
|
+
console.log(mode === 'install'
|
|
282
|
+
? `→ installing pipeline core GLOBALLY into ${dest}`
|
|
283
|
+
: `→ updating pipeline core GLOBALLY in ${dest} (keeping global settings.json)`);
|
|
284
|
+
copyFixedAgents();
|
|
285
|
+
copyCore();
|
|
286
|
+
const hookState = mode === 'install' ? registerGlobalHook() : 'unchanged';
|
|
287
|
+
await seedConfig();
|
|
288
|
+
console.log(`
|
|
289
|
+
✓ pipeline core installed globally into ${dest} (version ${VERSION})
|
|
290
|
+
gate hook: ${hookState} (reads each repo's .claude/gate-config.json; silent where absent)
|
|
291
|
+
|
|
292
|
+
The commands (/init-pipeline, /brainstorm, /build …) and the review/release agents are now
|
|
293
|
+
available in EVERY project on this machine — nothing is copied per repo.
|
|
294
|
+
|
|
295
|
+
Per repo:
|
|
296
|
+
1. Open the project in Claude Code.
|
|
297
|
+
2. Run /init-pipeline — it generates PIPELINE.md, renders the surface agents, writes
|
|
298
|
+
.claude/gate-config.json, and drops a committed .claude/pipeline.json pointer so
|
|
299
|
+
teammates know to install the global core (${REPO_URL}).
|
|
300
|
+
3. Commit PIPELINE.md + .claude/, then /brainstorm to start a feature.
|
|
301
|
+
|
|
302
|
+
Update later with: npx cohorte@latest update --global
|
|
303
|
+
|
|
304
|
+
Global kanban config, user-scoped — optional:
|
|
305
|
+
· One consolidated file: ${path.join(globalDir, 'cohorte.config.yaml')}
|
|
306
|
+
· Don't hand-edit it — /init-pipeline (new project) and /update-pipeline (existing) wire it
|
|
307
|
+
for you: creating + syncing an Obsidian kanban board of the pipeline in your shared vault.`);
|
|
308
|
+
} else if (mode === 'install') {
|
|
309
|
+
console.log(`→ installing pipeline core into ${dest}`);
|
|
310
|
+
copyFixedAgents();
|
|
311
|
+
copyCore();
|
|
312
|
+
await seedConfig();
|
|
313
|
+
fs.mkdirSync(path.join(target, 'specs'), { recursive: true });
|
|
314
|
+
const specTemplate = path.join(target, 'specs', '_template.md');
|
|
315
|
+
if (!fs.existsSync(specTemplate)) {
|
|
316
|
+
fs.copyFileSync(path.join(src, 'core', 'templates', 'spec.template.md'), specTemplate);
|
|
317
|
+
}
|
|
318
|
+
console.log(`
|
|
319
|
+
✓ pipeline core installed into ${dest} (version ${VERSION})
|
|
320
|
+
|
|
321
|
+
Next:
|
|
322
|
+
1. Open the project in Claude Code.
|
|
323
|
+
2. Run /init-pipeline — it detects your stack, asks the gaps, and generates
|
|
324
|
+
PIPELINE.md + renders one implementer agent per surface.
|
|
325
|
+
3. Commit PIPELINE.md, then /brainstorm to start a feature.
|
|
326
|
+
|
|
327
|
+
Update later with: npx cohorte@latest update
|
|
328
|
+
Prefer one shared core across all your repos? Re-run with --global.`);
|
|
329
|
+
} else {
|
|
330
|
+
console.log(`→ updating pipeline core in ${dest} (keeping your PIPELINE.md + rendered agents)`);
|
|
331
|
+
copyCore();
|
|
332
|
+
try { copyFixedAgents(); } catch { /* best-effort, as in install.sh */ }
|
|
333
|
+
await seedConfig();
|
|
334
|
+
bumpPointerVersion(path.join(dest, 'pipeline.json'));
|
|
335
|
+
console.log(`
|
|
336
|
+
✓ core refreshed to ${VERSION}. Your PIPELINE.md, rendered surface agents, gate-config.json and
|
|
337
|
+
settings.json were left as-is. Re-run /init-pipeline if your stack changed.`);
|
|
338
|
+
}
|
|
339
|
+
})();
|