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.
Files changed (57) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/LICENSE +661 -0
  3. package/README.md +269 -0
  4. package/bin/cli.js +339 -0
  5. package/core/agents/implementer.template.md +74 -0
  6. package/core/agents/release.md +51 -0
  7. package/core/agents/review.md +85 -0
  8. package/core/commands/align-ds.md +32 -0
  9. package/core/commands/audit.md +31 -0
  10. package/core/commands/brainstorm.md +48 -0
  11. package/core/commands/build.md +91 -0
  12. package/core/commands/doctor.md +50 -0
  13. package/core/commands/fix.md +62 -0
  14. package/core/commands/init-pipeline.md +32 -0
  15. package/core/commands/refactor.md +38 -0
  16. package/core/commands/review.md +68 -0
  17. package/core/commands/ship.md +68 -0
  18. package/core/commands/smoke.md +55 -0
  19. package/core/commands/spec.md +67 -0
  20. package/core/commands/update-pipeline.md +96 -0
  21. package/core/hooks/__pycache__/gate.cpython-312.pyc +0 -0
  22. package/core/hooks/gate.py +129 -0
  23. package/core/templates/agent-handoff.md +34 -0
  24. package/core/templates/brainstorm-return.md +36 -0
  25. package/core/templates/design-brief.md +35 -0
  26. package/core/templates/pr-body.md +29 -0
  27. package/core/templates/review-feedback.md +36 -0
  28. package/core/templates/spec.template.md +84 -0
  29. package/core/templates/steps/init-pipeline/01-detect-stack.md +40 -0
  30. package/core/templates/steps/init-pipeline/02-interview-gaps.md +41 -0
  31. package/core/templates/steps/init-pipeline/03-draft-profile.md +10 -0
  32. package/core/templates/steps/init-pipeline/04-write-render.md +88 -0
  33. package/core/templates/steps/init-pipeline/05-report.md +12 -0
  34. package/dashboard/README.md +54 -0
  35. package/dashboard/dist/apple-touch-icon-180.png +0 -0
  36. package/dashboard/dist/assets/index-CoBuEdy-.js +42 -0
  37. package/dashboard/dist/assets/index-DN5OGW9g.css +1 -0
  38. package/dashboard/dist/favicon-16.png +0 -0
  39. package/dashboard/dist/favicon-32.png +0 -0
  40. package/dashboard/dist/favicon-48.png +0 -0
  41. package/dashboard/dist/icon-192.png +0 -0
  42. package/dashboard/dist/icon-512.png +0 -0
  43. package/dashboard/dist/index.html +16 -0
  44. package/dashboard/server/doctor.js +266 -0
  45. package/dashboard/server/fleet.js +119 -0
  46. package/dashboard/server/index.js +306 -0
  47. package/dashboard/server/kanban.js +158 -0
  48. package/dashboard/server/versions.js +111 -0
  49. package/dashboard/server/yaml.js +126 -0
  50. package/install.ps1 +359 -0
  51. package/install.sh +301 -0
  52. package/package.json +40 -0
  53. package/profile/PIPELINE.template.md +208 -0
  54. package/profile/SCHEMA.md +303 -0
  55. package/profile/cohorte.config.template.yaml +43 -0
  56. package/scripts/new-feature.sh.template +89 -0
  57. 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
+ [![npm version](https://img.shields.io/npm/v/cohorte?logo=npm&color=cb3837)](https://www.npmjs.com/package/cohorte)
6
+ [![npm downloads](https://img.shields.io/npm/dm/cohorte?logo=npm)](https://www.npmjs.com/package/cohorte)
7
+ [![Publish to npm](https://github.com/TheBidouilleAgency/cohorte/actions/workflows/publish.yml/badge.svg)](https://github.com/TheBidouilleAgency/cohorte/actions/workflows/publish.yml)
8
+ [![node >=18](https://img.shields.io/node/v/cohorte?logo=node.js&logoColor=white)](https://nodejs.org)
9
+ [![license: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](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
+ })();