cohorte 2.6.0 → 2.8.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 (42) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +11 -9
  3. package/bin/cli.js +15 -2
  4. package/core/agents/review.md +4 -2
  5. package/core/commands/cohorte-build.md +3 -1
  6. package/core/commands/cohorte-doctor.md +27 -4
  7. package/core/commands/cohorte-fix.md +6 -5
  8. package/core/commands/cohorte-review.md +32 -22
  9. package/core/commands/cohorte-ship.md +2 -1
  10. package/core/commands/cohorte-spec.md +1 -1
  11. package/core/hooks/gate.py +10 -4
  12. package/core/runtimes/claude.json +1 -0
  13. package/core/runtimes/codex.json +1 -0
  14. package/core/runtimes/cursor.json +1 -0
  15. package/core/runtimes/gemini.json +1 -0
  16. package/core/runtimes/opencode.json +1 -0
  17. package/core/templates/design-brief.md +12 -3
  18. package/core/workflows/audit.js +20 -5
  19. package/core/workflows/loop.js +617 -0
  20. package/core/workflows/refactor.js +21 -8
  21. package/core/workflows/review.js +81 -12
  22. package/dashboard/dist/assets/{index-D1rsbLat.js → index-DO3_nq2Q.js} +1 -1
  23. package/dashboard/dist/index.html +1 -1
  24. package/dashboard/server/doctor.js +11 -3
  25. package/dashboard/server/index.js +6 -1
  26. package/dashboard/server/kanban.js +15 -4
  27. package/dashboard/server/runtime.js +20 -1
  28. package/install.ps1 +16 -333
  29. package/install.sh +27 -297
  30. package/package.json +1 -1
  31. package/profile/SCHEMA.md +34 -10
  32. package/profile/cohorte.config.template.yaml +1 -1
  33. package/scripts/kanban-move.sh +15 -5
  34. package/scripts/metrics/prices.json +6 -3
  35. package/scripts/new-feature.sh.template +8 -1
  36. package/scripts/preflight.sh +10 -2
  37. package/scripts/remove-feature.sh.template +3 -1
  38. package/scripts/test-dashboard.mjs +42 -1
  39. package/scripts/test-gate.mjs +6 -0
  40. package/scripts/test-metrics.mjs +2 -2
  41. package/scripts/test-workflows.mjs +386 -6
  42. package/scripts/validate-core.mjs +64 -31
@@ -4,6 +4,7 @@
4
4
  // stays local + dependency-free. The kanban mirror is Obsidian-only by design.
5
5
 
6
6
  const fs = require('fs');
7
+ const os = require('os');
7
8
  const path = require('path');
8
9
  const { spawnSync } = require('child_process');
9
10
  const { parse, parseProfileBlock } = require('./yaml');
@@ -37,9 +38,16 @@ function fetchPRs(repo) {
37
38
  }
38
39
 
