cohorte 2.10.0 → 3.0.0-dev.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/README.md +7 -0
- package/assets/manifest.json +193 -0
- package/assets/migrations/config/README.md +12 -0
- package/assets/migrations/state/0001_init.sql +154 -0
- package/assets/prompts/README.md +11 -0
- package/assets/prompts/agents/fixer.md +7 -0
- package/assets/prompts/agents/implementer.md +12 -0
- package/assets/prompts/agents/reviewer.md +8 -0
- package/assets/prompts/agents/security-reviewer.md +7 -0
- package/assets/prompts/phases/build.md +5 -0
- package/assets/prompts/phases/review.md +5 -0
- package/assets/prompts/phases/test.md +5 -0
- package/assets/prompts/system.md +11 -0
- package/assets/schemas/agent-output.schema.json +225 -0
- package/assets/schemas/auth-status.schema.json +87 -0
- package/assets/schemas/command-result.schema.json +192 -0
- package/assets/schemas/commands.schema.json +952 -0
- package/assets/schemas/config.schema.json +2259 -0
- package/assets/schemas/doctor-report.schema.json +104 -0
- package/assets/schemas/events.schema.json +8514 -0
- package/assets/schemas/fake-script.schema.json +549 -0
- package/assets/schemas/inspect.schema.json +3133 -0
- package/assets/schemas/manifest.schema.json +79 -0
- package/assets/schemas/ownership.schema.json +52 -0
- package/assets/schemas/policy-verdict.schema.json +282 -0
- package/assets/schemas/project-model.schema.json +783 -0
- package/assets/schemas/project-status.schema.json +217 -0
- package/assets/schemas/reconcile-plan.schema.json +155 -0
- package/assets/schemas/run-diff.schema.json +177 -0
- package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
- package/assets/schemas/run-state.schema.json +1755 -0
- package/assets/schemas/runtime-capabilities.schema.json +1545 -0
- package/assets/schemas/sandbox-capabilities.schema.json +116 -0
- package/assets/schemas/skill.schema.json +87 -0
- package/assets/schemas/spec.schema.json +202 -0
- package/assets/schemas/tool-catalogue.schema.json +655 -0
- package/assets/schemas/trust-record.schema.json +38 -0
- package/assets/skills/README.md +9 -0
- package/assets/skills/inspect-surface.md +4 -0
- package/dist/agent-host.mjs +50 -0
- package/dist/agent-host.mjs.map +1 -0
- package/dist/bundle-manifest.json +102 -0
- package/dist/chunks/__host-DgGPyco9.mjs +18 -0
- package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
- package/dist/chunks/approve-Cv135UqI.mjs +19 -0
- package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
- package/dist/chunks/auth-BT5oVp76.mjs +132 -0
- package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
- package/dist/chunks/auth-D8rCVY33.mjs +84 -0
- package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs.map +1 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
- package/dist/chunks/compose-Cr0hP0qm.mjs +7303 -0
- package/dist/chunks/compose-Cr0hP0qm.mjs.map +1 -0
- package/dist/chunks/config-DefHA2Lw.mjs +96 -0
- package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
- package/dist/chunks/contract-B0ra2-bm.mjs +295 -0
- package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs.map +1 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
- package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
- package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
- package/dist/chunks/discover-30Ciya_0.mjs +20 -0
- package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs.map +1 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
- package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
- package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
- package/dist/chunks/init-Okz4mlhW.mjs +30 -0
- package/dist/chunks/init-Okz4mlhW.mjs.map +1 -0
- package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
- package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
- package/dist/chunks/logs-Giq5sUau.mjs +25 -0
- package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
- package/dist/chunks/migrate-DRoFUg53.mjs +19 -0
- package/dist/chunks/migrate-DRoFUg53.mjs.map +1 -0
- package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
- package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
- package/dist/chunks/pause-BASBfKLO.mjs +16 -0
- package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
- package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
- package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
- package/dist/chunks/providers-Dx78imKj.mjs +19 -0
- package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
- package/dist/chunks/reconcile-BfD785gd.mjs +26 -0
- package/dist/chunks/reconcile-BfD785gd.mjs.map +1 -0
- package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
- package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
- package/dist/chunks/retry-CLrUem1H.mjs +15 -0
- package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
- package/dist/chunks/review-BDdjb6kB.mjs +17 -0
- package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
- package/dist/chunks/run-CgTWYlcd.mjs +82 -0
- package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
- package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
- package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
- package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
- package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
- package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
- package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
- package/dist/chunks/src-CId6kMsY.mjs +1689 -0
- package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
- package/dist/chunks/src-CfAOde7C.mjs +5403 -0
- package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
- package/dist/chunks/src-Cu3TxNob.mjs +783 -0
- package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
- package/dist/chunks/src-DcU4IXMe.mjs +117 -0
- package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
- package/dist/chunks/src-JxawBQBn.mjs +446 -0
- package/dist/chunks/src-JxawBQBn.mjs.map +1 -0
- package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
- package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
- package/dist/chunks/status-SMjiOMYy.mjs +29 -0
- package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
- package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
- package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
- package/dist/chunks/update-BXIppsbi.mjs +16 -0
- package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
- package/dist/cli.mjs +175 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +17 -32
- package/CHANGELOG.md +0 -1453
- package/bin/cli.js +0 -812
- package/bin/report.js +0 -141
- package/core/adapter/render.js +0 -389
- package/core/agents/implementer.template.md +0 -142
- package/core/agents/profile-reader.md +0 -50
- package/core/agents/release.md +0 -58
- package/core/agents/review.md +0 -179
- package/core/commands/cohorte-align-ds.md +0 -33
- package/core/commands/cohorte-audit.md +0 -58
- package/core/commands/cohorte-brainstorm.md +0 -70
- package/core/commands/cohorte-build.md +0 -219
- package/core/commands/cohorte-doctor.md +0 -170
- package/core/commands/cohorte-fix.md +0 -80
- package/core/commands/cohorte-fleet.md +0 -103
- package/core/commands/cohorte-init-pipeline.md +0 -31
- package/core/commands/cohorte-intake.md +0 -92
- package/core/commands/cohorte-patch.md +0 -118
- package/core/commands/cohorte-refactor.md +0 -59
- package/core/commands/cohorte-retro.md +0 -85
- package/core/commands/cohorte-review.md +0 -270
- package/core/commands/cohorte-ship.md +0 -122
- package/core/commands/cohorte-spec.md +0 -101
- package/core/commands/cohorte-update-pipeline.md +0 -134
- package/core/hooks/gate.py +0 -580
- package/core/runtimes/claude.json +0 -74
- package/core/runtimes/codex.json +0 -83
- package/core/runtimes/cursor.json +0 -76
- package/core/runtimes/gemini.json +0 -76
- package/core/runtimes/opencode.json +0 -73
- package/core/templates/agent-handoff.md +0 -34
- package/core/templates/brainstorm-return.md +0 -36
- package/core/templates/decisions.template.md +0 -42
- package/core/templates/design-brief.md +0 -44
- package/core/templates/patch.template.md +0 -86
- package/core/templates/pr-body.md +0 -25
- package/core/templates/review-feedback.md +0 -38
- package/core/templates/spec.template.md +0 -87
- package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
- package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
- package/core/templates/steps/init-pipeline/04-write-render.md +0 -115
- package/core/templates/steps/init-pipeline/05-report.md +0 -12
- package/core/workflows/audit.js +0 -240
- package/core/workflows/loop.js +0 -641
- package/core/workflows/refactor.js +0 -276
- package/core/workflows/review.js +0 -456
- package/install.ps1 +0 -103
- package/install.sh +0 -114
- package/lib/doctor.js +0 -504
- package/lib/runtime.js +0 -134
- package/lib/versions.js +0 -149
- package/lib/yaml.js +0 -129
- package/profile/PIPELINE.template.md +0 -251
- package/profile/SCHEMA.md +0 -698
- package/profile/cohorte.config.template.yaml +0 -43
- package/scripts/assert-gate-hook.mjs +0 -75
- package/scripts/kanban-move.sh +0 -313
- package/scripts/metrics/collect.mjs +0 -531
- package/scripts/metrics/prices.json +0 -42
- package/scripts/new-feature.sh.template +0 -98
- package/scripts/preflight.sh +0 -124
- package/scripts/remove-feature.sh.template +0 -56
- package/scripts/test-adapter.mjs +0 -368
- package/scripts/test-gate.mjs +0 -428
- package/scripts/test-kanban.mjs +0 -304
- package/scripts/test-lib.mjs +0 -310
- package/scripts/test-metrics.mjs +0 -156
- package/scripts/test-workflows.mjs +0 -758
- package/scripts/validate-core.mjs +0 -320
package/bin/cli.js
DELETED
|
@@ -1,812 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// cohorte — installer CLI for the portable multi-agent pipeline.
|
|
3
|
-
// Cross-platform, dependency-free port of install.sh / install.ps1.
|
|
4
|
-
//
|
|
5
|
-
// npm i -g cohorte # once — the CLI, and the `cohorte` binary on PATH
|
|
6
|
-
// cohorte install # bundle the core into <cwd>/.claude (committable)
|
|
7
|
-
// cohorte install [target] # same, into another project
|
|
8
|
-
// cohorte install --global # one shared core in ~/.claude
|
|
9
|
-
// cohorte update [--global] # refresh the core, keep every generated file
|
|
10
|
-
// cohorte version
|
|
11
|
-
//
|
|
12
|
-
// `npx cohorte <verb>` runs any of these without installing anything — it is the
|
|
13
|
-
// escape hatch, not the documented path: a Francois extension panel can only spawn
|
|
14
|
-
// a bare binary on PATH, and a globally installed CLI is what puts one there.
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const os = require('os');
|
|
20
|
-
const path = require('path');
|
|
21
|
-
const { spawnSync } = require('child_process');
|
|
22
|
-
const adapter = require('../core/adapter/render.js');
|
|
23
|
-
|
|
24
|
-
const pkgRoot = path.resolve(__dirname, '..');
|
|
25
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
|
|
26
|
-
const VERSION = pkg.version;
|
|
27
|
-
|
|
28
|
-
const REPO_URL = 'https://github.com/TheBidouilleAgency/cohorte';
|
|
29
|
-
|
|
30
|
-
// PreToolUse matcher for the gate hook. MUST cover Task as well as Bash:
|
|
31
|
-
// gate.py's preflight phase gate keys off tool_name === "Task" (the `preflight`
|
|
32
|
-
// block in a repo's gate-config.json). A Bash-only matcher never delivers a Task
|
|
33
|
-
// dispatch to the hook, so that gate silently never fires — it was dead code
|
|
34
|
-
// from 1.3.0 to 1.3.1. Keep in lockstep with install.sh and install.ps1.
|
|
35
|
-
const GATE_MATCHER = 'Bash|Task';
|
|
36
|
-
|
|
37
|
-
function usage(code) {
|
|
38
|
-
console.log(`cohorte v${VERSION}
|
|
39
|
-
|
|
40
|
-
Usage:
|
|
41
|
-
cohorte install [target] [--global] [--runtime=a,b | --all-runtimes]
|
|
42
|
-
cohorte update [target] [--global] [--runtime=a,b | --all-runtimes]
|
|
43
|
-
cohorte metrics [target] [--days=N] [--since=ISO] [--runs] [--json]
|
|
44
|
-
cohorte specs [target] [--porcelain | --json | --panel]
|
|
45
|
-
cohorte doctor [target] [--porcelain | --json | --panel]
|
|
46
|
-
cohorte version
|
|
47
|
-
|
|
48
|
-
Commands:
|
|
49
|
-
install Fresh install. Default: bundle the core into <target>/.claude
|
|
50
|
-
(committed with the repo). --global: one shared core in ~/.claude,
|
|
51
|
-
available to every project on this machine.
|
|
52
|
-
update Refresh the stack-agnostic core only. PIPELINE.md, rendered surface
|
|
53
|
-
agents, gate-config.json, settings.json and your filled
|
|
54
|
-
~/.claude/cohorte.config.yaml are never touched.
|
|
55
|
-
metrics Cost and runtime per command, reconstructed from Claude Code's own
|
|
56
|
-
transcripts (tokens, USD, wall/active time, subagent count). Reads
|
|
57
|
-
~/.claude/projects — nothing to enable, and it covers runs that
|
|
58
|
-
already happened. Worktree-aware, so a feature adds up. --json for
|
|
59
|
-
the raw rollup, --runs to include every individual invocation.
|
|
60
|
-
specs The spec board of <target>: id, status, branch, title, read from the
|
|
61
|
-
frontmatter of specs/*.md. --porcelain for one record per line with
|
|
62
|
-
U+001F between fields, --json for the list.
|
|
63
|
-
doctor The same checks /cohorte-doctor runs (core, pointer, profile, agents,
|
|
64
|
-
hook, gate, retrieval, design, isolation, specs), without a coding
|
|
65
|
-
agent in the loop. Exits 1 when any check is bad, 0 otherwise, so it
|
|
66
|
-
drops into CI. --porcelain / --json as above.
|
|
67
|
-
version Print the installed CLI version.
|
|
68
|
-
|
|
69
|
-
--panel on specs, doctor and metrics emits the payload a Francois extension
|
|
70
|
-
panel expects (github.com/TheBidouilleAgency/francois-plugin-cohorte).
|
|
71
|
-
|
|
72
|
-
Runtimes (--runtime=): ${adapter.listRuntimes().join(', ')}
|
|
73
|
-
The pipeline's doctrine is one set of source prompts; the installer renders them into
|
|
74
|
-
whatever each coding agent reads (markdown + frontmatter, plain markdown, or TOML) and
|
|
75
|
-
branches the text on what that agent can actually enforce — where there is no blocking
|
|
76
|
-
hook the gate becomes an explicit check the commands call. Real subagents are required.
|
|
77
|
-
Omit the flag and the installer detects what you have and asks; with no TTY it installs
|
|
78
|
-
for Claude Code alone.`);
|
|
79
|
-
process.exit(code);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// --- arg parsing -------------------------------------------------------------
|
|
83
|
-
const args = process.argv.slice(2);
|
|
84
|
-
let mode = null;
|
|
85
|
-
let scope = 'project';
|
|
86
|
-
let target = process.cwd();
|
|
87
|
-
|
|
88
|
-
// Flags that belong to `metrics` and are forwarded verbatim to the collector. They are
|
|
89
|
-
// listed here rather than parsed, so the collector stays the single source of truth for
|
|
90
|
-
// its own options — but the generic "unknown flag" guard below must not reject them.
|
|
91
|
-
const metricsFlags = [];
|
|
92
|
-
const isMetricsFlag = (a) =>
|
|
93
|
-
a === '--json' || a === '--runs' || a.startsWith('--days=') || a.startsWith('--since=');
|
|
94
|
-
|
|
95
|
-
// How `specs`, `doctor` and `metrics` render. One variable, not one flag per command,
|
|
96
|
-
// so `--porcelain` never means two different things depending on where it sits.
|
|
97
|
-
let format = 'human';
|
|
98
|
-
|
|
99
|
-
// Which coding agents to install for. Empty ⇒ resolved later (detect, then ask on a TTY,
|
|
100
|
-
// then fall back to claude — the only behaviour that existed before 2.2.0).
|
|
101
|
-
let wantRuntimes = [];
|
|
102
|
-
|
|
103
|
-
for (const a of args) {
|
|
104
|
-
if (a === 'install' || a === 'update' || a === 'metrics'
|
|
105
|
-
|| a === 'specs' || a === 'doctor') mode = a;
|
|
106
|
-
else if (a === '--porcelain' || a === '--panel') format = a.slice(2);
|
|
107
|
-
else if (isMetricsFlag(a)) { if (a === '--json') format = 'json'; metricsFlags.push(a); }
|
|
108
|
-
else if (a === '--all-runtimes') wantRuntimes = adapter.listRuntimes();
|
|
109
|
-
else if (a === '--runtimes' || a === '--runtime') {
|
|
110
|
-
console.error('error: --runtime needs a value, e.g. --runtime=codex,cursor'); process.exit(2);
|
|
111
|
-
}
|
|
112
|
-
else if (a.startsWith('--runtime=') || a.startsWith('--runtimes=')) {
|
|
113
|
-
for (const id of a.slice(a.indexOf('=') + 1).split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
114
|
-
if (!adapter.listRuntimes().includes(id)) {
|
|
115
|
-
console.error(`error: unknown runtime "${id}" (known: ${adapter.listRuntimes().join(', ')})`);
|
|
116
|
-
process.exit(2);
|
|
117
|
-
}
|
|
118
|
-
if (!wantRuntimes.includes(id)) wantRuntimes.push(id);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
else if (a === 'version' || a === '--version' || a === '-v') { console.log(VERSION); process.exit(0); }
|
|
122
|
-
else if (a === '--global' || a === '-g') scope = 'global';
|
|
123
|
-
else if (a === 'help' || a === '--help' || a === '-h') usage(0);
|
|
124
|
-
else if (a.startsWith('-')) { console.error(`error: unknown flag: ${a}`); usage(2); }
|
|
125
|
-
else target = path.resolve(a);
|
|
126
|
-
}
|
|
127
|
-
if (!mode) usage(args.length ? 2 : 0);
|
|
128
|
-
|
|
129
|
-
// --- specs / doctor: read-only reports on <target> ---------------------------
|
|
130
|
-
// Both read through lib/, so every consumer of the pipeline's state — the CLI, a
|
|
131
|
-
// Francois panel — answers from one implementation. Nothing here writes or spawns.
|
|
132
|
-
if (mode === 'specs' || mode === 'doctor') {
|
|
133
|
-
const report = require('./report.js');
|
|
134
|
-
const { state, scanSpecs } = require('../lib/doctor.js');
|
|
135
|
-
const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
136
|
-
|
|
137
|
-
if (mode === 'specs') {
|
|
138
|
-
const records = report.specRecords(target, scanSpecs);
|
|
139
|
-
if (format === 'json') console.log(JSON.stringify({ project: target, specs: records }, null, 2));
|
|
140
|
-
else if (format === 'porcelain') { if (records.length) console.log(report.specsPorcelain(records)); }
|
|
141
|
-
else if (format === 'panel') console.log(report.specsPanel(records));
|
|
142
|
-
else console.log(report.specsHuman(records));
|
|
143
|
-
process.exit(0);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// `state` is async (it probes versions), so this branch owns the whole tail.
|
|
147
|
-
state({ projectRoot: target, globalDir, cliVersion: VERSION }).then((s) => {
|
|
148
|
-
const records = report.checkRecords(s);
|
|
149
|
-
if (format === 'json') console.log(JSON.stringify(s, null, 2));
|
|
150
|
-
else if (format === 'porcelain') { if (records.length) console.log(report.doctorPorcelain(records)); }
|
|
151
|
-
else if (format === 'panel') console.log(report.doctorPanel(records));
|
|
152
|
-
else console.log(report.doctorHuman(s, records));
|
|
153
|
-
// A bad check is a failure the shell can branch on; warn and skip are not.
|
|
154
|
-
process.exit(s.summary.bad > 0 && format !== 'panel' ? 1 : 0);
|
|
155
|
-
}).catch((e) => {
|
|
156
|
-
console.error(`error: ${e && e.message ? e.message : e}`);
|
|
157
|
-
process.exit(1);
|
|
158
|
-
});
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// --- metrics: cost + runtime per command ------------------------------------
|
|
163
|
-
// The collector is ESM and this CLI is CommonJS, so it runs as a child process rather
|
|
164
|
-
// than being required. stdio is inherited so --json stays pipeable.
|
|
165
|
-
if (mode === 'metrics') {
|
|
166
|
-
// `--panel` is not in isMetricsFlag (it is not metrics-specific), so forward it here.
|
|
167
|
-
if (format === 'panel') metricsFlags.push('--panel');
|
|
168
|
-
const script = path.join(pkgRoot, 'scripts', 'metrics', 'collect.mjs');
|
|
169
|
-
if (!fs.existsSync(script)) {
|
|
170
|
-
console.error(`error: metrics collector not found at ${script}`);
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
const r = spawnSync(process.execPath, [script, target, ...metricsFlags], { stdio: 'inherit' });
|
|
174
|
-
process.exit(r.status == null ? 1 : r.status);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// --- paths -------------------------------------------------------------------
|
|
178
|
-
const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
179
|
-
const src = pkgRoot;
|
|
180
|
-
|
|
181
|
-
if (!fs.existsSync(path.join(src, 'core'))) {
|
|
182
|
-
console.error(`error: pipeline source not found (no core/ in ${src})`);
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Filled per runtime by the install loop. `dest` stays the Claude-shaped root (`.claude` /
|
|
187
|
-
// `~/.claude`) so every helper below — hook registration, the legacy scrubs — keeps working
|
|
188
|
-
// unchanged; non-Claude runtimes put their shared assets under `paths.core` instead.
|
|
189
|
-
// CLAUDE_CONFIG_DIR moves Claude Code's whole tree; the registry declares it as `~/.claude`,
|
|
190
|
-
// so every resolution has to be re-rooted or the install lands half here, half there.
|
|
191
|
-
const PATH_OVERRIDES = { '~/.claude': globalDir };
|
|
192
|
-
|
|
193
|
-
let runtime = adapter.loadRuntime('claude');
|
|
194
|
-
let paths = adapter.resolvePaths(runtime, scope, target, { overrides: PATH_OVERRIDES });
|
|
195
|
-
let dest = scope === 'global' ? globalDir : path.join(target, '.claude');
|
|
196
|
-
|
|
197
|
-
// 2.2.0 retired /cohorte-loop — the autonomous build→review→fix driver. Copy-over never
|
|
198
|
-
// deletes, so on an upgrade its command file and its two scripts would survive as a decoy the
|
|
199
|
-
// model can still fire: a command that spawns headless children against a core that no longer
|
|
200
|
-
// documents them. Scrub the command in every shape this runtime could have installed it, plus
|
|
201
|
-
// the scripts, for every runtime rather than only the Claude layout.
|
|
202
|
-
function scrubLoop() {
|
|
203
|
-
const ext = runtime.command.ext || '.md';
|
|
204
|
-
fs.rmSync(path.join(paths.commands, `cohorte-loop${ext}`), { recursive: true, force: true });
|
|
205
|
-
// Skills are a directory (`<name>/SKILL.md`), so the parent has to go too.
|
|
206
|
-
if (runtime.command.format === 'skill') {
|
|
207
|
-
fs.rmSync(path.join(paths.commands, 'cohorte-loop'), { recursive: true, force: true });
|
|
208
|
-
}
|
|
209
|
-
for (const f of ['loop.sh', 'loop-detach.sh']) {
|
|
210
|
-
fs.rmSync(path.join(paths.core, 'pipeline', 'scripts', f), { force: true });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Resolve every capability conditional in the copied templates, in place. Unlike commands
|
|
215
|
-
// and agents these get no frontmatter and no preamble of their own: a template is always
|
|
216
|
-
// read from within a command that already established the runtime.
|
|
217
|
-
function resolveTemplateConditionals(dir) {
|
|
218
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
219
|
-
const p = path.join(dir, e.name);
|
|
220
|
-
if (e.isDirectory()) { resolveTemplateConditionals(p); continue; }
|
|
221
|
-
if (!p.endsWith('.md')) continue;
|
|
222
|
-
const src = fs.readFileSync(p, 'utf8');
|
|
223
|
-
if (!src.includes('cohorte:if')) continue;
|
|
224
|
-
fs.writeFileSync(p, adapter.applyConditionals(src, runtime));
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- helpers (mirror install.sh) --------------------------------------------
|
|
229
|
-
function copyCore() {
|
|
230
|
-
// The runtime-neutral assets. `commands` is NOT among them any more: it is rendered per
|
|
231
|
-
// runtime by renderSurfaces() into whatever directory that agent actually reads.
|
|
232
|
-
// `workflows` = the deterministic orchestration scripts (review/audit/refactor), copied
|
|
233
|
-
// only where a Workflow engine exists — shipping them elsewhere would advertise a path
|
|
234
|
-
// the rendered commands have already conditionalled out.
|
|
235
|
-
const dirs = ['hooks', 'templates'];
|
|
236
|
-
if (runtime.capabilities.workflows) dirs.push('workflows');
|
|
237
|
-
for (const d of dirs) {
|
|
238
|
-
fs.cpSync(path.join(src, 'core', d), path.join(paths.core, d), {
|
|
239
|
-
recursive: true,
|
|
240
|
-
force: true,
|
|
241
|
-
// Never carry a Python bytecode cache into a user's .claude. It appears in a
|
|
242
|
-
// source checkout the moment anyone compiles or imports gate.py (CI does), it
|
|
243
|
-
// is machine- and interpreter-specific, and copy-over never deletes it later.
|
|
244
|
-
filter: s => !s.split(/[\\/]/).includes('__pycache__') && !s.endsWith('.pyc'),
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
fs.rmSync(path.join(paths.core, 'hooks', '__pycache__'), { recursive: true, force: true });
|
|
248
|
-
// Templates are read at run time BY a command, so they inherit that command's preamble and
|
|
249
|
-
// its `<core>`/`<state>` tokens — but their capability conditionals are still theirs to
|
|
250
|
-
// resolve, and copying them raw would leave `cohorte:if` markers in the model's context.
|
|
251
|
-
resolveTemplateConditionals(path.join(paths.core, 'templates'));
|
|
252
|
-
// 0.1.19 renamed questionnaire-domain-brief.md → research-brief.md; drop the stale copy.
|
|
253
|
-
fs.rmSync(path.join(paths.core, 'templates', 'questionnaire-domain-brief.md'), { force: true });
|
|
254
|
-
const pipelineDir = path.join(paths.core, 'pipeline');
|
|
255
|
-
fs.mkdirSync(path.join(pipelineDir, 'scripts'), { recursive: true });
|
|
256
|
-
for (const f of ['PIPELINE.template.md', 'SCHEMA.md', 'cohorte.config.template.yaml']) {
|
|
257
|
-
fs.copyFileSync(path.join(src, 'profile', f), path.join(pipelineDir, f));
|
|
258
|
-
}
|
|
259
|
-
// SCHEMA.md is the agents' rulebook, read at RUN time from `<core>/pipeline/`, so its
|
|
260
|
-
// capability conditionals have to be resolved here like any other prompt — a `cohorte:if`
|
|
261
|
-
// left in it would reach the model as visible noise, and the wrong branch would tell it to
|
|
262
|
-
// write where this runtime does not look.
|
|
263
|
-
resolveTemplateConditionals(pipelineDir);
|
|
264
|
-
// Copy the *.template files AND the shipped executables (kanban-move.sh,
|
|
265
|
-
// preflight.sh). Until 1.2.4 this loop took only `.template`, so every
|
|
266
|
-
// `cohorte install/update` produced a core missing both scripts — and since
|
|
267
|
-
// every caller chains them with `|| true`, the result was silent: no kanban card
|
|
268
|
-
// moves, no error. The shell installers named them explicitly
|
|
269
|
-
// and this port drifted. The rule below needs no list to keep in sync: a `<x>.sh`
|
|
270
|
-
// with an `<x>.sh.template` sibling is rendered per-project by /cohorte-init-pipeline, so
|
|
271
|
-
// only the template ships; every other `.sh` is a shipped executable.
|
|
272
|
-
const scriptFiles = fs.readdirSync(path.join(src, 'scripts'));
|
|
273
|
-
for (const f of scriptFiles) {
|
|
274
|
-
const isTemplate = f.endsWith('.template');
|
|
275
|
-
const isShipped = f.endsWith('.sh') && !scriptFiles.includes(`${f}.template`);
|
|
276
|
-
if (!isTemplate && !isShipped) continue;
|
|
277
|
-
const target = path.join(pipelineDir, 'scripts', f);
|
|
278
|
-
fs.copyFileSync(path.join(src, 'scripts', f), target);
|
|
279
|
-
if (isShipped && process.platform !== 'win32') {
|
|
280
|
-
try { fs.chmodSync(target, 0o755); } catch { /* optional */ }
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
// 2.3.0 removed telemetry. The copy loop above is by-rule, so it simply stops shipping the
|
|
284
|
-
// sender — but copy-over never deletes, and an existing install would keep an executable
|
|
285
|
-
// that still POSTs to the collector. Scrub it. The dead `telemetry:` block in the user's
|
|
286
|
-
// config is not this installer's to parse: /cohorte-update-pipeline deletes it, where an
|
|
287
|
-
// agent can edit the YAML surgically (SCHEMA.md §Reconcile step 5).
|
|
288
|
-
fs.rmSync(path.join(pipelineDir, 'scripts', 'telemetry-send.sh'), { force: true });
|
|
289
|
-
// The per-surface implementer template is rendered per SURFACE later, by
|
|
290
|
-
// /cohorte-init-pipeline inside the target repo — but its runtime shape (frontmatter keys,
|
|
291
|
-
// capability branches, the preamble) is fixed here, at install time. Run it through the
|
|
292
|
-
// adapter with its <SURFACE_*> placeholders untouched, so init only fills the blanks.
|
|
293
|
-
fs.writeFileSync(path.join(pipelineDir, 'implementer.template.md'), adapter.renderAgent({
|
|
294
|
-
source: fs.readFileSync(path.join(src, 'core', 'agents', 'implementer.template.md'), 'utf8'),
|
|
295
|
-
name: 'implementer.template', runtime, paths, projectRoot: target,
|
|
296
|
-
}).content);
|
|
297
|
-
// What the runtimes ARE, for /cohorte-doctor and anything else that must branch on them at
|
|
298
|
-
// run time. A MAP, merged across installs, not one record: every non-Claude runtime shares
|
|
299
|
-
// the same `.cohorte` core, so a single-record file made each install silently erase the
|
|
300
|
-
// previous one and left /cohorte-doctor diagnosing the wrong agent. Each rendered command
|
|
301
|
-
// already names its own runtime in its preamble; this file supplies the details.
|
|
302
|
-
const registry = path.join(pipelineDir, 'runtimes.json');
|
|
303
|
-
let installed = {};
|
|
304
|
-
try { installed = JSON.parse(fs.readFileSync(registry, 'utf8')) || {}; } catch { /* first install */ }
|
|
305
|
-
installed[runtime.id] = {
|
|
306
|
-
label: runtime.label, scope, core_version: VERSION,
|
|
307
|
-
capabilities: runtime.capabilities,
|
|
308
|
-
excluded_commands: runtime.exclude_commands || [],
|
|
309
|
-
paths: {
|
|
310
|
-
core: paths.core, commands: paths.commands,
|
|
311
|
-
agents: paths.agents || path.join(paths.core, 'agents'),
|
|
312
|
-
// Where the gate registration lives. `cohorte doctor` needs it to tell a registered
|
|
313
|
-
// hook from a missing one without re-deriving each runtime's config layout itself.
|
|
314
|
-
hooks_config: paths.hooks_config,
|
|
315
|
-
state: adapter.stateDir(runtime), config: adapter.configPath(runtime),
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
fs.writeFileSync(registry, JSON.stringify(installed, null, 2) + '\n');
|
|
319
|
-
fs.rmSync(path.join(pipelineDir, 'runtime.json'), { force: true }); // 2.2.0-dev single-record form
|
|
320
|
-
// /cohorte-doctor reads this to tell the human what they're missing; the shell installers
|
|
321
|
-
// have always copied it, this port never did.
|
|
322
|
-
const changelog = path.join(src, 'CHANGELOG.md');
|
|
323
|
-
if (fs.existsSync(changelog)) fs.copyFileSync(changelog, path.join(pipelineDir, 'CHANGELOG.md'));
|
|
324
|
-
fs.writeFileSync(path.join(pipelineDir, 'VERSION'), VERSION + '\n');
|
|
325
|
-
if (process.platform !== 'win32') {
|
|
326
|
-
try { fs.chmodSync(path.join(paths.core, 'hooks', 'gate.py'), 0o755); } catch { /* optional */ }
|
|
327
|
-
}
|
|
328
|
-
scrubTddGate();
|
|
329
|
-
scrubLoop();
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// The TDD gate was removed in 0.1.6. Older installs have hooks/tdd_gate.py on disk and
|
|
333
|
-
// registered in settings.json — copy-over never deletes, and a registered hook whose file
|
|
334
|
-
// is gone errors on every Write/Edit, so scrub both.
|
|
335
|
-
function scrubTddGate() {
|
|
336
|
-
fs.rmSync(path.join(dest, 'hooks', 'tdd_gate.py'), { force: true });
|
|
337
|
-
const settingsPath = path.join(dest, 'settings.json');
|
|
338
|
-
let data;
|
|
339
|
-
try { data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { return; }
|
|
340
|
-
const pre = data && data.hooks && Array.isArray(data.hooks.PreToolUse) ? data.hooks.PreToolUse : null;
|
|
341
|
-
if (!pre) return;
|
|
342
|
-
const kept = pre.filter(entry => !(entry.hooks || []).some(
|
|
343
|
-
h => typeof h.command === 'string' && h.command.trim().endsWith('tdd_gate.py')));
|
|
344
|
-
if (kept.length !== pre.length) {
|
|
345
|
-
data.hooks.PreToolUse = kept;
|
|
346
|
-
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n');
|
|
347
|
-
console.log(' · removed the retired tdd_gate.py hook (file + settings registration)');
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// The fixed (non-rendered-per-surface) agents — review, release, profile-reader — plus every
|
|
352
|
-
// command. Both go through the adapter, so the same source text lands as a native subagent +
|
|
353
|
-
// slash command on Claude/OpenCode and as a persona file + prompt file everywhere else.
|
|
354
|
-
function renderSurfaces() {
|
|
355
|
-
const agentsOut = paths.agents || path.join(paths.core, 'agents');
|
|
356
|
-
fs.mkdirSync(agentsOut, { recursive: true });
|
|
357
|
-
fs.mkdirSync(paths.commands, { recursive: true });
|
|
358
|
-
|
|
359
|
-
// Every agent in core/agents/ EXCEPT the *.template.md ones, which /cohorte-init-pipeline renders
|
|
360
|
-
// per-surface. Until 1.2.6 this was a hardcoded ['review.md', 'release.md'] that never grew
|
|
361
|
-
// the agents the shell installers copy, so `cohorte install` shipped a command with no
|
|
362
|
-
// agent to dispatch — the run reported the command as not installed.
|
|
363
|
-
// Reading the directory needs no list to keep in sync with the shell installers.
|
|
364
|
-
const agentDir = path.join(src, 'core', 'agents');
|
|
365
|
-
for (const f of fs.readdirSync(agentDir)) {
|
|
366
|
-
if (!f.endsWith('.md') || f.endsWith('.template.md')) continue;
|
|
367
|
-
const out = adapter.renderAgent({
|
|
368
|
-
source: fs.readFileSync(path.join(agentDir, f), 'utf8'),
|
|
369
|
-
name: f.slice(0, -3), runtime, paths, projectRoot: target,
|
|
370
|
-
});
|
|
371
|
-
fs.writeFileSync(path.join(agentsOut, out.filename), out.content);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const cmdDir = path.join(src, 'core', 'commands');
|
|
375
|
-
const excluded = runtime.exclude_commands || [];
|
|
376
|
-
for (const f of fs.readdirSync(cmdDir)) {
|
|
377
|
-
if (!f.endsWith('.md')) continue;
|
|
378
|
-
// A command the runtime cannot actually execute is not installed at all. Shipping it
|
|
379
|
-
// would put a working-looking entry in the slash menu that fails on first use — and the
|
|
380
|
-
// human would reasonably read that as the pipeline being broken.
|
|
381
|
-
if (excluded.includes(f.slice(0, -3))) continue;
|
|
382
|
-
const out = adapter.renderCommand({
|
|
383
|
-
source: fs.readFileSync(path.join(cmdDir, f), 'utf8'),
|
|
384
|
-
name: f.slice(0, -3), runtime, paths, projectRoot: target,
|
|
385
|
-
});
|
|
386
|
-
// A Codex skill is `<name>/SKILL.md`, so the filename can carry a directory.
|
|
387
|
-
const dest = path.join(paths.commands, out.filename);
|
|
388
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
389
|
-
fs.writeFileSync(dest, out.content);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// The scrubs below repair Claude-shaped installs that predate the adapter. They are a
|
|
393
|
-
// no-op anywhere else — a fresh non-Claude runtime has no history to clean up — so guard
|
|
394
|
-
// them rather than rm-ing paths that never existed.
|
|
395
|
-
if (runtime.id !== 'claude') return;
|
|
396
|
-
// 0.1.19 split the bi-mode questionnaire-researcher into research-agent + questionnaire-architect;
|
|
397
|
-
// copy-over never deletes, so scrub the retired agent lest a dead subagent_type linger.
|
|
398
|
-
fs.rmSync(path.join(dest, 'agents', 'questionnaire-researcher.md'), { force: true });
|
|
399
|
-
// 1.5.0 removed the /smoke phase; copy-over never deletes, so scrub the orphan agent+command.
|
|
400
|
-
fs.rmSync(path.join(dest, 'agents', 'smoke.md'), { force: true });
|
|
401
|
-
fs.rmSync(path.join(dest, 'commands', 'smoke.md'), { force: true });
|
|
402
|
-
// 1.4.0 removed /cycle and its workflow — and no installer ever scrubbed them, so every
|
|
403
|
-
// install since has kept offering a command that dispatches a workflow whose phases were
|
|
404
|
-
// later deleted. A dead command is worse than a missing one: the model can still fire it.
|
|
405
|
-
fs.rmSync(path.join(dest, 'commands', 'cycle.md'), { force: true });
|
|
406
|
-
fs.rmSync(path.join(dest, 'workflows', 'cycle.js'), { force: true });
|
|
407
|
-
// 1.6.0 renamed /loop → /drive: Claude Code's own built-in /loop shadowed ours, so a leftover
|
|
408
|
-
// commands/loop.md is a command the user can never reach — scrub it rather than leave a decoy.
|
|
409
|
-
fs.rmSync(path.join(dest, 'commands', 'loop.md'), { force: true });
|
|
410
|
-
// 2.0.0 prefixed every command with `cohorte-`, which ends the shadowing problem for good.
|
|
411
|
-
// Copy-over never deletes, so all 13 bare names would survive an upgrade as decoys — and a
|
|
412
|
-
// stale /build is the worst kind: it still dispatches implementers, from a 1.x command file
|
|
413
|
-
// that knows nothing of this core's contract. /drive goes too (it became /cohorte-loop).
|
|
414
|
-
for (const c of ['align-ds', 'audit', 'brainstorm', 'build', 'doctor', 'drive', 'fix',
|
|
415
|
-
'init-pipeline', 'refactor', 'review', 'ship', 'spec', 'update-pipeline']) {
|
|
416
|
-
fs.rmSync(path.join(dest, 'commands', `${c}.md`), { force: true });
|
|
417
|
-
}
|
|
418
|
-
scrubResearchQuestionnaire();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// The research + questionnaire capability was removed. Older installs have its agents, commands,
|
|
422
|
-
// templates and template-step dirs on disk; copy-over never deletes, so scrub every orphan.
|
|
423
|
-
function scrubResearchQuestionnaire() {
|
|
424
|
-
for (const f of ['research-agent.md', 'questionnaire-architect.md',
|
|
425
|
-
'questionnaire-writer.md', 'questionnaire-validator.md']) {
|
|
426
|
-
fs.rmSync(path.join(dest, 'agents', f), { force: true });
|
|
427
|
-
}
|
|
428
|
-
for (const f of ['research.md', 'questionnaire.md']) {
|
|
429
|
-
fs.rmSync(path.join(dest, 'commands', f), { force: true });
|
|
430
|
-
}
|
|
431
|
-
for (const f of ['research-brief.md', 'questionnaire-blueprint.md',
|
|
432
|
-
'questionnaire-declaration.md', 'questionnaire-verdict.md']) {
|
|
433
|
-
fs.rmSync(path.join(dest, 'templates', f), { force: true });
|
|
434
|
-
}
|
|
435
|
-
for (const d of ['research', 'questionnaire']) {
|
|
436
|
-
fs.rmSync(path.join(dest, 'templates', 'steps', d), { recursive: true, force: true });
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// --- interactive config helpers ---------------------------------------------
|
|
441
|
-
// Ask one question on the TTY. Resolves to the trimmed answer (or '' on EOF).
|
|
442
|
-
function ask(question) {
|
|
443
|
-
const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
444
|
-
return new Promise(res => rl.question(question, a => { rl.close(); res((a || '').trim()); }));
|
|
445
|
-
}
|
|
446
|
-
function yes(a) { return /^(y|yes|o|oui)$/i.test(a); }
|
|
447
|
-
|
|
448
|
-
// Set the value on the line carrying `# cfg:<cfgKey>`, preserving the yaml key + the comment.
|
|
449
|
-
// The config template anchors every interactive field this way, so we never parse YAML.
|
|
450
|
-
// Line-scoped on purpose (a multiline regex would let \s span newlines and mangle keys).
|
|
451
|
-
function setCfg(text, cfgKey, value) {
|
|
452
|
-
const marker = `# cfg:${cfgKey}`;
|
|
453
|
-
return text.split('\n').map(line => {
|
|
454
|
-
const idx = line.indexOf(marker);
|
|
455
|
-
if (idx === -1) return line;
|
|
456
|
-
const m = line.slice(0, idx).match(/^(\s*[\w.]+:\s*)/); // " key: "
|
|
457
|
-
return m ? `${m[1]}${value} ${line.slice(idx)}` : line;
|
|
458
|
-
}).join('\n');
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Register gate.py as this runtime's blocking pre-command hook. Four of the five can do it —
|
|
462
|
-
// only OpenCode has no hook contract (plugins are a different thing), and there the rendered
|
|
463
|
-
// commands call `gate.py --check` explicitly instead.
|
|
464
|
-
//
|
|
465
|
-
// Three config shapes, all reconciled rather than appended: drop every existing cohorte gate
|
|
466
|
-
// entry, then add exactly one. Idempotent, collapses duplicates an older installer left, and
|
|
467
|
-
// repairs a stale matcher in place — an append-if-absent would find the stale entry and skip.
|
|
468
|
-
function registerRuntimeHook() {
|
|
469
|
-
const spec = runtime.hook;
|
|
470
|
-
const cfgPath = runtime.scopes[scope].hooks_config;
|
|
471
|
-
if (!spec || !cfgPath) return 'n/a (this runtime has no hook contract — the gate runs as an explicit --check)';
|
|
472
|
-
const python = findPython();
|
|
473
|
-
if (!python) return 'skipped (no python found — register the gate hook manually)';
|
|
474
|
-
|
|
475
|
-
const file = path.join(paths.core, 'hooks', 'gate.py');
|
|
476
|
-
// Quoted on every platform — see registerGlobalHook: an unquoted path with a space breaks
|
|
477
|
-
// every tool call in the session, and the config dirs these runtimes use live under the
|
|
478
|
-
// user's home, which on macOS routinely contains one.
|
|
479
|
-
const cmd = `${python} "${file}" --runtime ${runtime.id}`;
|
|
480
|
-
const isOurs = (s) => typeof s === 'string' && s.includes('gate.py');
|
|
481
|
-
|
|
482
|
-
const dest = adapter.expandHome(cfgPath);
|
|
483
|
-
const abs = path.isAbsolute(dest) ? dest : path.join(target, dest);
|
|
484
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
485
|
-
let data = {};
|
|
486
|
-
try {
|
|
487
|
-
const parsed = JSON.parse(fs.readFileSync(abs, 'utf8'));
|
|
488
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) data = parsed;
|
|
489
|
-
} catch { /* absent or invalid → start fresh */ }
|
|
490
|
-
if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {};
|
|
491
|
-
|
|
492
|
-
if (spec.format === 'cursor') {
|
|
493
|
-
// { "version": 1, "hooks": { "beforeShellExecution": [ { "command": "…" } ] } }
|
|
494
|
-
if (!data.version) data.version = 1;
|
|
495
|
-
const list = Array.isArray(data.hooks[spec.event]) ? data.hooks[spec.event] : [];
|
|
496
|
-
data.hooks[spec.event] = list.filter((e) => !isOurs(e && e.command));
|
|
497
|
-
data.hooks[spec.event].push({ command: cmd });
|
|
498
|
-
} else {
|
|
499
|
-
// Claude/Codex PreToolUse and Gemini BeforeTool share the matcher-group shape.
|
|
500
|
-
const list = Array.isArray(data.hooks[spec.event]) ? data.hooks[spec.event] : [];
|
|
501
|
-
data.hooks[spec.event] = list.filter(
|
|
502
|
-
(e) => !((e && e.hooks) || []).some((h) => isOurs(h && h.command)));
|
|
503
|
-
data.hooks[spec.event].push({
|
|
504
|
-
matcher: spec.matcher, hooks: [{ type: 'command', command: cmd }],
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
fs.writeFileSync(abs, JSON.stringify(data, null, 2) + '\n');
|
|
508
|
-
const where = adapter.displayPath(abs, target);
|
|
509
|
-
return spec.supports_ask
|
|
510
|
-
? `registered in ${where} (${spec.event})`
|
|
511
|
-
: `registered in ${where} (${spec.event}) — no confirmation tier here, so a gated command is denied rather than queried`;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Fill the seeded config from a short TTY interview (shared Obsidian vault for the kanban mirror).
|
|
515
|
-
// Kanban is per-project, so it is wired later by /cohorte-init-pipeline — not asked here.
|
|
516
|
-
async function promptConfig(text) {
|
|
517
|
-
console.log('\n Quick setup (Enter to skip — you can also wire this later via');
|
|
518
|
-
console.log(' /cohorte-init-pipeline or /cohorte-update-pipeline):');
|
|
519
|
-
const vault = await ask(' · absolute path to your shared Obsidian vault (for the kanban mirror): ');
|
|
520
|
-
if (vault) text = setCfg(text, 'vault_path', `"${vault}"`);
|
|
521
|
-
return text;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// The pipeline capability config is USER-level (vault, Notion DB, kanban boards) — it lives in
|
|
525
|
-
// ~/.claude regardless of install scope. Seed it only if the user has no copy (consolidated OR
|
|
526
|
-
// legacy). On a TTY, offer a quick interview to fill it; otherwise seed disabled defaults.
|
|
527
|
-
async function seedConfig() {
|
|
528
|
-
// One config per human, but it has to live somewhere the running agent can find without
|
|
529
|
-
// being told: `~/.claude` for Claude Code (unchanged — existing files stay authoritative),
|
|
530
|
-
// `~/.cohorte` for every other runtime. The shipped scripts probe both, in that order, so
|
|
531
|
-
// a human who drives one repo from two agents still has a single board and a single consent.
|
|
532
|
-
//
|
|
533
|
-
// CLAUDE_CONFIG_DIR moves the whole `~/.claude` tree (globalDir already follows it), but
|
|
534
|
-
// adapter.configPath() speaks in literal `~/.claude/…` — expanding through HOME alone
|
|
535
|
-
// seeded a file at a path no reader probes (kanban-move.sh follows
|
|
536
|
-
// CLAUDE_CONFIG_DIR), missed a filled config there (re-seeding disabled defaults beside
|
|
537
|
-
// it — the exact two-file fork this function's comments forbid), and printed a banner
|
|
538
|
-
// path that did not exist. Re-root the claude-shaped path onto globalDir so the seed,
|
|
539
|
-
// the existing-file check, the banner and every reader agree on one file.
|
|
540
|
-
const homeClaude = path.join(os.homedir(), '.claude');
|
|
541
|
-
const rerootClaude = (p) =>
|
|
542
|
-
p === homeClaude || p.startsWith(homeClaude + path.sep)
|
|
543
|
-
? path.join(globalDir, p.slice(homeClaude.length + 1) || '.')
|
|
544
|
-
: p;
|
|
545
|
-
const cfg = rerootClaude(adapter.expandHome(adapter.configPath(runtime)));
|
|
546
|
-
fs.mkdirSync(path.dirname(cfg), { recursive: true });
|
|
547
|
-
// Pre-rename names, newest first — read as a fallback so upgrades don't lose the config.
|
|
548
|
-
const legacy = ['thebidouille.config.yaml']
|
|
549
|
-
.map((n) => path.join(globalDir, n)).find(fs.existsSync);
|
|
550
|
-
if (fs.existsSync(cfg)) { console.log(` · kept your existing ${cfg}`); return; }
|
|
551
|
-
// A second runtime must not fork the config: two files means two boards and two consent
|
|
552
|
-
// records, and the human edits whichever one they happen to open. The scripts read
|
|
553
|
-
// `~/.cohorte` first, so seeding it here would SHADOW a filled `~/.claude` copy.
|
|
554
|
-
const claudeCfg = rerootClaude(adapter.expandHome(adapter.configPath({ id: 'claude' })));
|
|
555
|
-
if (cfg !== claudeCfg && fs.existsSync(claudeCfg)) {
|
|
556
|
-
console.log(` · reusing your existing ${claudeCfg} (the scripts read it as a fallback)`);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (legacy) {
|
|
560
|
-
console.log(` · found legacy ${legacy} — kept as-is (still read as a fallback).`);
|
|
561
|
-
console.log(' Run /cohorte-update-pipeline to migrate it into cohorte.config.yaml + wire the kanban.');
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
fs.mkdirSync(path.dirname(cfg), { recursive: true });
|
|
565
|
-
let text = fs.readFileSync(path.join(src, 'profile', 'cohorte.config.template.yaml'), 'utf8');
|
|
566
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
567
|
-
text = await promptConfig(text);
|
|
568
|
-
fs.writeFileSync(cfg, text);
|
|
569
|
-
console.log(` · seeded ${cfg} from your answers`);
|
|
570
|
-
} else {
|
|
571
|
-
fs.writeFileSync(cfg, text);
|
|
572
|
-
console.log(` · seeded ${cfg} (disabled defaults — enable via /cohorte-init-pipeline or /cohorte-update-pipeline)`);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function findPython() {
|
|
577
|
-
const candidates = process.platform === 'win32' ? ['py', 'python', 'python3'] : ['python3', 'python'];
|
|
578
|
-
for (const c of candidates) {
|
|
579
|
-
const r = spawnSync(c, ['--version'], { stdio: 'ignore', shell: false });
|
|
580
|
-
if (r.status === 0) return c;
|
|
581
|
-
}
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Register the profile-driven gate hook in the GLOBAL settings.json. Idempotent: the
|
|
586
|
-
// hook reads each repo's own .claude/gate-config.json (and no-ops where absent),
|
|
587
|
-
// so one registration serves every project.
|
|
588
|
-
function registerGlobalHook() {
|
|
589
|
-
const python = findPython();
|
|
590
|
-
if (!python) return 'skipped (no python found — register the gate hook manually)';
|
|
591
|
-
const settingsPath = path.join(dest, 'settings.json');
|
|
592
|
-
let data = {};
|
|
593
|
-
try {
|
|
594
|
-
const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
595
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) data = parsed;
|
|
596
|
-
} catch { /* absent or invalid → start fresh */ }
|
|
597
|
-
if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {};
|
|
598
|
-
if (!Array.isArray(data.hooks.PreToolUse)) data.hooks.PreToolUse = [];
|
|
599
|
-
|
|
600
|
-
const file = path.join(dest, 'hooks', 'gate.py');
|
|
601
|
-
const base = path.basename(file);
|
|
602
|
-
// Trailing-quote tolerant: the Windows form is `py "C:\...\gate.py"`, and a
|
|
603
|
-
// bare .endsWith() missed it — which is how repeat `cohorte install`
|
|
604
|
-
// runs accumulated a duplicate registration every time (gate.py then ran
|
|
605
|
-
// once per copy on every Bash call).
|
|
606
|
-
const isGate = entry => (entry.hooks || []).some(
|
|
607
|
-
h => typeof h.command === 'string' && h.command.trim().replace(/"+$/, '').endsWith(base));
|
|
608
|
-
// ALWAYS quote the path. The hook command is handed to a shell, so a config dir containing a
|
|
609
|
-
// space — `~/Library/Application Support/…`, which is exactly where a desktop host puts it —
|
|
610
|
-
// splits into two arguments and python reports `can't open file '/Users/x/Library/Application'`
|
|
611
|
-
// on EVERY tool call, in a session the human cannot easily un-break. Only the Windows form was
|
|
612
|
-
// quoted, so this failed silently on precisely the platform where the path is most likely to
|
|
613
|
-
// contain a space.
|
|
614
|
-
const cmd = `${python} "${file}"`;
|
|
615
|
-
|
|
616
|
-
// Reconcile rather than append-if-absent: drop every existing gate.py
|
|
617
|
-
// registration, then add exactly one. Idempotent, collapses duplicates older
|
|
618
|
-
// installers left behind, and upgrades a stale "Bash"-only matcher in place —
|
|
619
|
-
// an append-if-absent would find the stale entry and skip, pinning the bug.
|
|
620
|
-
data.hooks.PreToolUse = data.hooks.PreToolUse.filter(e => !isGate(e));
|
|
621
|
-
data.hooks.PreToolUse.push({ matcher: GATE_MATCHER, hooks: [{ type: 'command', command: cmd }] });
|
|
622
|
-
|
|
623
|
-
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n');
|
|
624
|
-
return 'ok';
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Bump only the core_version in a repo's committed .claude/pipeline.json (bundled mode).
|
|
628
|
-
// Leaves every other field intact; no-ops if the pointer is absent or has no core_version.
|
|
629
|
-
function bumpPointerVersion(ptr) {
|
|
630
|
-
if (!fs.existsSync(ptr)) return;
|
|
631
|
-
let data;
|
|
632
|
-
try { data = JSON.parse(fs.readFileSync(ptr, 'utf8')); } catch { return; }
|
|
633
|
-
if (data && typeof data === 'object' && 'core_version' in data) {
|
|
634
|
-
data.core_version = VERSION;
|
|
635
|
-
fs.writeFileSync(ptr, JSON.stringify(data, null, 2) + '\n');
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// --- runtime selection -------------------------------------------------------
|
|
640
|
-
|
|
641
|
-
// Which coding agents this install targets. Explicit flags win. Otherwise: detect what is
|
|
642
|
-
// configured on this machine, and — on a TTY — let the human confirm, because installing
|
|
643
|
-
// into a runtime they don't use litters a config dir they never asked us to touch. With no
|
|
644
|
-
// TTY and no flag we install for Claude Code alone: the behaviour of every version before
|
|
645
|
-
// the adapter, so a scripted `cohorte install` keeps doing exactly what it did.
|
|
646
|
-
async function selectRuntimes() {
|
|
647
|
-
if (wantRuntimes.length) return wantRuntimes;
|
|
648
|
-
const found = adapter.detectRuntimes();
|
|
649
|
-
if (found.length <= 1) return found.length ? found : ['claude'];
|
|
650
|
-
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
651
|
-
console.log(` · several runtimes detected (${found.join(', ')}) — installing for claude only.`);
|
|
652
|
-
console.log(' Pass --runtime=a,b or --all-runtimes to cover the others.');
|
|
653
|
-
return ['claude'];
|
|
654
|
-
}
|
|
655
|
-
console.log('\n Coding agents detected on this machine:');
|
|
656
|
-
found.forEach((id, i) => console.log(` ${i + 1}. ${adapter.loadRuntime(id).label}`));
|
|
657
|
-
const a = await ask(' Install for which? (Enter = all, or e.g. 1,3): ');
|
|
658
|
-
if (!a) return found;
|
|
659
|
-
const picked = a.split(/[,\s]+/).map((n) => found[parseInt(n, 10) - 1]).filter(Boolean);
|
|
660
|
-
return picked.length ? [...new Set(picked)] : found;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// --- staleness notice --------------------------------------------------------
|
|
664
|
-
// `install` and `update` write a core taken from THIS package, so the core is only
|
|
665
|
-
// as fresh as the CLI that ran. `npx cohorte@latest` made that self-correcting; a
|
|
666
|
-
// global install does not — `cohorte update` on a pinned 2.5.0 would re-lay the
|
|
667
|
-
// 2.5.0 core forever and report success, which is an update that updates nothing.
|
|
668
|
-
// So the CLI checks its own version against the registry and says what to run.
|
|
669
|
-
// Never fatal, never blocking a successful install: a short fetch, no `npm view`
|
|
670
|
-
// fallback, and silence on any failure. COHORTE_NO_VERSION_CHECK / CI opt out.
|
|
671
|
-
async function staleVersionNotice() {
|
|
672
|
-
if (process.env.COHORTE_NO_VERSION_CHECK || process.env.CI) return;
|
|
673
|
-
let latest = null;
|
|
674
|
-
try {
|
|
675
|
-
const v = require('../lib/versions.js');
|
|
676
|
-
latest = await v.latestNpm({ timeoutMs: 2500, fallback: false });
|
|
677
|
-
if (!latest || v.cmpSemver(VERSION, latest) >= 0) return;
|
|
678
|
-
} catch { return; }
|
|
679
|
-
// How you got here decides the fix: under npx the package is transient (and may
|
|
680
|
-
// have come from the cache rather than the registry), so the answer is to pin
|
|
681
|
-
// @latest; with a global install the answer is to upgrade the global.
|
|
682
|
-
const viaNpx = /[\\/]_npx[\\/]/.test(pkgRoot);
|
|
683
|
-
console.log(`
|
|
684
|
-
! You ran cohorte ${VERSION}, but ${latest} is published — the core just written is ${VERSION}.
|
|
685
|
-
${viaNpx
|
|
686
|
-
? 'Re-run as npx cohorte@latest <same command> to lay down the current core.'
|
|
687
|
-
: 'Upgrade with npm i -g cohorte@latest and re-run the same command.'}`);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// --- run ---------------------------------------------------------------------
|
|
691
|
-
(async () => {
|
|
692
|
-
const selected = await selectRuntimes();
|
|
693
|
-
for (const id of selected) {
|
|
694
|
-
runtime = adapter.loadRuntime(id);
|
|
695
|
-
paths = adapter.resolvePaths(runtime, scope, target, { overrides: PATH_OVERRIDES });
|
|
696
|
-
dest = runtime.id === 'claude'
|
|
697
|
-
? (scope === 'global' ? globalDir : path.join(target, '.claude'))
|
|
698
|
-
: paths.core;
|
|
699
|
-
fs.mkdirSync(paths.core, { recursive: true });
|
|
700
|
-
if (selected.length > 1) console.log(`\n── ${runtime.label} ──`);
|
|
701
|
-
await installOne();
|
|
702
|
-
}
|
|
703
|
-
if (selected.length > 1) {
|
|
704
|
-
console.log(`\n✓ installed for ${selected.length} runtimes: ${selected.join(', ')}.`);
|
|
705
|
-
console.log(' The doctrine is identical; what differs is enforcement — run /cohorte-doctor in');
|
|
706
|
-
console.log(' each one to see what it can and cannot guarantee.');
|
|
707
|
-
}
|
|
708
|
-
// Last, so a stale CLI is the final thing on screen rather than scrolled past.
|
|
709
|
-
await staleVersionNotice();
|
|
710
|
-
})();
|
|
711
|
-
|
|
712
|
-
async function installOne() {
|
|
713
|
-
if (runtime.id !== 'claude') {
|
|
714
|
-
// Everything below the Claude branch assumes Claude's own layout (settings.json hook
|
|
715
|
-
// registration, the `.claude` pointer). A non-Claude runtime gets the shared core, the
|
|
716
|
-
// rendered commands/personas, and nothing that would only pretend to be wired.
|
|
717
|
-
copyCore();
|
|
718
|
-
renderSurfaces();
|
|
719
|
-
const hookState = registerRuntimeHook();
|
|
720
|
-
await seedConfig();
|
|
721
|
-
if (mode === 'install' && scope === 'project') {
|
|
722
|
-
fs.mkdirSync(path.join(target, 'specs'), { recursive: true });
|
|
723
|
-
const specTemplate = path.join(target, 'specs', '_template.md');
|
|
724
|
-
if (!fs.existsSync(specTemplate)) {
|
|
725
|
-
fs.copyFileSync(path.join(src, 'core', 'templates', 'spec.template.md'), specTemplate);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
const cmds = adapter.displayPath(paths.commands, target);
|
|
729
|
-
const agentsWhere = paths.agents
|
|
730
|
-
? `${adapter.displayPath(paths.agents, target)} (${runtime.agent.format})`
|
|
731
|
-
: 'sequential personas — this runtime has no subagents';
|
|
732
|
-
console.log(`
|
|
733
|
-
✓ ${runtime.label}: core in ${adapter.displayPath(paths.core, target)}, commands in ${cmds} (version ${VERSION})
|
|
734
|
-
invoke: ${runtime.command.invoke.replace('<name>', 'cohorte-init-pipeline')}
|
|
735
|
-
agents: ${agentsWhere}
|
|
736
|
-
gate: ${hookState}`);
|
|
737
|
-
if ((runtime.exclude_commands || []).length) {
|
|
738
|
-
const named = runtime.exclude_commands.map((c) => runtime.command.invoke.replace('<name>', c));
|
|
739
|
-
console.log(` not installed here: ${named.join(', ')}${runtime.exclude_reason ? ` — ${runtime.exclude_reason}` : ''}`);
|
|
740
|
-
}
|
|
741
|
-
if (runtime.scopes[scope].commands_scope === 'global' && scope === 'project') {
|
|
742
|
-
console.log(` note: ${runtime.label} reads commands only from your home dir, so they were`);
|
|
743
|
-
console.log(' installed there even though the core is bundled in this repo.');
|
|
744
|
-
}
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
if (scope === 'global') {
|
|
748
|
-
console.log(mode === 'install'
|
|
749
|
-
? `→ installing pipeline core GLOBALLY into ${dest}`
|
|
750
|
-
: `→ updating pipeline core GLOBALLY in ${dest} (keeping global settings.json)`);
|
|
751
|
-
renderSurfaces();
|
|
752
|
-
copyCore();
|
|
753
|
-
// Register on UPDATE too — install.sh and install.ps1 always have, and this
|
|
754
|
-
// port skipping it is why a duplicated or stale-matcher registration could
|
|
755
|
-
// never be repaired by `cohorte update`: the only route that rewrites it
|
|
756
|
-
// was a full re-install, which is not what anyone runs to get a fix. Safe to
|
|
757
|
-
// run every time — registration reconciles only gate.py entries and leaves
|
|
758
|
-
// every other hook and settings key untouched.
|
|
759
|
-
const hookState = registerGlobalHook();
|
|
760
|
-
await seedConfig();
|
|
761
|
-
console.log(`
|
|
762
|
-
✓ pipeline core installed globally into ${dest} (version ${VERSION})
|
|
763
|
-
gate hook: ${hookState} (reads each repo's .claude/gate-config.json; silent where absent)
|
|
764
|
-
|
|
765
|
-
The commands (/cohorte-init-pipeline, /cohorte-brainstorm, /cohorte-build …) and the review/release agents are now
|
|
766
|
-
available in EVERY project on this machine — nothing is copied per repo.
|
|
767
|
-
|
|
768
|
-
Per repo:
|
|
769
|
-
1. Open the project in Claude Code.
|
|
770
|
-
2. Run /cohorte-init-pipeline — it generates PIPELINE.md, renders the surface agents, writes
|
|
771
|
-
.claude/gate-config.json, and drops a committed .claude/pipeline.json pointer so
|
|
772
|
-
teammates know to install the global core (${REPO_URL}).
|
|
773
|
-
3. Commit PIPELINE.md + .claude/, then /cohorte-brainstorm to start a feature.
|
|
774
|
-
|
|
775
|
-
Update later with: npm i -g cohorte@latest && cohorte update --global
|
|
776
|
-
|
|
777
|
-
Global kanban config, user-scoped — optional:
|
|
778
|
-
· One consolidated file: ${path.join(globalDir, 'cohorte.config.yaml')}
|
|
779
|
-
· Don't hand-edit it — /cohorte-init-pipeline (new project) and /cohorte-update-pipeline (existing) wire it
|
|
780
|
-
for you: creating + syncing an Obsidian kanban board of the pipeline in your shared vault.`);
|
|
781
|
-
} else if (mode === 'install') {
|
|
782
|
-
console.log(`→ installing pipeline core into ${dest}`);
|
|
783
|
-
renderSurfaces();
|
|
784
|
-
copyCore();
|
|
785
|
-
await seedConfig();
|
|
786
|
-
fs.mkdirSync(path.join(target, 'specs'), { recursive: true });
|
|
787
|
-
const specTemplate = path.join(target, 'specs', '_template.md');
|
|
788
|
-
if (!fs.existsSync(specTemplate)) {
|
|
789
|
-
fs.copyFileSync(path.join(src, 'core', 'templates', 'spec.template.md'), specTemplate);
|
|
790
|
-
}
|
|
791
|
-
console.log(`
|
|
792
|
-
✓ pipeline core installed into ${dest} (version ${VERSION})
|
|
793
|
-
|
|
794
|
-
Next:
|
|
795
|
-
1. Open the project in Claude Code.
|
|
796
|
-
2. Run /cohorte-init-pipeline — it detects your stack, asks the gaps, and generates
|
|
797
|
-
PIPELINE.md + renders one implementer agent per surface.
|
|
798
|
-
3. Commit PIPELINE.md, then /cohorte-brainstorm to start a feature.
|
|
799
|
-
|
|
800
|
-
Update later with: npm i -g cohorte@latest && cohorte update
|
|
801
|
-
Prefer one shared core across all your repos? Re-run with --global.`);
|
|
802
|
-
} else {
|
|
803
|
-
console.log(`→ updating pipeline core in ${dest} (keeping your PIPELINE.md + rendered agents)`);
|
|
804
|
-
copyCore();
|
|
805
|
-
try { renderSurfaces(); } catch { /* best-effort, as in install.sh */ }
|
|
806
|
-
await seedConfig();
|
|
807
|
-
bumpPointerVersion(path.join(dest, 'pipeline.json'));
|
|
808
|
-
console.log(`
|
|
809
|
-
✓ core refreshed to ${VERSION}. Your PIPELINE.md, rendered surface agents, gate-config.json and
|
|
810
|
-
settings.json were left as-is. Re-run /cohorte-init-pipeline if your stack changed.`);
|
|
811
|
-
}
|
|
812
|
-
}
|