cohorte 2.7.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.
- package/CHANGELOG.md +71 -0
- package/README.md +11 -9
- package/bin/cli.js +15 -2
- package/core/agents/review.md +4 -2
- package/core/commands/cohorte-build.md +3 -1
- package/core/commands/cohorte-doctor.md +5 -3
- package/core/commands/cohorte-fix.md +6 -5
- package/core/commands/cohorte-review.md +32 -22
- package/core/commands/cohorte-ship.md +2 -1
- package/core/commands/cohorte-spec.md +1 -1
- package/core/hooks/gate.py +10 -4
- package/core/workflows/audit.js +14 -2
- package/core/workflows/loop.js +617 -0
- package/core/workflows/refactor.js +21 -8
- package/core/workflows/review.js +81 -12
- package/dashboard/dist/assets/{index-D1rsbLat.js → index-DO3_nq2Q.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/server/doctor.js +8 -2
- package/dashboard/server/index.js +6 -1
- package/dashboard/server/kanban.js +15 -4
- package/dashboard/server/runtime.js +20 -1
- package/install.ps1 +16 -333
- package/install.sh +27 -297
- package/package.json +1 -1
- package/profile/SCHEMA.md +33 -10
- package/profile/cohorte.config.template.yaml +1 -1
- package/scripts/kanban-move.sh +15 -5
- package/scripts/new-feature.sh.template +8 -1
- package/scripts/preflight.sh +10 -2
- package/scripts/remove-feature.sh.template +3 -1
- package/scripts/test-dashboard.mjs +42 -1
- package/scripts/test-gate.mjs +6 -0
- package/scripts/test-workflows.mjs +386 -6
- 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
|
|
41
|
-
for (
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|