39
40
  function readConfig(globalDir) {
40
- // cohorte.config.yaml, then the pre-rename legacy names (read-only fallback).
41
- for (const n of ['cohorte.config.yaml', 'thebidouille.config.yaml']) {
42
- try { return parse(fs.readFileSync(path.join(globalDir, n), 'utf8')); } catch { /* try next */ }
41
+ // cohorte.config.yaml under the Claude global dir first, then `~/.cohorte/` — where the
42
+ // installer seeds it for every non-Claude runtime (the shipped scripts probe the same two,
43
+ // in the same order) then the pre-rename legacy name (read-only fallback).
44
+ const candidates = [
45
+ path.join(globalDir, 'cohorte.config.yaml'),
46
+ path.join(os.homedir(), '.cohorte', 'cohorte.config.yaml'),
47
+ path.join(globalDir, 'thebidouille.config.yaml'),
48
+ ];
49
+ for (const p of candidates) {
50
+ try { return parse(fs.readFileSync(p, 'utf8')); } catch { /* try next */ }
43
51
  }
44
52
  return null;
45
53
  }
@@ -78,7 +86,10 @@ function parseBoard(md, repo) {
78
86
  const prs = [...src.matchAll(/#(\d+)\b/g)].map(m => ({
79
87
  num: m[1], url: repo ? `https://github.com/${repo}/pull/${m[1]}` : null,
80
88
  }));
81
- const tags = [...src.matchAll(/#([A-Za-zÀ-ɏ][\wÀ-ɏ/-]*)/g)].map(m => m[1]);
89
+ // A tag is any #-word that is not a bare number (`#123` = PR ref) feature ids
90
+ // may start with a digit (`#2fa-login`), which a letter-first pattern dropped
91
+ // from tags while the text-strip below still removed it: the card lost its join key.
92
+ const tags = [...src.matchAll(/#(?!\d+\b)([\wÀ-ɏ][\wÀ-ɏ/-]*)/g)].map(m => m[1]);
82
93
  const text = src
83
94
  .replace(/#[\wÀ-ɏ/-]+/g, '') // strip tags + PR refs
84
95
  .replace(/\bPR\b/g, '') // and the leftover "PR" label
@@ -64,10 +64,29 @@ function layouts({ projectRoot, globalDir }) {
64
64
  seen.add(key);
65
65
  const rec = (registry && registry[id]) || {};
66
66
  const p = rec.paths || {};
67
+ // runtimes.json records ABSOLUTE install-time paths. For a bundled (committed)
68
+ // core, a clone or a moved checkout still carries the ORIGINAL machine's paths —
69
+ // taken verbatim, every doctor check goes red on a healthy install ("no rendered
70
+ // agent", "gate not registered"). Recover the install-time project root from the
71
+ // recorded core path (its tail must match this core's path relative to the
72
+ // current project root — '.claude', '.cohorte/<rt>') and re-root anything under
73
+ // it onto the current projectRoot. Global cores sit outside the project and are
74
+ // machine-local by definition — their absolute paths pass through untouched.
75
+ const rel = path.relative(projectRoot, dir);
76
+ let recordedRoot = null;
77
+ if (p.core && rel && !rel.startsWith('..') && !path.isAbsolute(rel)
78
+ && (p.core === rel || p.core.endsWith(path.sep + rel))) {
79
+ recordedRoot = p.core.slice(0, p.core.length - rel.length - 1) || null;
80
+ }
67
81
  const abs = (v, fallback) => {
68
82
  const raw = v || fallback;
69
83
  if (!raw) return null;
70
- return path.isAbsolute(raw) ? raw : path.join(projectRoot, raw);
84
+ if (!path.isAbsolute(raw)) return path.join(projectRoot, raw);
85
+ if (recordedRoot && recordedRoot !== projectRoot
86
+ && (raw === recordedRoot || raw.startsWith(recordedRoot + path.sep))) {
87
+ return path.join(projectRoot, raw.slice(recordedRoot.length + 1) || '.');
88
+ }
89
+ return raw;
71
90
  };
72
91
  found.push({
73
92
  id,
package/install.ps1 CHANGED
@@ -33,50 +33,6 @@ if (-not $repoUrl) { $repoUrl = 'https://github.com/TheBidouilleAgency/cohorte'
33
33
 
34
34
  if (-not $Target) { $Target = (Get-Location).Path }
35
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
36
  # --- locate the source (this checkout, or clone if piped via irm|iex) --------
81
37
  $tmp = $null
82
38
  try {
@@ -112,10 +68,23 @@ try {
112
68
  # renderer, so hand the whole job to bin/cli.js, the documented route anyway.
113
69
  $node = Get-Command node -ErrorAction SilentlyContinue
114
70
  if ($node) {
71
+ # An old Node fails DEEP into cli.js (fs.cpSync needs >= 16.7) after some files
72
+ # are already on disk — a half-install that reports as a crash. Refuse up front.
73
+ $nodeMajor = 0
74
+ try { $nodeMajor = [int](& $node.Source -p 'process.versions.node.split(".")[0]') } catch { }
75
+ if ($nodeMajor -lt 18) {
76
+ Write-Error "cohorte needs Node >= 18 — found $(& $node.Source --version). Upgrade Node, then re-run."
77
+ if ($PSCommandPath) { exit 1 }
78
+ return
79
+ }
115
80
  $cliArgs = @((Join-Path $src 'bin\cli.js'), $(if ($Update) { 'update' } else { 'install' }))
116
81
  if ($Global) { $cliArgs += '--global' } else { $cliArgs += $Target }
117
82
  & $node.Source @cliArgs
118
- exit $LASTEXITCODE
83
+ # `exit` under `irm … | iex` terminates the user's interactive PowerShell
84
+ # session (there is no script file to exit from) — closing the console that
85
+ # just ran the documented one-liner. Exit only when running as a file.
86
+ if ($PSCommandPath) { exit $LASTEXITCODE }
87
+ return
119
88
  }
120
89
  Write-Error @"
121
90
  cohorte needs Node >= 18 to install.
@@ -124,295 +93,9 @@ cohorte needs Node >= 18 to install.
124
93
  step, and a raw copy would install prompts this runtime cannot follow.
125
94
  Install Node, then: npm i -g cohorte; cohorte install$(if ($Global) { ' --global' })
126
95
  "@
127
- exit 1
128
-
129
- # --- resolve the destination .claude dir ---------------------------------
130
- if ($Global) {
131
- $dest = $env:CLAUDE_CONFIG_DIR
132
- if (-not $dest) { $dest = Join-Path $HOME '.claude' }
133
- } else {
134
- $dest = Join-Path $Target '.claude'
135
- }
136
- New-Item -ItemType Directory -Force -Path $dest | Out-Null
137
-
138
- # version stamp so a per-repo pointer can record which core it expects:
139
- # the package.json semver, with the git sha for traceability on from-main installs
140
- $semver = ''
141
- try {
142
- $pkgPath = Join-Path $src 'package.json'
143
- if (Test-Path $pkgPath) {
144
- $pkgData = Get-Content -Raw $pkgPath | ConvertFrom-Json
145
- if ($pkgData.version) { $semver = "$($pkgData.version)".Trim() }
146
- }
147
- } catch { }
148
- $sha = ''
149
- try {
150
- $v = git -C $src rev-parse --short HEAD 2>$null
151
- if ($LASTEXITCODE -eq 0 -and $v) { $sha = "$v".Trim() }
152
- } catch { }
153
- if ($semver -and $sha) { $ver = "$semver ($sha)" }
154
- elseif ($semver) { $ver = $semver }
155
- elseif ($sha) { $ver = $sha }
156
- else { $ver = 'unknown' }
96
+ if ($PSCommandPath) { exit 1 }
97
+ return
157
98
 
158
- function Copy-Core {
159
- Copy-Tree (Join-Path $src 'core\commands') (Join-Path $dest 'commands')
160
- Copy-Tree (Join-Path $src 'core\hooks') (Join-Path $dest 'hooks')
161
- Copy-Tree (Join-Path $src 'core\templates') (Join-Path $dest 'templates')
162
- Copy-Tree (Join-Path $src 'core\workflows') (Join-Path $dest 'workflows')
163
- # A Python bytecode cache appears in a source checkout the moment anyone compiles
164
- # or imports gate.py (CI does) and Copy-Item carries it along — machine- and
165
- # interpreter-specific, and copy-over would never delete it later.
166
- Remove-Item -LiteralPath (Join-Path $dest 'hooks\__pycache__') -Recurse -Force -ErrorAction SilentlyContinue
167
- # 0.1.19 renamed questionnaire-domain-brief.md -> research-brief.md; drop the stale copy.
168
- Remove-Item -LiteralPath (Join-Path $dest 'templates\questionnaire-domain-brief.md') -Force -ErrorAction SilentlyContinue
169
- New-Item -ItemType Directory -Force -Path (Join-Path $dest 'pipeline\scripts') | Out-Null
170
- Copy-Item (Join-Path $src 'profile\PIPELINE.template.md') (Join-Path $dest 'pipeline') -Force
171
- Copy-Item (Join-Path $src 'profile\SCHEMA.md') (Join-Path $dest 'pipeline') -Force
172
- Copy-Item (Join-Path $src 'profile\cohorte.config.template.yaml') (Join-Path $dest 'pipeline') -Force
173
- Copy-Item (Join-Path $src 'scripts\*.template') (Join-Path $dest 'pipeline\scripts') -Force
174
- Copy-Item (Join-Path $src 'scripts\kanban-move.sh') (Join-Path $dest 'pipeline\scripts') -Force
175
- Copy-Item (Join-Path $src 'scripts\preflight.sh') (Join-Path $dest 'pipeline\scripts') -Force
176
- # 2.3.0 removed telemetry; copy-over never deletes, so scrub the sender from existing
177
- # installs. The dead `telemetry:` config block is deleted by /cohorte-update-pipeline.
178
- Remove-Item (Join-Path $dest 'pipeline\scripts\telemetry-send.sh') -Force -ErrorAction SilentlyContinue
179
- Copy-Item (Join-Path $src 'core\agents\implementer.template.md') (Join-Path $dest 'pipeline') -Force
180
- if (Test-Path (Join-Path $src 'CHANGELOG.md')) { Copy-Item (Join-Path $src 'CHANGELOG.md') (Join-Path $dest 'pipeline') -Force }
181
- [System.IO.File]::WriteAllText((Join-Path $dest 'pipeline\VERSION'), "$ver`n", [System.Text.UTF8Encoding]::new($false))
182
- Clear-TddGate
183
- }
184
-
185
- # The TDD gate was removed in 0.1.6. Older installs have hooks\tdd_gate.py on disk and
186
- # possibly registered in settings.json (by the sh/npm installers) — copy-over never deletes,
187
- # and a registered hook whose file is gone errors on every Write/Edit, so scrub both.
188
- function Clear-TddGate {
189
- Remove-Item -LiteralPath (Join-Path $dest 'hooks\tdd_gate.py') -Force -ErrorAction SilentlyContinue
190
- $settingsPath = Join-Path $dest 'settings.json'
191
- $data = Read-JsonFile $settingsPath
192
- if ($data -is [pscustomobject] -and $data.PSObject.Properties['hooks'] -and
193
- $data.hooks.PSObject.Properties['PreToolUse']) {
194
- $kept = @()
195
- $dropped = $false
196
- foreach ($entry in @($data.hooks.PreToolUse)) {
197
- $isTdd = $false
198
- foreach ($h in @($entry.hooks)) {
199
- if ($h -and $h.command -and "$($h.command)".Trim().TrimEnd('"').EndsWith('tdd_gate.py')) { $isTdd = $true }
200
- }
201
- if ($isTdd) { $dropped = $true } else { $kept += $entry }
202
- }
203
- if ($dropped) {
204
- $data.hooks.PreToolUse = $kept
205
- Write-JsonFile $settingsPath $data
206
- Write-Host " - removed the retired tdd_gate.py hook (file + settings registration)"
207
- }
208
- }
209
- }
210
-
211
- # the fixed (non-rendered) agents: the dev review/release pipeline agents
212
- function Copy-FixedAgents {
213
- New-Item -ItemType Directory -Force -Path (Join-Path $dest 'agents') | Out-Null
214
- Copy-Item (Join-Path $src 'core\agents\review.md'),
215
- (Join-Path $src 'core\agents\release.md'),
216
- (Join-Path $src 'core\agents\profile-reader.md') (Join-Path $dest 'agents') -Force
217
- # 1.5.0 removed the /smoke phase; copy-over never deletes, so scrub the orphan agent.
218
- Remove-Item -LiteralPath (Join-Path $dest 'agents\smoke.md') -Force -ErrorAction SilentlyContinue
219
- Remove-Item -LiteralPath (Join-Path $dest 'commands\smoke.md') -Force -ErrorAction SilentlyContinue
220
- # 1.4.0 removed /cycle and its workflow — and no installer ever scrubbed them, so every
221
- # install since has kept offering a command that dispatches a workflow whose phases were
222
- # later deleted. A dead command is worse than a missing one: the model can still fire it.
223
- Remove-Item -LiteralPath (Join-Path $dest 'commands\cycle.md') -Force -ErrorAction SilentlyContinue
224
- Remove-Item -LiteralPath (Join-Path $dest 'workflows\cycle.js') -Force -ErrorAction SilentlyContinue
225
- # 1.6.0 renamed /loop → /drive: Claude Code's own built-in /loop shadowed ours, so a leftover
226
- # commands\loop.md is a command the user can never reach — scrub it rather than leave a decoy.
227
- Remove-Item -LiteralPath (Join-Path $dest 'commands\loop.md') -Force -ErrorAction SilentlyContinue
228
- # 2.0.0 prefixed every command with `cohorte-`, which ends the shadowing problem for good.
229
- # Copy-Item never deletes, so all 13 bare names would survive an upgrade as decoys — and a
230
- # stale /build is the worst kind: it still dispatches implementers, from a 1.x command file
231
- # that knows nothing of this core's contract. /drive goes too (it became /cohorte-loop).
232
- foreach ($c in @('align-ds','audit','brainstorm','build','doctor','drive','fix',
233
- 'init-pipeline','refactor','review','ship','spec','update-pipeline')) {
234
- Remove-Item -LiteralPath (Join-Path $dest "commands\$c.md") -Force -ErrorAction SilentlyContinue
235
- }
236
- # 0.1.19 split the bi-mode questionnaire-researcher into research-agent + questionnaire-architect;
237
- # copy-over never deletes, so scrub the retired agent lest a dead subagent_type linger.
238
- Remove-Item -LiteralPath (Join-Path $dest 'agents\questionnaire-researcher.md') -Force -ErrorAction SilentlyContinue
239
- Clear-ResearchQuestionnaire
240
- }
241
-
242
- # The research + questionnaire capability was removed. Older installs have its agents, commands,
243
- # templates and template-step dirs on disk; copy-over never deletes, so scrub every orphan.
244
- function Clear-ResearchQuestionnaire {
245
- foreach ($f in @('agents\research-agent.md', 'agents\questionnaire-architect.md',
246
- 'agents\questionnaire-writer.md', 'agents\questionnaire-validator.md',
247
- 'commands\research.md', 'commands\questionnaire.md',
248
- 'templates\research-brief.md', 'templates\questionnaire-blueprint.md',
249
- 'templates\questionnaire-declaration.md', 'templates\questionnaire-verdict.md')) {
250
- Remove-Item -LiteralPath (Join-Path $dest $f) -Force -ErrorAction SilentlyContinue
251
- }
252
- foreach ($d in @('templates\steps\research', 'templates\steps\questionnaire')) {
253
- Remove-Item -LiteralPath (Join-Path $dest $d) -Recurse -Force -ErrorAction SilentlyContinue
254
- }
255
- }
256
-
257
- # pipeline capability config is USER-level (vault, Notion DB, kanban boards) — it lives in
258
- # the user's .claude regardless of install scope. Seed only if neither the consolidated nor
259
- # the legacy copy exists. Non-interactive here: seeds disabled defaults; /cohorte-init-pipeline +
260
- # /cohorte-update-pipeline wire it (the npm CLI's installer offers a quick interview instead).
261
- function Initialize-Config {
262
- $userClaude = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME '.claude' }
263
- $cfg = Join-Path $userClaude 'cohorte.config.yaml'
264
- $legacy = @('thebidouille.config.yaml') |
265
- ForEach-Object { Join-Path $userClaude $_ } |
266
- Where-Object { Test-Path -LiteralPath $_ } |
267
- Select-Object -First 1
268
- if (Test-Path -LiteralPath $cfg) {
269
- Write-Host " - kept your existing $cfg"
270
- } elseif ($legacy) {
271
- Write-Host " - found legacy $legacy — kept as-is (read as a fallback)."
272
- Write-Host " Run /cohorte-update-pipeline to migrate it into cohorte.config.yaml + wire the kanban."
273
- } else {
274
- New-Item -ItemType Directory -Force -Path $userClaude | Out-Null
275
- Copy-Item (Join-Path $src 'profile\cohorte.config.template.yaml') $cfg
276
- Write-Host " - seeded $cfg (disabled defaults — enable via /cohorte-init-pipeline or /cohorte-update-pipeline)"
277
- }
278
- }
279
-
280
- # Register the profile-driven gate hook in the GLOBAL settings.json. Idempotent:
281
- # the hook reads each repo's own .claude/gate-config.json (and no-ops where absent),
282
- # so one registration serves every project.
283
- function Register-GlobalHook {
284
- $settingsPath = Join-Path $dest 'settings.json'
285
- $gate = Join-Path $dest 'hooks\gate.py'
286
- $python = Find-Python
287
- if (-not $python) {
288
- return 'skipped — no Python 3 found on PATH; install it and re-run .\install.ps1 -Global'
289
- }
290
- $cmd = '{0} "{1}"' -f $python, $gate
291
-
292
- $data = Read-JsonFile $settingsPath
293
- if ($null -eq $data -or $data -isnot [pscustomobject]) { $data = [pscustomobject]@{} }
294
- if (-not $data.PSObject.Properties['hooks']) {
295
- $data | Add-Member -NotePropertyName hooks -NotePropertyValue ([pscustomobject]@{})
296
- }
297
- if (-not $data.hooks.PSObject.Properties['PreToolUse']) {
298
- $data.hooks | Add-Member -NotePropertyName PreToolUse -NotePropertyValue @()
299
- }
300
-
301
- # Reconcile rather than append-if-absent: drop every existing gate.py
302
- # registration, then add exactly one. Idempotent, collapses duplicates older
303
- # installers left behind, and upgrades a stale "Bash"-only matcher in place —
304
- # an append-if-absent would find the stale entry and skip, pinning the bug.
305
- $kept = @()
306
- foreach ($entry in @($data.hooks.PreToolUse)) {
307
- $isGate = $false
308
- foreach ($h in @($entry.hooks)) {
309
- if ($h -and $h.command -and "$($h.command)".Trim().TrimEnd('"').EndsWith('gate.py')) {
310
- $isGate = $true
311
- }
312
- }
313
- if (-not $isGate) { $kept += $entry }
314
- }
315
- # The matcher MUST cover Task as well as Bash: gate.py's preflight phase gate
316
- # keys off tool_name == "Task" (the `preflight` block in a repo's
317
- # gate-config.json). A Bash-only matcher never delivers a Task dispatch to the
318
- # hook, so that gate silently never fires. Keep in lockstep with install.sh
319
- # and bin/cli.js.
320
- $data.hooks.PreToolUse = @($kept) + [pscustomobject]@{
321
- matcher = 'Bash|Task'
322
- hooks = @([pscustomobject]@{ type = 'command'; command = $cmd })
323
- }
324
- Write-JsonFile $settingsPath $data
325
- return 'ok'
326
- }
327
-
328
- # Bump only the core_version in a repo's committed .claude/pipeline.json (bundled mode).
329
- # Leaves every other field intact; no-ops if the pointer is absent or has no core_version.
330
- function Update-PointerVersion([string]$Ptr, [string]$NewVer) {
331
- $data = Read-JsonFile $Ptr
332
- if ($data -is [pscustomobject] -and $data.PSObject.Properties['core_version']) {
333
- $data.core_version = $NewVer
334
- Write-JsonFile $Ptr $data
335
- }
336
- }
337
-
338
- if ($Global) {
339
- if (-not $Update) {
340
- Write-Host "-> installing pipeline core GLOBALLY into $dest"
341
- } else {
342
- Write-Host "-> updating pipeline core GLOBALLY in $dest (keeping global settings.json)"
343
- }
344
- Copy-FixedAgents
345
- Copy-Core
346
- $hookState = Register-GlobalHook
347
- Initialize-Config
348
- Write-Host @"
349
-
350
- OK pipeline core installed globally into $dest (version $ver)
351
- gate hook: $hookState (reads each repo's .claude/gate-config.json; silent where absent)
352
-
353
- The commands (/cohorte-init-pipeline, /cohorte-brainstorm, /cohorte-build ...) and the review/release agents are now
354
- available in EVERY project on this machine — nothing is copied per repo.
355
-
356
- Per repo:
357
- 1. Open the project in Claude Code.
358
- 2. Run /cohorte-init-pipeline — it generates PIPELINE.md, renders the surface agents, writes
359
- .claude/gate-config.json, and drops a committed .claude/pipeline.json pointer so
360
- teammates know to install the global core ($repoUrl).
361
- 3. Commit PIPELINE.md + .claude/, then /cohorte-brainstorm to start a feature.
362
-
363
- Code retrieval (Serena — the default provider /cohorte-init-pipeline wires per repo):
364
- uv tool install -p 3.13 serena-agent # once per machine
365
- Make sure the uv tools dir is on PATH (uv tool update-shell) — otherwise the
366
- registered MCP server silently fails to start.
367
-
368
- Global kanban config, user-scoped — optional:
369
- · One consolidated file: ~/.claude/cohorte.config.yaml (don't hand-edit it).
370
- · /cohorte-init-pipeline (new project) and /cohorte-update-pipeline (existing) wire it for you: creating +
371
- syncing an Obsidian kanban board of the pipeline in your shared vault.
372
- "@
373
- return
374
- }
375
-
376
- if (-not $Update) {
377
- Write-Host "-> installing pipeline core into $dest"
378
- Copy-FixedAgents
379
- Copy-Core
380
- Initialize-Config
381
- New-Item -ItemType Directory -Force -Path (Join-Path $Target 'specs') | Out-Null
382
- if (-not (Test-Path -LiteralPath (Join-Path $Target 'specs\_template.md'))) {
383
- Copy-Item (Join-Path $src 'core\templates\spec.template.md') (Join-Path $Target 'specs\_template.md')
384
- }
385
- Write-Host @"
386
-
387
- OK pipeline core installed into $dest (version $ver)
388
-
389
- Next:
390
- 1. Open the project in Claude Code.
391
- 2. Run /cohorte-init-pipeline — it detects your stack, asks the gaps, and generates
392
- PIPELINE.md + renders one implementer agent per surface.
393
- 3. Commit PIPELINE.md, then /cohorte-brainstorm to start a feature.
394
-
395
- Code retrieval (Serena — the default provider /cohorte-init-pipeline wires per repo):
396
- uv tool install -p 3.13 serena-agent # once per machine
397
- Make sure the uv tools dir is on PATH (uv tool update-shell) — otherwise the
398
- registered MCP server silently fails to start.
399
-
400
- Prefer one shared core across all your repos? Re-run with -Global.
401
- "@
402
- } else {
403
- Write-Host "-> updating pipeline core in $dest (keeping your PIPELINE.md + rendered agents)"
404
- Copy-Core
405
- if (Test-Path -LiteralPath (Join-Path $dest 'agents')) {
406
- Copy-FixedAgents
407
- }
408
- Initialize-Config
409
- Update-PointerVersion (Join-Path $dest 'pipeline.json') $ver
410
- Write-Host @"
411
-
412
- OK core refreshed to $ver. Your PIPELINE.md, rendered surface agents, gate-config.json and
413
- settings.json were left as-is. Re-run /cohorte-init-pipeline if your stack changed.
414
- "@
415
- }
416
99
  } finally {
417
100
  if ($tmp -and (Test-Path -LiteralPath $tmp)) {
418
101
  Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue