cohorte 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +264 -0
- package/LICENSE +661 -0
- package/README.md +269 -0
- package/bin/cli.js +339 -0
- package/core/agents/implementer.template.md +74 -0
- package/core/agents/release.md +51 -0
- package/core/agents/review.md +85 -0
- package/core/commands/align-ds.md +32 -0
- package/core/commands/audit.md +31 -0
- package/core/commands/brainstorm.md +48 -0
- package/core/commands/build.md +91 -0
- package/core/commands/doctor.md +50 -0
- package/core/commands/fix.md +62 -0
- package/core/commands/init-pipeline.md +32 -0
- package/core/commands/refactor.md +38 -0
- package/core/commands/review.md +68 -0
- package/core/commands/ship.md +68 -0
- package/core/commands/smoke.md +55 -0
- package/core/commands/spec.md +67 -0
- package/core/commands/update-pipeline.md +96 -0
- package/core/hooks/__pycache__/gate.cpython-312.pyc +0 -0
- package/core/hooks/gate.py +129 -0
- package/core/templates/agent-handoff.md +34 -0
- package/core/templates/brainstorm-return.md +36 -0
- package/core/templates/design-brief.md +35 -0
- package/core/templates/pr-body.md +29 -0
- package/core/templates/review-feedback.md +36 -0
- package/core/templates/spec.template.md +84 -0
- package/core/templates/steps/init-pipeline/01-detect-stack.md +40 -0
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +41 -0
- package/core/templates/steps/init-pipeline/03-draft-profile.md +10 -0
- package/core/templates/steps/init-pipeline/04-write-render.md +88 -0
- package/core/templates/steps/init-pipeline/05-report.md +12 -0
- package/dashboard/README.md +54 -0
- package/dashboard/dist/apple-touch-icon-180.png +0 -0
- package/dashboard/dist/assets/index-CoBuEdy-.js +42 -0
- package/dashboard/dist/assets/index-DN5OGW9g.css +1 -0
- package/dashboard/dist/favicon-16.png +0 -0
- package/dashboard/dist/favicon-32.png +0 -0
- package/dashboard/dist/favicon-48.png +0 -0
- package/dashboard/dist/icon-192.png +0 -0
- package/dashboard/dist/icon-512.png +0 -0
- package/dashboard/dist/index.html +16 -0
- package/dashboard/server/doctor.js +266 -0
- package/dashboard/server/fleet.js +119 -0
- package/dashboard/server/index.js +306 -0
- package/dashboard/server/kanban.js +158 -0
- package/dashboard/server/versions.js +111 -0
- package/dashboard/server/yaml.js +126 -0
- package/install.ps1 +359 -0
- package/install.sh +301 -0
- package/package.json +40 -0
- package/profile/PIPELINE.template.md +208 -0
- package/profile/SCHEMA.md +303 -0
- package/profile/cohorte.config.template.yaml +43 -0
- package/scripts/new-feature.sh.template +89 -0
- package/scripts/remove-feature.sh.template +53 -0
|
@@ -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
|
+
}
|