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
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+ // Minimal block-YAML parser — just enough for the `yaml pipeline-profile` block in PIPELINE.md.
3
+ // Deliberately dependency-free (keeps the published package zero-dep). Handles:
4
+ // key: value · nested maps (indent) · block sequences (- item / - key: val) ·
5
+ // flow arrays [a, b] · flow maps { a: 1 } · quoted scalars · #-comments · bool/int coercion.
6
+ // NOT a general YAML implementation — anchors, multi-line scalars, etc. are out of scope.
7
+
8
+ function stripComment(line) {
9
+ let q = null;
10
+ for (let i = 0; i < line.length; i++) {
11
+ const c = line[i];
12
+ if (q) { if (c === q) q = null; continue; }
13
+ if (c === '"' || c === "'") { q = c; continue; }
14
+ // A '#' starts a comment only at line start or after whitespace (YAML rule).
15
+ if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i);
16
+ }
17
+ return line;
18
+ }
19
+
20
+ function splitTopLevel(s) {
21
+ const out = [];
22
+ let depth = 0, q = null, cur = '';
23
+ for (let i = 0; i < s.length; i++) {
24
+ const c = s[i];
25
+ if (q) { cur += c; if (c === q) q = null; continue; }
26
+ if (c === '"' || c === "'") { q = c; cur += c; continue; }
27
+ if (c === '[' || c === '{') { depth++; cur += c; continue; }
28
+ if (c === ']' || c === '}') { depth--; cur += c; continue; }
29
+ if (c === ',' && depth === 0) { out.push(cur); cur = ''; continue; }
30
+ cur += c;
31
+ }
32
+ if (cur.trim() !== '') out.push(cur);
33
+ return out;
34
+ }
35
+
36
+ function scalar(raw) {
37
+ const v = raw.trim();
38
+ if (v === '') return null;
39
+ if (v[0] === '[') {
40
+ const inner = v.slice(1, v.lastIndexOf(']'));
41
+ return inner.trim() === '' ? [] : splitTopLevel(inner).map(scalar);
42
+ }
43
+ if (v[0] === '{') {
44
+ const inner = v.slice(1, v.lastIndexOf('}'));
45
+ const obj = {};
46
+ if (inner.trim() !== '') {
47
+ for (const pair of splitTopLevel(inner)) {
48
+ const idx = pair.indexOf(':');
49
+ if (idx === -1) continue;
50
+ obj[pair.slice(0, idx).trim()] = scalar(pair.slice(idx + 1));
51
+ }
52
+ }
53
+ return obj;
54
+ }
55
+ if ((v[0] === '"' && v[v.length - 1] === '"') || (v[0] === "'" && v[v.length - 1] === "'")) {
56
+ return v.slice(1, -1);
57
+ }
58
+ if (v === 'true') return true;
59
+ if (v === 'false') return false;
60
+ if (v === 'null' || v === '~') return null;
61
+ if (/^-?\d+$/.test(v)) return parseInt(v, 10);
62
+ return v;
63
+ }
64
+
65
+ function keyValue(text) {
66
+ const idx = text.indexOf(':');
67
+ if (idx === -1) return null;
68
+ // Guard against flow-map values being mistaken for a key split.
69
+ return { key: text.slice(0, idx).trim(), rest: text.slice(idx + 1).trim() };
70
+ }
71
+
72
+ function parse(text) {
73
+ const lines = [];
74
+ for (const raw of text.split('\n')) {
75
+ const stripped = stripComment(raw).replace(/\s+$/, '');
76
+ if (stripped.trim() === '') continue;
77
+ lines.push({ indent: stripped.match(/^ */)[0].length, text: stripped.trim() });
78
+ }
79
+
80
+ let pos = 0;
81
+ function block(indent) {
82
+ const isSeq = lines[pos].text.startsWith('- ');
83
+ const result = isSeq ? [] : {};
84
+ while (pos < lines.length && lines[pos].indent >= indent) {
85
+ if (lines[pos].indent > indent) { pos++; continue; } // defensive: skip stray deeper line
86
+ const line = lines[pos];
87
+
88
+ if (line.text.startsWith('- ')) {
89
+ const remainder = line.text.slice(2).trim();
90
+ const kv = keyValue(remainder);
91
+ // A dash item that is `key: …` (not a flow scalar) opens a nested map.
92
+ if (kv && remainder[0] !== '[' && remainder[0] !== '{' && /^[\w.-]+$/.test(kv.key)) {
93
+ lines[pos] = { indent: indent + 2, text: remainder }; // realign so the item's keys share one indent
94
+ result.push(block(indent + 2));
95
+ } else {
96
+ result.push(scalar(remainder));
97
+ pos++;
98
+ }
99
+ continue;
100
+ }
101
+
102
+ const kv = keyValue(line.text);
103
+ if (!kv) { pos++; continue; }
104
+ if (kv.rest === '') {
105
+ pos++;
106
+ if (pos < lines.length && lines[pos].indent > indent) result[kv.key] = block(lines[pos].indent);
107
+ else result[kv.key] = null;
108
+ } else {
109
+ result[kv.key] = scalar(kv.rest);
110
+ pos++;
111
+ }
112
+ }
113
+ return result;
114
+ }
115
+
116
+ return lines.length ? block(lines[0].indent) : {};
117
+ }
118
+
119
+ // Extract + parse the ```yaml pipeline-profile fenced block from a PIPELINE.md string.
120
+ function parseProfileBlock(md) {
121
+ const m = md.match(/```yaml pipeline-profile\r?\n([\s\S]*?)\r?\n```/);
122
+ if (!m) return null;
123
+ return parse(m[1]);
124
+ }
125
+
126
+ module.exports = { parse, parseProfileBlock };
package/install.ps1 ADDED
@@ -0,0 +1,359 @@
1
+ # install.ps1 — install the portable multi-agent pipeline (Windows).
2
+ # Works on Windows PowerShell 5.1 and PowerShell 7+. Mirrors install.sh.
3
+ #
4
+ # Per-project install (default — bundles the core into <target>\.claude, committable):
5
+ # .\install.ps1 [target_dir]
6
+ # irm <raw-url>/install.ps1 | iex
7
+ #
8
+ # Global install (one core in ~\.claude, shared by every repo on this machine):
9
+ # .\install.ps1 -Global
10
+ # & ([scriptblock]::Create((irm <raw-url>/install.ps1))) -Global
11
+ #
12
+ # Update the generic core in place (keeps any generated PIPELINE.md + rendered agents):
13
+ # .\install.ps1 -Update [target_dir]
14
+ # .\install.ps1 -Update -Global
15
+ #
16
+ # Per-project install copies the core into <target>\.claude; global install copies it once
17
+ # into ~\.claude and registers the gate hook there. Either way you then run `/init-pipeline`
18
+ # in each repo to generate PIPELINE.md + render the surface agents. Update refreshes ONLY the
19
+ # stack-agnostic files; generated profiles, rendered agents, gate-config.json and any project
20
+ # settings.json are left untouched.
21
+
22
+ [CmdletBinding()]
23
+ param(
24
+ [switch]$Global,
25
+ [switch]$Update,
26
+ [Parameter(Position = 0)][string]$Target
27
+ )
28
+
29
+ $ErrorActionPreference = 'Stop'
30
+
31
+ $repoUrl = $env:PIPELINE_REPO
32
+ if (-not $repoUrl) { $repoUrl = 'https://github.com/TheBidouilleAgency/cohorte' }
33
+
34
+ if (-not $Target) { $Target = (Get-Location).Path }
35
+
36
+ # --- helpers -----------------------------------------------------------------
37
+
38
+ # Write JSON as UTF-8 without BOM, un-escaping \uXXXX sequences that ConvertTo-Json
39
+ # (notably on PowerShell 5.1) produces for non-ASCII and for < > & ' — so accented
40
+ # characters survive a round-trip. Escapes for control chars, `"` and `\` are kept,
41
+ # and \u preceded by an odd number of backslashes (a literal \u in a string) is untouched.
42
+ function Write-JsonFile([string]$Path, $Data) {
43
+ $json = ConvertTo-Json -InputObject $Data -Depth 100
44
+ $json = [regex]::Replace($json, '(?<=(?:^|[^\\])(?:\\\\)*)\\u([0-9a-fA-F]{4})', {
45
+ param($m)
46
+ $code = [Convert]::ToInt32($m.Groups[1].Value, 16)
47
+ if ($code -lt 0x20 -or $code -eq 0x22 -or $code -eq 0x5C) { $m.Value }
48
+ else { [string][char]$code }
49
+ })
50
+ $full = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
51
+ [System.IO.File]::WriteAllText($full, $json + "`n", [System.Text.UTF8Encoding]::new($false))
52
+ }
53
+
54
+ function Read-JsonFile([string]$Path) {
55
+ if (-not (Test-Path -LiteralPath $Path)) { return $null }
56
+ try { return Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json }
57
+ catch { return $null }
58
+ }
59
+
60
+ # cp -R src/. dest — deterministic merge/overwrite into an existing directory.
61
+ function Copy-Tree([string]$From, [string]$To) {
62
+ New-Item -ItemType Directory -Force -Path $To | Out-Null
63
+ Copy-Item -Path (Join-Path $From '*') -Destination $To -Recurse -Force
64
+ }
65
+
66
+ # The gate hook runs `python <gate.py>` at tool-use time; find a working Python 3
67
+ # launcher for the hook command (skipping the Microsoft Store alias stubs, which
68
+ # exist on PATH but exit non-zero).
69
+ function Find-Python {
70
+ foreach ($candidate in @('py', 'python', 'python3')) {
71
+ if (-not (Get-Command $candidate -ErrorAction SilentlyContinue)) { continue }
72
+ try {
73
+ $null = & $candidate --version 2>$null
74
+ if ($LASTEXITCODE -eq 0) { return $candidate }
75
+ } catch { }
76
+ }
77
+ return $null
78
+ }
79
+
80
+ # --- locate the source (this checkout, or clone if piped via irm|iex) --------
81
+ $tmp = $null
82
+ try {
83
+ $src = $null
84
+ if ($PSScriptRoot -and (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'core'))) {
85
+ $src = $PSScriptRoot
86
+ } else {
87
+ Write-Host "-> fetching pipeline from $repoUrl"
88
+ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
89
+ throw 'git is required to fetch the pipeline (or run install.ps1 from a checkout).'
90
+ }
91
+ $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("cohorte-" + [Guid]::NewGuid().ToString('N'))
92
+ New-Item -ItemType Directory -Path $tmp | Out-Null
93
+ # PS 5.1 turns redirected native stderr into terminating errors under EAP=Stop;
94
+ # relax it around the clone and rely on the exit code instead.
95
+ $prevEAP = $ErrorActionPreference
96
+ $ErrorActionPreference = 'Continue'
97
+ git clone --depth 1 --quiet $repoUrl (Join-Path $tmp 'pipeline') 2>&1 | Out-Null
98
+ $ErrorActionPreference = $prevEAP
99
+ if ($LASTEXITCODE -ne 0) { throw "git clone of $repoUrl failed" }
100
+ $src = Join-Path $tmp 'pipeline'
101
+ }
102
+ if (-not (Test-Path -LiteralPath (Join-Path $src 'core'))) {
103
+ throw "pipeline source not found (no core/ in $src)"
104
+ }
105
+
106
+ # --- resolve the destination .claude dir ---------------------------------
107
+ if ($Global) {
108
+ $dest = $env:CLAUDE_CONFIG_DIR
109
+ if (-not $dest) { $dest = Join-Path $HOME '.claude' }
110
+ } else {
111
+ $dest = Join-Path $Target '.claude'
112
+ }
113
+ New-Item -ItemType Directory -Force -Path $dest | Out-Null
114
+
115
+ # version stamp so a per-repo pointer can record which core it expects:
116
+ # the package.json semver, with the git sha for traceability on from-main installs
117
+ $semver = ''
118
+ try {
119
+ $pkgPath = Join-Path $src 'package.json'
120
+ if (Test-Path $pkgPath) {
121
+ $pkgData = Get-Content -Raw $pkgPath | ConvertFrom-Json
122
+ if ($pkgData.version) { $semver = "$($pkgData.version)".Trim() }
123
+ }
124
+ } catch { }
125
+ $sha = ''
126
+ try {
127
+ $v = git -C $src rev-parse --short HEAD 2>$null
128
+ if ($LASTEXITCODE -eq 0 -and $v) { $sha = "$v".Trim() }
129
+ } catch { }
130
+ if ($semver -and $sha) { $ver = "$semver ($sha)" }
131
+ elseif ($semver) { $ver = $semver }
132
+ elseif ($sha) { $ver = $sha }
133
+ else { $ver = 'unknown' }
134
+
135
+ function Copy-Core {
136
+ Copy-Tree (Join-Path $src 'core\commands') (Join-Path $dest 'commands')
137
+ Copy-Tree (Join-Path $src 'core\hooks') (Join-Path $dest 'hooks')
138
+ Copy-Tree (Join-Path $src 'core\templates') (Join-Path $dest 'templates')
139
+ # 0.1.19 renamed questionnaire-domain-brief.md -> research-brief.md; drop the stale copy.
140
+ Remove-Item -LiteralPath (Join-Path $dest 'templates\questionnaire-domain-brief.md') -Force -ErrorAction SilentlyContinue
141
+ New-Item -ItemType Directory -Force -Path (Join-Path $dest 'pipeline\scripts') | Out-Null
142
+ Copy-Item (Join-Path $src 'profile\PIPELINE.template.md') (Join-Path $dest 'pipeline') -Force
143
+ Copy-Item (Join-Path $src 'profile\SCHEMA.md') (Join-Path $dest 'pipeline') -Force
144
+ Copy-Item (Join-Path $src 'profile\cohorte.config.template.yaml') (Join-Path $dest 'pipeline') -Force
145
+ Copy-Item (Join-Path $src 'scripts\*.template') (Join-Path $dest 'pipeline\scripts') -Force
146
+ Copy-Item (Join-Path $src 'core\agents\implementer.template.md') (Join-Path $dest 'pipeline') -Force
147
+ if (Test-Path (Join-Path $src 'CHANGELOG.md')) { Copy-Item (Join-Path $src 'CHANGELOG.md') (Join-Path $dest 'pipeline') -Force }
148
+ [System.IO.File]::WriteAllText((Join-Path $dest 'pipeline\VERSION'), "$ver`n", [System.Text.UTF8Encoding]::new($false))
149
+ Clear-TddGate
150
+ }
151
+
152
+ # The TDD gate was removed in 0.1.6. Older installs have hooks\tdd_gate.py on disk and
153
+ # possibly registered in settings.json (by the sh/npm installers) — copy-over never deletes,
154
+ # and a registered hook whose file is gone errors on every Write/Edit, so scrub both.
155
+ function Clear-TddGate {
156
+ Remove-Item -LiteralPath (Join-Path $dest 'hooks\tdd_gate.py') -Force -ErrorAction SilentlyContinue
157
+ $settingsPath = Join-Path $dest 'settings.json'
158
+ $data = Read-JsonFile $settingsPath
159
+ if ($data -is [pscustomobject] -and $data.PSObject.Properties['hooks'] -and
160
+ $data.hooks.PSObject.Properties['PreToolUse']) {
161
+ $kept = @()
162
+ $dropped = $false
163
+ foreach ($entry in @($data.hooks.PreToolUse)) {
164
+ $isTdd = $false
165
+ foreach ($h in @($entry.hooks)) {
166
+ if ($h -and $h.command -and "$($h.command)".Trim().TrimEnd('"').EndsWith('tdd_gate.py')) { $isTdd = $true }
167
+ }
168
+ if ($isTdd) { $dropped = $true } else { $kept += $entry }
169
+ }
170
+ if ($dropped) {
171
+ $data.hooks.PreToolUse = $kept
172
+ Write-JsonFile $settingsPath $data
173
+ Write-Host " - removed the retired tdd_gate.py hook (file + settings registration)"
174
+ }
175
+ }
176
+ }
177
+
178
+ # the fixed (non-rendered) agents: the dev review/release pipeline agents
179
+ function Copy-FixedAgents {
180
+ New-Item -ItemType Directory -Force -Path (Join-Path $dest 'agents') | Out-Null
181
+ Copy-Item (Join-Path $src 'core\agents\review.md'),
182
+ (Join-Path $src 'core\agents\release.md') (Join-Path $dest 'agents') -Force
183
+ # 0.1.19 split the bi-mode questionnaire-researcher into research-agent + questionnaire-architect;
184
+ # copy-over never deletes, so scrub the retired agent lest a dead subagent_type linger.
185
+ Remove-Item -LiteralPath (Join-Path $dest 'agents\questionnaire-researcher.md') -Force -ErrorAction SilentlyContinue
186
+ Clear-ResearchQuestionnaire
187
+ }
188
+
189
+ # The research + questionnaire capability was removed. Older installs have its agents, commands,
190
+ # templates and template-step dirs on disk; copy-over never deletes, so scrub every orphan.
191
+ function Clear-ResearchQuestionnaire {
192
+ foreach ($f in @('agents\research-agent.md', 'agents\questionnaire-architect.md',
193
+ 'agents\questionnaire-writer.md', 'agents\questionnaire-validator.md',
194
+ 'commands\research.md', 'commands\questionnaire.md',
195
+ 'templates\research-brief.md', 'templates\questionnaire-blueprint.md',
196
+ 'templates\questionnaire-declaration.md', 'templates\questionnaire-verdict.md')) {
197
+ Remove-Item -LiteralPath (Join-Path $dest $f) -Force -ErrorAction SilentlyContinue
198
+ }
199
+ foreach ($d in @('templates\steps\research', 'templates\steps\questionnaire')) {
200
+ Remove-Item -LiteralPath (Join-Path $dest $d) -Recurse -Force -ErrorAction SilentlyContinue
201
+ }
202
+ }
203
+
204
+ # pipeline capability config is USER-level (vault, Notion DB, kanban boards) — it lives in
205
+ # the user's .claude regardless of install scope. Seed only if neither the consolidated nor
206
+ # the legacy copy exists. Non-interactive here: seeds disabled defaults; /init-pipeline +
207
+ # /update-pipeline wire it (npx's installer offers a quick interview instead).
208
+ function Initialize-Config {
209
+ $userClaude = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME '.claude' }
210
+ $cfg = Join-Path $userClaude 'cohorte.config.yaml'
211
+ $legacy = @('thebidouille.config.yaml') |
212
+ ForEach-Object { Join-Path $userClaude $_ } |
213
+ Where-Object { Test-Path -LiteralPath $_ } |
214
+ Select-Object -First 1
215
+ if (Test-Path -LiteralPath $cfg) {
216
+ Write-Host " - kept your existing $cfg"
217
+ } elseif ($legacy) {
218
+ Write-Host " - found legacy $legacy — kept as-is (read as a fallback)."
219
+ Write-Host " Run /update-pipeline to migrate it into cohorte.config.yaml + wire the kanban."
220
+ } else {
221
+ New-Item -ItemType Directory -Force -Path $userClaude | Out-Null
222
+ Copy-Item (Join-Path $src 'profile\cohorte.config.template.yaml') $cfg
223
+ Write-Host " - seeded $cfg (disabled defaults — enable via /init-pipeline or /update-pipeline)"
224
+ }
225
+ }
226
+
227
+ # Register the profile-driven gate hook in the GLOBAL settings.json. Idempotent:
228
+ # the hook reads each repo's own .claude/gate-config.json (and no-ops where absent),
229
+ # so one registration serves every project.
230
+ function Register-GlobalHook {
231
+ $settingsPath = Join-Path $dest 'settings.json'
232
+ $gate = Join-Path $dest 'hooks\gate.py'
233
+ $python = Find-Python
234
+ if (-not $python) {
235
+ return 'skipped — no Python 3 found on PATH; install it and re-run .\install.ps1 -Global'
236
+ }
237
+ $cmd = '{0} "{1}"' -f $python, $gate
238
+
239
+ $data = Read-JsonFile $settingsPath
240
+ if ($null -eq $data -or $data -isnot [pscustomobject]) { $data = [pscustomobject]@{} }
241
+ if (-not $data.PSObject.Properties['hooks']) {
242
+ $data | Add-Member -NotePropertyName hooks -NotePropertyValue ([pscustomobject]@{})
243
+ }
244
+ if (-not $data.hooks.PSObject.Properties['PreToolUse']) {
245
+ $data.hooks | Add-Member -NotePropertyName PreToolUse -NotePropertyValue @()
246
+ }
247
+
248
+ $already = $false
249
+ foreach ($entry in @($data.hooks.PreToolUse)) {
250
+ foreach ($h in @($entry.hooks)) {
251
+ if ($h -and $h.command -and "$($h.command)".Trim().TrimEnd('"').EndsWith('gate.py')) {
252
+ $already = $true
253
+ }
254
+ }
255
+ }
256
+ if (-not $already) {
257
+ $data.hooks.PreToolUse = @($data.hooks.PreToolUse) + [pscustomobject]@{
258
+ matcher = 'Bash'
259
+ hooks = @([pscustomobject]@{ type = 'command'; command = $cmd })
260
+ }
261
+ Write-JsonFile $settingsPath $data
262
+ return 'ok'
263
+ }
264
+ return 'present'
265
+ }
266
+
267
+ # Bump only the core_version in a repo's committed .claude/pipeline.json (bundled mode).
268
+ # Leaves every other field intact; no-ops if the pointer is absent or has no core_version.
269
+ function Update-PointerVersion([string]$Ptr, [string]$NewVer) {
270
+ $data = Read-JsonFile $Ptr
271
+ if ($data -is [pscustomobject] -and $data.PSObject.Properties['core_version']) {
272
+ $data.core_version = $NewVer
273
+ Write-JsonFile $Ptr $data
274
+ }
275
+ }
276
+
277
+ if ($Global) {
278
+ if (-not $Update) {
279
+ Write-Host "-> installing pipeline core GLOBALLY into $dest"
280
+ } else {
281
+ Write-Host "-> updating pipeline core GLOBALLY in $dest (keeping global settings.json)"
282
+ }
283
+ Copy-FixedAgents
284
+ Copy-Core
285
+ $hookState = Register-GlobalHook
286
+ Initialize-Config
287
+ Write-Host @"
288
+
289
+ OK pipeline core installed globally into $dest (version $ver)
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 ($repoUrl).
300
+ 3. Commit PIPELINE.md + .claude/, then /brainstorm to start a feature.
301
+
302
+ Code retrieval (Serena — the default provider /init-pipeline wires per repo):
303
+ uv tool install -p 3.13 serena-agent # once per machine
304
+ Make sure the uv tools dir is on PATH (uv tool update-shell) — otherwise the
305
+ registered MCP server silently fails to start.
306
+
307
+ Global kanban config, user-scoped — optional:
308
+ · One consolidated file: ~/.claude/cohorte.config.yaml (don't hand-edit it).
309
+ · /init-pipeline (new project) and /update-pipeline (existing) wire it for you: creating +
310
+ syncing an Obsidian kanban board of the pipeline in your shared vault.
311
+ "@
312
+ return
313
+ }
314
+
315
+ if (-not $Update) {
316
+ Write-Host "-> installing pipeline core into $dest"
317
+ Copy-FixedAgents
318
+ Copy-Core
319
+ Initialize-Config
320
+ New-Item -ItemType Directory -Force -Path (Join-Path $Target 'specs') | Out-Null
321
+ if (-not (Test-Path -LiteralPath (Join-Path $Target 'specs\_template.md'))) {
322
+ Copy-Item (Join-Path $src 'core\templates\spec.template.md') (Join-Path $Target 'specs\_template.md')
323
+ }
324
+ Write-Host @"
325
+
326
+ OK pipeline core installed into $dest (version $ver)
327
+
328
+ Next:
329
+ 1. Open the project in Claude Code.
330
+ 2. Run /init-pipeline — it detects your stack, asks the gaps, and generates
331
+ PIPELINE.md + renders one implementer agent per surface.
332
+ 3. Commit PIPELINE.md, then /brainstorm to start a feature.
333
+
334
+ Code retrieval (Serena — the default provider /init-pipeline wires per repo):
335
+ uv tool install -p 3.13 serena-agent # once per machine
336
+ Make sure the uv tools dir is on PATH (uv tool update-shell) — otherwise the
337
+ registered MCP server silently fails to start.
338
+
339
+ Prefer one shared core across all your repos? Re-run with -Global.
340
+ "@
341
+ } else {
342
+ Write-Host "-> updating pipeline core in $dest (keeping your PIPELINE.md + rendered agents)"
343
+ Copy-Core
344
+ if (Test-Path -LiteralPath (Join-Path $dest 'agents')) {
345
+ Copy-FixedAgents
346
+ }
347
+ Initialize-Config
348
+ Update-PointerVersion (Join-Path $dest 'pipeline.json') $ver
349
+ Write-Host @"
350
+
351
+ OK core refreshed to $ver. Your PIPELINE.md, rendered surface agents, gate-config.json and
352
+ settings.json were left as-is. Re-run /init-pipeline if your stack changed.
353
+ "@
354
+ }
355
+ } finally {
356
+ if ($tmp -and (Test-Path -LiteralPath $tmp)) {
357
+ Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
358
+ }
359
+ }