cohorte 2.4.0 → 2.6.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 +66 -0
- package/README.md +63 -16
- package/bin/cli.js +101 -14
- package/bin/report.js +141 -0
- package/core/commands/cohorte-update-pipeline.md +7 -3
- package/core/templates/steps/init-pipeline/04-write-render.md +2 -2
- package/dashboard/README.md +2 -2
- package/dashboard/server/doctor.js +8 -8
- package/dashboard/server/versions.js +10 -5
- package/install.ps1 +2 -2
- package/install.sh +5 -4
- package/package.json +1 -1
- package/scripts/metrics/collect.mjs +16 -0
- package/scripts/validate-core.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,72 @@ short, user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` secti
|
|
|
7
7
|
> They are history and are deliberately not rewritten — every command gained a `cohorte-` prefix
|
|
8
8
|
> in 2.0.0.
|
|
9
9
|
|
|
10
|
+
## 2.6.0 — 2026-08-14
|
|
11
|
+
|
|
12
|
+
- **Two ways to type the same command, and one of them silently doesn't work.** The docs wrote
|
|
13
|
+
`npx cohorte install`, the Francois extension's manifest names a bare `cohorte` binary, and
|
|
14
|
+
nothing said why the two could not be the same string. They cannot: `npx` fetches a package
|
|
15
|
+
into a cache and runs it once, leaving nothing on `PATH`, and a Francois panel may only spawn
|
|
16
|
+
a **bare binary name resolved on `PATH`**. So the extension's requirement read like a
|
|
17
|
+
preference the rest of the documentation contradicted.
|
|
18
|
+
|
|
19
|
+
Every command is now written one way — `cohorte <verb>`, after `npm i -g cohorte` — across the
|
|
20
|
+
README, the docs, both shell installers, the CLI's own banners, the `/cohorte-doctor` fix
|
|
21
|
+
lines, and the `install` string `/cohorte-init-pipeline` writes into a repo's committed
|
|
22
|
+
`pipeline.json`. `npx cohorte@latest <verb>` still works and is still the right call for a
|
|
23
|
+
one-off on a machine you don't own; it is named once, as the escape hatch it is.
|
|
24
|
+
|
|
25
|
+
- **The cost of pinning, removed before it could bite.** `install` and `update` lay down the core
|
|
26
|
+
carried by the CLI that runs them. `npx cohorte@latest` made that self-correcting; a global
|
|
27
|
+
install does not, so `cohorte update` on a CLI left at an old version would re-lay an old core
|
|
28
|
+
and report success — an update that updates nothing.
|
|
29
|
+
|
|
30
|
+
Both verbs now compare themselves against the registry when they finish and print exactly what
|
|
31
|
+
to run (`npm i -g cohorte@latest`) when they are behind. It is never fatal, never blocks a
|
|
32
|
+
successful install, costs a 2.5s fetch with no `npm view` fallback, and stays silent on any
|
|
33
|
+
failure — an offline install pays 2.5 seconds and says nothing. `COHORTE_NO_VERSION_CHECK=1`
|
|
34
|
+
opts out; `CI` opts out already.
|
|
35
|
+
|
|
36
|
+
Nothing in the pipeline changed: no new agent, no gate, no template — this release needs no
|
|
37
|
+
`/cohorte-update-pipeline`.
|
|
38
|
+
|
|
39
|
+
## 2.5.0 — 2026-08-14
|
|
40
|
+
|
|
41
|
+
- **Everything the pipeline knew about your repo, it would only say to a coding agent.**
|
|
42
|
+
`/cohorte-doctor` reads the health of an install and `specs/*.md` holds the board, but both
|
|
43
|
+
answers cost a session: you opened Claude Code, ran a command, and read prose. There was no
|
|
44
|
+
way to ask "is this repo's pipeline sound?" from a shell script, a CI job, or anything that
|
|
45
|
+
isn't an LLM.
|
|
46
|
+
|
|
47
|
+
Two read-only commands now answer without an agent in the loop:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
cohorte specs # the board: id · status · branch · title, from specs/*.md frontmatter
|
|
51
|
+
cohorte doctor # the /cohorte-doctor checks — exits 1 when any check is bad
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`doctor` **exits 1 on any bad check**, so it drops into CI as-is. Both reuse the dashboard's
|
|
55
|
+
own readers, so the board, the dashboard and the CLI can never give three answers about one
|
|
56
|
+
repo. `--porcelain` gives one record per line with `U+001F` between fields — a spec title
|
|
57
|
+
with a space in it never misaligns a column — and `--json` gives the native document.
|
|
58
|
+
|
|
59
|
+
- **The pipeline, in Francois.** A third shape, `--panel` (on `specs`, `doctor` and `metrics`),
|
|
60
|
+
emits the payload a [Francois](https://github.com/antoine-gmnz/francois) extension panel
|
|
61
|
+
validates against. It exists for
|
|
62
|
+
[**francois-plugin-cohorte**](https://github.com/TheBidouilleAgency/francois-plugin-cohorte):
|
|
63
|
+
a manifest-only extension — no code, no binary — that renders the 30-day cost, the doctor
|
|
64
|
+
report and the spec board as three panels beside your sessions, on any project with a
|
|
65
|
+
`PIPELINE.md`.
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm i -g cohorte # a Francois extension may only spawn a
|
|
69
|
+
francois ext install TheBidouilleAgency/cohorte # bare binary on PATH — never npx, never a shell
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This is the only Francois-aware surface in the package, and it is one flag wide. Nothing in
|
|
73
|
+
the pipeline itself changed: no new agent, no new gate, no template to re-render — this
|
|
74
|
+
release needs no `/cohorte-update-pipeline`.
|
|
75
|
+
|
|
10
76
|
## 2.4.0 — 2026-08-13
|
|
11
77
|
|
|
12
78
|
- **The pipeline froze *what* to build, never *how much*.** A spec pins the contract and the
|
package/README.md
CHANGED
|
@@ -46,9 +46,9 @@ The doctrine is one set of source prompts. The installer renders them into whate
|
|
|
46
46
|
actually reads, and branches the instructions on what it can actually do:
|
|
47
47
|
|
|
48
48
|
```sh
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
cohorte install --runtime=codex,cursor # pick explicitly
|
|
50
|
+
cohorte install --all-runtimes # every supported one
|
|
51
|
+
cohorte install # detects what you have and asks
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
| | Commands | Subagents | Gate | Workflows |
|
|
@@ -82,10 +82,10 @@ install is unchanged.
|
|
|
82
82
|
|
|
83
83
|
Only one hard requirement — the rest is optional and independent:
|
|
84
84
|
|
|
85
|
-
- **Node ≥ 18 + npm** — _required_, for the `
|
|
85
|
+
- **Node ≥ 18 + npm** — _required_, for the `cohorte` CLI that lays down the core. Nothing else needs it.
|
|
86
86
|
- **[`uv`](https://docs.astral.sh/uv/) + the Serena CLI** — _optional_, the default code-retrieval
|
|
87
87
|
provider. Install it separately (`uv tool install -p 3.13 serena-agent && uv tool update-shell`); the
|
|
88
|
-
|
|
88
|
+
cohorte install neither needs nor touches it, so the order between the two is irrelevant. Without Serena
|
|
89
89
|
the pipeline still runs — agents just fall back to Grep/Read. Having it installed **before**
|
|
90
90
|
`/cohorte-init-pipeline` lets init wire it in one pass (otherwise `/cohorte-update-pipeline` wires it later).
|
|
91
91
|
- **On a new machine cloning a repo that's already pipeline-ised:** the Serena registration is committed
|
|
@@ -94,15 +94,25 @@ Only one hard requirement — the rest is optional and independent:
|
|
|
94
94
|
|
|
95
95
|
## Install
|
|
96
96
|
|
|
97
|
-
The pipeline ships as an npm package (`cohorte`), so releases are semver-tagged
|
|
98
|
-
|
|
97
|
+
The pipeline ships as an npm package (`cohorte`), so releases are semver-tagged — no clone
|
|
98
|
+
needed, works on macOS/Linux/Windows. **Install it once, globally**; every command below is
|
|
99
|
+
written as `cohorte …`, and that single form is what the rest of this README, the docs, and the
|
|
100
|
+
[Francois extension](https://github.com/TheBidouilleAgency/francois-plugin-cohorte) all assume:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npm i -g cohorte
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`npx cohorte <verb>` runs any of them without installing anything, for a one-off on a machine
|
|
107
|
+
you don't own. It is the escape hatch, not the path — see
|
|
108
|
+
[why one form](https://thebidouilleagency.github.io/cohorte/reference/installers).
|
|
99
109
|
|
|
100
110
|
**Global (recommended)** — install the generic core ONCE into `~/.claude`; it serves every repo on
|
|
101
111
|
your machine. Nothing is copied per project; the gate hook is registered once and reads each repo's
|
|
102
112
|
own `gate-config.json`:
|
|
103
113
|
|
|
104
114
|
```sh
|
|
105
|
-
|
|
115
|
+
cohorte install --global
|
|
106
116
|
```
|
|
107
117
|
|
|
108
118
|
The per-project part is NOT the core — it's the **profile** `/cohorte-init-pipeline` generates and you
|
|
@@ -116,7 +126,7 @@ repo; each teammate just runs the same global one-liner once, guided by the comm
|
|
|
116
126
|
|
|
117
127
|
```sh
|
|
118
128
|
# inside your project (or pass its path as an argument)
|
|
119
|
-
|
|
129
|
+
cohorte install
|
|
120
130
|
```
|
|
121
131
|
|
|
122
132
|
Copies the core into `<project>/.claude`, committed with the repo. Choose this when you want
|
|
@@ -178,8 +188,9 @@ Sanity-check `PIPELINE.md`, commit it, and run `/cohorte-brainstorm`.
|
|
|
178
188
|
## Update
|
|
179
189
|
|
|
180
190
|
```sh
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
npm i -g cohorte@latest # refresh the CLI — the core it lays down is its own
|
|
192
|
+
cohorte update --global # the shared core in ~/.claude (recommended setup)
|
|
193
|
+
cohorte update # a repo's bundled core in <project>/.claude
|
|
183
194
|
```
|
|
184
195
|
|
|
185
196
|
(Script equivalents: `sh install.sh --update [--global]` / `.\install.ps1 -Update [-Global]`.)
|
|
@@ -200,9 +211,9 @@ maintenance command you ever run (`/cohorte-build` auto-grows surfaces as specs
|
|
|
200
211
|
A browser view of pipeline state, for when a checklist beats scanning files:
|
|
201
212
|
|
|
202
213
|
```sh
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
214
|
+
cohorte dashboard # serves http://localhost:4317 (Ctrl-C to stop)
|
|
215
|
+
cohorte dashboard <path> # start focused on another project
|
|
216
|
+
cohorte dashboard --port=4400 --open # custom port, open the browser
|
|
206
217
|
```
|
|
207
218
|
|
|
208
219
|
**Bound to `127.0.0.1` by default** — the dashboard's actions execute code (install/update/reset,
|
|
@@ -234,6 +245,40 @@ source lives in `dashboard/app/`, built to `dashboard/dist/` at publish time). T
|
|
|
234
245
|
are reimplemented in JS, so the dashboard needs no Claude session to compute state. See
|
|
235
246
|
[`dashboard/README.md`](dashboard/README.md) for the architecture.
|
|
236
247
|
|
|
248
|
+
## Reading a project without an agent
|
|
249
|
+
|
|
250
|
+
The two read-only halves of the pipeline, in the shell:
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
cohorte specs # the board: id · status · branch · title, from specs/*.md
|
|
254
|
+
cohorte doctor # the /cohorte-doctor checks — exits 1 when any check is bad
|
|
255
|
+
cohorte metrics --days=30 # cost + runtime per command, from Claude Code's transcripts
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`doctor`'s exit code is what makes it a CI step, and the three of them are what the Francois
|
|
259
|
+
extension below renders.
|
|
260
|
+
|
|
261
|
+
`doctor`'s exit code makes it a CI step as-is. Add `--porcelain` for one record per line with
|
|
262
|
+
`U+001F` between fields (a spec title with a space in it never misaligns a column), or `--json`
|
|
263
|
+
for the native document. Both reuse the dashboard's own readers, so the board and the CLI can
|
|
264
|
+
never give two answers about one repo.
|
|
265
|
+
|
|
266
|
+
`--panel` — on `specs`, `doctor` and `metrics` — emits the payload shape a
|
|
267
|
+
[Francois](https://github.com/antoine-gmnz/francois) extension panel expects. It is the one
|
|
268
|
+
Francois-aware surface in the package, and it exists for
|
|
269
|
+
[**francois-plugin-cohorte**](https://github.com/TheBidouilleAgency/francois-plugin-cohorte): a
|
|
270
|
+
manifest-only extension that renders the spec board, the doctor report and the 30-day cost as
|
|
271
|
+
three panels beside your sessions.
|
|
272
|
+
|
|
273
|
+
A Francois extension may only spawn a **bare binary name resolved on `PATH`** — never `npx`,
|
|
274
|
+
never a shell, never an absolute path. So this one needs the global install, and nothing else
|
|
275
|
+
will do:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
npm i -g cohorte
|
|
279
|
+
francois ext install TheBidouilleAgency/cohorte
|
|
280
|
+
```
|
|
281
|
+
|
|
237
282
|
## Releasing (maintainers)
|
|
238
283
|
|
|
239
284
|
Versions are tracked with npm semver — the published package is the release artifact.
|
|
@@ -246,8 +291,10 @@ a version bump just run the sanity checks.
|
|
|
246
291
|
`npm version patch --no-git-tag-version`), commit, push — CI does the rest (publish + tag +
|
|
247
292
|
release). No local tagging needed.
|
|
248
293
|
|
|
249
|
-
`
|
|
294
|
+
`npm i -g cohorte@latest` then serves the new version everywhere; installed cores record
|
|
250
295
|
it in `.claude/pipeline/VERSION` and bundled repos in their committed `pipeline.json` pointer.
|
|
296
|
+
`install` and `update` compare themselves against the registry and say so when they are behind,
|
|
297
|
+
so a CLI pinned at an old version cannot quietly re-lay an old core.
|
|
251
298
|
|
|
252
299
|
## The commands
|
|
253
300
|
|
|
@@ -380,7 +427,7 @@ profile/
|
|
|
380
427
|
SCHEMA.md # field reference
|
|
381
428
|
cohorte.config.template.yaml # seeds ~/.claude/cohorte.config.yaml (kanban)
|
|
382
429
|
scripts/ # worktree-isolation templates + the shipped preflight/kanban scripts
|
|
383
|
-
dashboard/ # local web cockpit (
|
|
430
|
+
dashboard/ # local web cockpit (cohorte dashboard) — see dashboard/README.md
|
|
384
431
|
server/ # dependency-free node runtime (serves the built app + JSON/stream API)
|
|
385
432
|
app/ # Vite + React source (built to dashboard/dist/ at publish time)
|
|
386
433
|
```
|
package/bin/cli.js
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
// cohorte — installer CLI for the portable multi-agent pipeline.
|
|
3
3
|
// Cross-platform, dependency-free port of install.sh / install.ps1.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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.
|
|
10
15
|
|
|
11
16
|
'use strict';
|
|
12
17
|
|
|
@@ -37,6 +42,8 @@ Usage:
|
|
|
37
42
|
cohorte update [target] [--global] [--runtime=a,b | --all-runtimes]
|
|
38
43
|
cohorte dashboard [target] [--port=N] [--host=ADDR] [--open]
|
|
39
44
|
cohorte metrics [target] [--days=N] [--since=ISO] [--runs] [--json]
|
|
45
|
+
cohorte specs [target] [--porcelain | --json | --panel]
|
|
46
|
+
cohorte doctor [target] [--porcelain | --json | --panel]
|
|
40
47
|
cohorte version
|
|
41
48
|
|
|
42
49
|
Commands:
|
|
@@ -56,8 +63,18 @@ Commands:
|
|
|
56
63
|
~/.claude/projects — nothing to enable, and it covers runs that
|
|
57
64
|
already happened. Worktree-aware, so a feature adds up. --json for
|
|
58
65
|
the raw rollup, --runs to include every individual invocation.
|
|
66
|
+
specs The spec board of <target>: id, status, branch, title, read from the
|
|
67
|
+
frontmatter of specs/*.md. --porcelain for one record per line with
|
|
68
|
+
U+001F between fields, --json for the list.
|
|
69
|
+
doctor The same checks /cohorte-doctor runs (core, pointer, profile, agents,
|
|
70
|
+
hook, gate, retrieval, design, isolation, specs), without a coding
|
|
71
|
+
agent in the loop. Exits 1 when any check is bad, 0 otherwise, so it
|
|
72
|
+
drops into CI. --porcelain / --json as above.
|
|
59
73
|
version Print the installed CLI version.
|
|
60
74
|
|
|
75
|
+
--panel on specs, doctor and metrics emits the payload a Francois extension
|
|
76
|
+
panel expects (github.com/TheBidouilleAgency/francois-plugin-cohorte).
|
|
77
|
+
|
|
61
78
|
Runtimes (--runtime=): ${adapter.listRuntimes().join(', ')}
|
|
62
79
|
The pipeline's doctrine is one set of source prompts; the installer renders them into
|
|
63
80
|
whatever each coding agent reads (markdown + frontmatter, plain markdown, or TOML) and
|
|
@@ -86,13 +103,19 @@ const metricsFlags = [];
|
|
|
86
103
|
const isMetricsFlag = (a) =>
|
|
87
104
|
a === '--json' || a === '--runs' || a.startsWith('--days=') || a.startsWith('--since=');
|
|
88
105
|
|
|
106
|
+
// How `specs`, `doctor` and `metrics` render. One variable, not one flag per command,
|
|
107
|
+
// so `--porcelain` never means two different things depending on where it sits.
|
|
108
|
+
let format = 'human';
|
|
109
|
+
|
|
89
110
|
// Which coding agents to install for. Empty ⇒ resolved later (detect, then ask on a TTY,
|
|
90
111
|
// then fall back to claude — the only behaviour that existed before 2.2.0).
|
|
91
112
|
let wantRuntimes = [];
|
|
92
113
|
|
|
93
114
|
for (const a of args) {
|
|
94
|
-
if (a === 'install' || a === 'update' || a === 'dashboard' || a === 'metrics'
|
|
95
|
-
|
|
115
|
+
if (a === 'install' || a === 'update' || a === 'dashboard' || a === 'metrics'
|
|
116
|
+
|| a === 'specs' || a === 'doctor') mode = a;
|
|
117
|
+
else if (a === '--porcelain' || a === '--panel') format = a.slice(2);
|
|
118
|
+
else if (isMetricsFlag(a)) { if (a === '--json') format = 'json'; metricsFlags.push(a); }
|
|
96
119
|
else if (a === '--all-runtimes') wantRuntimes = adapter.listRuntimes();
|
|
97
120
|
else if (a === '--runtimes' || a === '--runtime') {
|
|
98
121
|
console.error('error: --runtime needs a value, e.g. --runtime=codex,cursor'); process.exit(2);
|
|
@@ -136,10 +159,45 @@ if (mode === 'dashboard') {
|
|
|
136
159
|
return;
|
|
137
160
|
}
|
|
138
161
|
|
|
162
|
+
// --- specs / doctor: read-only reports on <target> ---------------------------
|
|
163
|
+
// Both reuse the dashboard's own readers, so the board and the CLI can never drift
|
|
164
|
+
// into two answers about the same repo. Nothing here writes or spawns anything.
|
|
165
|
+
if (mode === 'specs' || mode === 'doctor') {
|
|
166
|
+
const report = require('./report.js');
|
|
167
|
+
const { state, scanSpecs } = require('../dashboard/server/doctor.js');
|
|
168
|
+
const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
169
|
+
|
|
170
|
+
if (mode === 'specs') {
|
|
171
|
+
const records = report.specRecords(target, scanSpecs);
|
|
172
|
+
if (format === 'json') console.log(JSON.stringify({ project: target, specs: records }, null, 2));
|
|
173
|
+
else if (format === 'porcelain') { if (records.length) console.log(report.specsPorcelain(records)); }
|
|
174
|
+
else if (format === 'panel') console.log(report.specsPanel(records));
|
|
175
|
+
else console.log(report.specsHuman(records));
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// `state` is async (it probes versions), so this branch owns the whole tail.
|
|
180
|
+
state({ projectRoot: target, globalDir, cliVersion: VERSION }).then((s) => {
|
|
181
|
+
const records = report.checkRecords(s);
|
|
182
|
+
if (format === 'json') console.log(JSON.stringify(s, null, 2));
|
|
183
|
+
else if (format === 'porcelain') { if (records.length) console.log(report.doctorPorcelain(records)); }
|
|
184
|
+
else if (format === 'panel') console.log(report.doctorPanel(records));
|
|
185
|
+
else console.log(report.doctorHuman(s, records));
|
|
186
|
+
// A bad check is a failure the shell can branch on; warn and skip are not.
|
|
187
|
+
process.exit(s.summary.bad > 0 && format !== 'panel' ? 1 : 0);
|
|
188
|
+
}).catch((e) => {
|
|
189
|
+
console.error(`error: ${e && e.message ? e.message : e}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
139
195
|
// --- metrics: cost + runtime per command ------------------------------------
|
|
140
196
|
// The collector is ESM and this CLI is CommonJS, so it runs as a child process rather
|
|
141
197
|
// than being required. stdio is inherited so --json stays pipeable.
|
|
142
198
|
if (mode === 'metrics') {
|
|
199
|
+
// `--panel` is not in isMetricsFlag (it is not metrics-specific), so forward it here.
|
|
200
|
+
if (format === 'panel') metricsFlags.push('--panel');
|
|
143
201
|
const script = path.join(pkgRoot, 'scripts', 'metrics', 'collect.mjs');
|
|
144
202
|
if (!fs.existsSync(script)) {
|
|
145
203
|
console.error(`error: metrics collector not found at ${script}`);
|
|
@@ -238,7 +296,7 @@ function copyCore() {
|
|
|
238
296
|
resolveTemplateConditionals(pipelineDir);
|
|
239
297
|
// Copy the *.template files AND the shipped executables (kanban-move.sh,
|
|
240
298
|
// preflight.sh). Until 1.2.4 this loop took only `.template`, so every
|
|
241
|
-
// `
|
|
299
|
+
// `cohorte install/update` produced a core missing both scripts — and since
|
|
242
300
|
// every caller chains them with `|| true`, the result was silent: no kanban card
|
|
243
301
|
// moves, no error. The shell installers named them explicitly
|
|
244
302
|
// and this port drifted. The rule below needs no list to keep in sync: a `<x>.sh`
|
|
@@ -333,7 +391,7 @@ function renderSurfaces() {
|
|
|
333
391
|
|
|
334
392
|
// Every agent in core/agents/ EXCEPT the *.template.md ones, which /cohorte-init-pipeline renders
|
|
335
393
|
// per-surface. Until 1.2.6 this was a hardcoded ['review.md', 'release.md'] that never grew
|
|
336
|
-
// the agents the shell installers copy, so `
|
|
394
|
+
// the agents the shell installers copy, so `cohorte install` shipped a command with no
|
|
337
395
|
// agent to dispatch — the run reported the command as not installed.
|
|
338
396
|
// Reading the directory needs no list to keep in sync with the shell installers.
|
|
339
397
|
const agentDir = path.join(src, 'core', 'agents');
|
|
@@ -562,7 +620,7 @@ function registerGlobalHook() {
|
|
|
562
620
|
const file = path.join(dest, 'hooks', 'gate.py');
|
|
563
621
|
const base = path.basename(file);
|
|
564
622
|
// Trailing-quote tolerant: the Windows form is `py "C:\...\gate.py"`, and a
|
|
565
|
-
// bare .endsWith() missed it — which is how repeat `
|
|
623
|
+
// bare .endsWith() missed it — which is how repeat `cohorte install`
|
|
566
624
|
// runs accumulated a duplicate registration every time (gate.py then ran
|
|
567
625
|
// once per copy on every Bash call).
|
|
568
626
|
const isGate = entry => (entry.hooks || []).some(
|
|
@@ -604,7 +662,7 @@ function bumpPointerVersion(ptr) {
|
|
|
604
662
|
// configured on this machine, and — on a TTY — let the human confirm, because installing
|
|
605
663
|
// into a runtime they don't use litters a config dir they never asked us to touch. With no
|
|
606
664
|
// TTY and no flag we install for Claude Code alone: the behaviour of every version before
|
|
607
|
-
// the adapter, so a scripted `
|
|
665
|
+
// the adapter, so a scripted `cohorte install` keeps doing exactly what it did.
|
|
608
666
|
async function selectRuntimes() {
|
|
609
667
|
if (wantRuntimes.length) return wantRuntimes;
|
|
610
668
|
const found = adapter.detectRuntimes();
|
|
@@ -622,6 +680,33 @@ async function selectRuntimes() {
|
|
|
622
680
|
return picked.length ? [...new Set(picked)] : found;
|
|
623
681
|
}
|
|
624
682
|
|
|
683
|
+
// --- staleness notice --------------------------------------------------------
|
|
684
|
+
// `install` and `update` write a core taken from THIS package, so the core is only
|
|
685
|
+
// as fresh as the CLI that ran. `npx cohorte@latest` made that self-correcting; a
|
|
686
|
+
// global install does not — `cohorte update` on a pinned 2.5.0 would re-lay the
|
|
687
|
+
// 2.5.0 core forever and report success, which is an update that updates nothing.
|
|
688
|
+
// So the CLI checks its own version against the registry and says what to run.
|
|
689
|
+
// Never fatal, never blocking a successful install: a short fetch, no `npm view`
|
|
690
|
+
// fallback, and silence on any failure. COHORTE_NO_VERSION_CHECK / CI opt out.
|
|
691
|
+
async function staleVersionNotice() {
|
|
692
|
+
if (process.env.COHORTE_NO_VERSION_CHECK || process.env.CI) return;
|
|
693
|
+
let latest = null;
|
|
694
|
+
try {
|
|
695
|
+
const v = require('../dashboard/server/versions.js');
|
|
696
|
+
latest = await v.latestNpm({ timeoutMs: 2500, fallback: false });
|
|
697
|
+
if (!latest || v.cmpSemver(VERSION, latest) >= 0) return;
|
|
698
|
+
} catch { return; }
|
|
699
|
+
// How you got here decides the fix: under npx the package is transient (and may
|
|
700
|
+
// have come from the cache rather than the registry), so the answer is to pin
|
|
701
|
+
// @latest; with a global install the answer is to upgrade the global.
|
|
702
|
+
const viaNpx = /[\\/]_npx[\\/]/.test(pkgRoot);
|
|
703
|
+
console.log(`
|
|
704
|
+
! You ran cohorte ${VERSION}, but ${latest} is published — the core just written is ${VERSION}.
|
|
705
|
+
${viaNpx
|
|
706
|
+
? 'Re-run as npx cohorte@latest <same command> to lay down the current core.'
|
|
707
|
+
: 'Upgrade with npm i -g cohorte@latest and re-run the same command.'}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
625
710
|
// --- run ---------------------------------------------------------------------
|
|
626
711
|
(async () => {
|
|
627
712
|
const selected = await selectRuntimes();
|
|
@@ -640,6 +725,8 @@ if (selected.length > 1) {
|
|
|
640
725
|
console.log(' The doctrine is identical; what differs is enforcement — run /cohorte-doctor in');
|
|
641
726
|
console.log(' each one to see what it can and cannot guarantee.');
|
|
642
727
|
}
|
|
728
|
+
// Last, so a stale CLI is the final thing on screen rather than scrolled past.
|
|
729
|
+
await staleVersionNotice();
|
|
643
730
|
})();
|
|
644
731
|
|
|
645
732
|
async function installOne() {
|
|
@@ -685,7 +772,7 @@ if (scope === 'global') {
|
|
|
685
772
|
copyCore();
|
|
686
773
|
// Register on UPDATE too — install.sh and install.ps1 always have, and this
|
|
687
774
|
// port skipping it is why a duplicated or stale-matcher registration could
|
|
688
|
-
// never be repaired by `
|
|
775
|
+
// never be repaired by `cohorte update`: the only route that rewrites it
|
|
689
776
|
// was a full re-install, which is not what anyone runs to get a fix. Safe to
|
|
690
777
|
// run every time — registration reconciles only gate.py entries and leaves
|
|
691
778
|
// every other hook and settings key untouched.
|
|
@@ -705,7 +792,7 @@ Per repo:
|
|
|
705
792
|
teammates know to install the global core (${REPO_URL}).
|
|
706
793
|
3. Commit PIPELINE.md + .claude/, then /cohorte-brainstorm to start a feature.
|
|
707
794
|
|
|
708
|
-
Update later with:
|
|
795
|
+
Update later with: npm i -g cohorte@latest && cohorte update --global
|
|
709
796
|
|
|
710
797
|
Global kanban config, user-scoped — optional:
|
|
711
798
|
· One consolidated file: ${path.join(globalDir, 'cohorte.config.yaml')}
|
|
@@ -730,7 +817,7 @@ Next:
|
|
|
730
817
|
PIPELINE.md + renders one implementer agent per surface.
|
|
731
818
|
3. Commit PIPELINE.md, then /cohorte-brainstorm to start a feature.
|
|
732
819
|
|
|
733
|
-
Update later with:
|
|
820
|
+
Update later with: npm i -g cohorte@latest && cohorte update
|
|
734
821
|
Prefer one shared core across all your repos? Re-run with --global.`);
|
|
735
822
|
} else {
|
|
736
823
|
console.log(`→ updating pipeline core in ${dest} (keeping your PIPELINE.md + rendered agents)`);
|
package/bin/report.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Machine-readable reports for `cohorte specs` and `cohorte doctor`.
|
|
3
|
+
//
|
|
4
|
+
// Both commands read what the dashboard already computes (dashboard/server/doctor.js:
|
|
5
|
+
// `scanSpecs` and `state`) and render it four ways:
|
|
6
|
+
//
|
|
7
|
+
// default a human table on a terminal
|
|
8
|
+
// --porcelain one record per line, fields separated by U+001F (the ASCII unit
|
|
9
|
+
// separator) — stable, greppable, and immune to a spec title that
|
|
10
|
+
// contains a space, a tab or a pipe
|
|
11
|
+
// --json the native document (the full doctor state, the spec list)
|
|
12
|
+
// --panel the payload shape a Francois extension panel validates against
|
|
13
|
+
// (`{rows:[{key,value,tone}]}` / `{rows:[{id,cells,tone}]}`). This is
|
|
14
|
+
// the ONLY Francois-aware surface in cohorte; everything else here is
|
|
15
|
+
// generic. See github.com/TheBidouilleAgency/francois-plugin-cohorte.
|
|
16
|
+
//
|
|
17
|
+
// Dependency-free, and it never writes anything.
|
|
18
|
+
|
|
19
|
+
const US = String.fromCharCode(0x1f); // ASCII unit separator (U+001F)
|
|
20
|
+
|
|
21
|
+
// A spec's status → a display tone. The tones are the ones a status column can carry
|
|
22
|
+
// anywhere (terminal colour, panel row tone); `busy` marks the two statuses that mean
|
|
23
|
+
// "a command is mid-flight on this spec".
|
|
24
|
+
const STATUS_TONE = {
|
|
25
|
+
draft: 'neutral',
|
|
26
|
+
frozen: 'neutral',
|
|
27
|
+
'in-progress': 'busy',
|
|
28
|
+
'in-review': 'busy',
|
|
29
|
+
shipped: 'ok',
|
|
30
|
+
blocked: 'error',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// doctor's four check states → the same tone vocabulary. `skip` is not a failure:
|
|
34
|
+
// a check that does not apply to this project (no design system, no worktrees) reads
|
|
35
|
+
// neutral, never warn.
|
|
36
|
+
const CHECK_TONE = { ok: 'ok', warn: 'warn', bad: 'error', skip: 'neutral' };
|
|
37
|
+
|
|
38
|
+
// Collapse anything that would break the line format or a single-line cell. A spec
|
|
39
|
+
// title is free text written by a human in YAML frontmatter — it can contain a newline
|
|
40
|
+
// continuation, and it must never split one record into two. A stray separator inside a
|
|
41
|
+
// field goes the same way, and so does the padding a doctor `fix` carries for terminal
|
|
42
|
+
// alignment — in a one-line panel cell that padding reads as a hole.
|
|
43
|
+
function flat(value) {
|
|
44
|
+
return String(value == null ? '' : value)
|
|
45
|
+
.replace(new RegExp(`[\\s${US}]+`, 'g'), ' ')
|
|
46
|
+
.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function specRecords(projectRoot, scanSpecs) {
|
|
50
|
+
return scanSpecs(projectRoot).map((s) => ({
|
|
51
|
+
id: s.id,
|
|
52
|
+
title: s.title || '',
|
|
53
|
+
status: s.status || 'unknown',
|
|
54
|
+
branch: s.branch || '',
|
|
55
|
+
file: s.file,
|
|
56
|
+
tone: STATUS_TONE[s.status] || 'warn', // an unknown status IS the warning
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function specsPorcelain(records) {
|
|
61
|
+
return records
|
|
62
|
+
.map((r) => [r.id, r.title, r.status, r.branch, r.tone].map(flat).join(US))
|
|
63
|
+
.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function specsPanel(records) {
|
|
67
|
+
return JSON.stringify({
|
|
68
|
+
rows: records.map((r) => ({
|
|
69
|
+
id: flat(r.id),
|
|
70
|
+
tone: r.tone,
|
|
71
|
+
cells: { id: flat(r.id), title: flat(r.title), status: flat(r.status), branch: flat(r.branch) },
|
|
72
|
+
})),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function specsHuman(records) {
|
|
77
|
+
if (!records.length) return 'no specs in ./specs — /cohorte-spec writes the first one';
|
|
78
|
+
const w = (k) => Math.max(...records.map((r) => flat(r[k]).length), k.length);
|
|
79
|
+
const [wi, ws, wb] = [w('id'), w('status'), w('branch')];
|
|
80
|
+
const head = `${'id'.padEnd(wi)} ${'status'.padEnd(ws)} ${'branch'.padEnd(wb)} title`;
|
|
81
|
+
const rows = records.map(
|
|
82
|
+
(r) => `${flat(r.id).padEnd(wi)} ${flat(r.status).padEnd(ws)} ${flat(r.branch).padEnd(wb)} ${flat(r.title)}`,
|
|
83
|
+
);
|
|
84
|
+
return [head, '-'.repeat(head.length), ...rows].join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function checkRecords(state) {
|
|
88
|
+
return state.checks.map((c) => ({
|
|
89
|
+
id: c.id,
|
|
90
|
+
label: c.label,
|
|
91
|
+
status: c.status,
|
|
92
|
+
detail: c.detail,
|
|
93
|
+
fix: c.fix || '',
|
|
94
|
+
tone: CHECK_TONE[c.status] || 'neutral',
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function doctorPorcelain(records) {
|
|
99
|
+
return records
|
|
100
|
+
.map((r) => [r.id, r.label, r.status, r.detail, r.fix].map(flat).join(US))
|
|
101
|
+
.join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// key-value, one row per check: the check's label keyed against what it found. The fix
|
|
105
|
+
// rides in the value for a failing check — a panel row has nowhere else to put it, and
|
|
106
|
+
// a health report that says "broken" without saying "run this" is half a report.
|
|
107
|
+
function doctorPanel(records) {
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
rows: records.map((r) => ({
|
|
110
|
+
key: flat(r.label),
|
|
111
|
+
value: flat(r.fix && r.tone !== 'ok' ? `${r.detail} → ${r.fix}` : r.detail),
|
|
112
|
+
tone: r.tone,
|
|
113
|
+
})),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function doctorHuman(state, records) {
|
|
118
|
+
const mark = { ok: 'ok ', warn: 'warn', bad: 'BAD ', skip: 'skip' };
|
|
119
|
+
const lines = [`cohorte doctor — ${state.project}`, ''];
|
|
120
|
+
for (const r of records) {
|
|
121
|
+
lines.push(`${mark[r.status] || '? '} ${r.label} — ${flat(r.detail)}`);
|
|
122
|
+
if (r.fix && r.status !== 'ok') lines.push(` fix: ${flat(r.fix)}`);
|
|
123
|
+
}
|
|
124
|
+
const s = state.summary;
|
|
125
|
+
lines.push('', `${s.ok} ok · ${s.warn} warn · ${s.bad} bad · ${s.skip} skipped`);
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
US,
|
|
131
|
+
STATUS_TONE,
|
|
132
|
+
CHECK_TONE,
|
|
133
|
+
specRecords,
|
|
134
|
+
specsPorcelain,
|
|
135
|
+
specsPanel,
|
|
136
|
+
specsHuman,
|
|
137
|
+
checkRecords,
|
|
138
|
+
doctorPorcelain,
|
|
139
|
+
doctorPanel,
|
|
140
|
+
doctorHuman,
|
|
141
|
+
};
|
|
@@ -35,11 +35,15 @@ the human's choices — so `/cohorte-init-pipeline` never needs re-running for a
|
|
|
35
35
|
- Otherwise use the published npm package (preferred — installs the latest tagged release):
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
npm i -g cohorte@latest # the CLI itself, refreshed
|
|
39
|
+
cohorte update --global # global core
|
|
40
|
+
cohorte update # bundled core of the current repo
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
If `cohorte` is not on PATH, `npx cohorte@latest update [--global]` runs the same thing
|
|
44
|
+
without installing anything.
|
|
45
|
+
|
|
46
|
+
- If npm is unavailable, fall back to piping the installer from the repo's latest `main`:
|
|
43
47
|
|
|
44
48
|
```sh
|
|
45
49
|
curl -fsSL https://raw.githubusercontent.com/TheBidouilleAgency/cohorte/main/install.sh | sh -s -- --update --global
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
9. **Write the pointer** `<state>/pipeline.json` (committed — this is how a teammate who clones the repo
|
|
84
84
|
knows which core to install):
|
|
85
85
|
`{ "pipeline": "cohorte", "mode": "<bundled|global>", "core_version": "<contents of the
|
|
86
|
-
installer's pipeline/VERSION>", "install": "<per mode: bundled ⇒ \"
|
|
87
|
-
note that the core is committed under .claude/; global ⇒ \"
|
|
86
|
+
installer's pipeline/VERSION>", "install": "<per mode: bundled ⇒ \"npm i -g cohorte && cohorte install\"
|
|
87
|
+
note that the core is committed under .claude/; global ⇒ \"npm i -g cohorte && cohorte install --global\"
|
|
88
88
|
(or, without npm: curl -fsSL https://raw.githubusercontent.com/TheBidouilleAgency/cohorte/main/install.sh | sh -s -- --global;
|
|
89
89
|
Windows: install.ps1 -Global from the same repo)> " }`.
|
|
90
90
|
In **global** mode also add, near the top of `<memory>`, a one-liner:
|
package/dashboard/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dashboard — architecture
|
|
2
2
|
|
|
3
|
-
A local web cockpit for the pipeline, launched with `
|
|
3
|
+
A local web cockpit for the pipeline, launched with `cohorte dashboard`
|
|
4
4
|
(see the [root README](../README.md#dashboard--a-local-web-cockpit) for user-facing docs).
|
|
5
5
|
|
|
6
6
|
## Two halves: shipped runtime vs dev build
|
|
@@ -12,7 +12,7 @@ dashboard/
|
|
|
12
12
|
dist/ → app/ built output. Shipped, served by server/. Git-ignored, rebuilt at publish.
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
- **`server/`** is plain node (`http`, `fs`, `child_process`) — no deps, so `
|
|
15
|
+
- **`server/`** is plain node (`http`, `fs`, `child_process`) — no deps, so `cohorte dashboard`
|
|
16
16
|
needs no install. It serves `dist/` as static files + a small JSON/stream API.
|
|
17
17
|
- **`app/`** is a Vite+React app built to `dist/`. `npm run build:dashboard` (root) runs
|
|
18
18
|
`npm --prefix dashboard/app ci && … run build`; CI does this before `npm pack`/`publish`
|
|
@@ -58,18 +58,18 @@ function mk(id, label, status, detail, fix) {
|
|
|
58
58
|
function checkCore(v) {
|
|
59
59
|
if (v.installMode === 'none') {
|
|
60
60
|
return mk('core', 'Core & pointer', 'bad', 'no pipeline core installed for this project',
|
|
61
|
-
'
|
|
61
|
+
'cohorte install (or --global)');
|
|
62
62
|
}
|
|
63
63
|
if (v.pointer.present && v.pointer.core_version && v.installedVersion &&
|
|
64
64
|
v.pointer.core_version !== v.installedVersion) {
|
|
65
65
|
return mk('core', 'Core & pointer', 'warn',
|
|
66
66
|
`pointer says core ${v.pointer.core_version} but installed core is ${v.installedVersion}`,
|
|
67
|
-
'
|
|
67
|
+
'cohorte update (reconcile the pointer)');
|
|
68
68
|
}
|
|
69
69
|
if (v.freshness === -1) {
|
|
70
70
|
return mk('core', 'Core & pointer', 'warn',
|
|
71
71
|
`core ${v.installedVersion} installed (${v.installMode}); npm latest is ${v.latest}`,
|
|
72
|
-
'/cohorte-update-pipeline (or
|
|
72
|
+
'/cohorte-update-pipeline (or cohorte update)');
|
|
73
73
|
}
|
|
74
74
|
const tail = v.latest ? `, npm latest ${v.latest}` : ', npm unreachable';
|
|
75
75
|
return mk('core', 'Core & pointer', 'ok', `core ${v.installedVersion} (${v.installMode})${tail}`);
|
|
@@ -264,7 +264,7 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
264
264
|
'nothing to fix; run /cohorte-doctor inside that runtime for the full picture');
|
|
265
265
|
}
|
|
266
266
|
return mk('hooks', 'Gate hook', 'warn', 'no runtime installed to register the gate hook against',
|
|
267
|
-
'
|
|
267
|
+
'cohorte install (or --global)');
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
const problems = [];
|
|
@@ -306,7 +306,7 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
306
306
|
// alone reads as if the gate were off everywhere, when four of five may be fine.
|
|
307
307
|
const tail = okLines.length ? ` (ok: ${okLines.join(', ')})` : '';
|
|
308
308
|
return mk('hooks', 'Gate hook', 'warn', problems.join(' · ') + tail,
|
|
309
|
-
'
|
|
309
|
+
'npm i -g cohorte@latest && cohorte update (re-registers the gate hook for every installed runtime)');
|
|
310
310
|
}
|
|
311
311
|
const tail = advisory.length ? ` · ${advisory.map(l => l.label).join(', ')}: advisory --check only` : '';
|
|
312
312
|
return mk('hooks', 'Gate hook', 'ok', `registered once for ${okLines.join(', ')}${tail}`);
|
|
@@ -381,16 +381,16 @@ function checkWorkflows(projectRoot, globalDir, installMode, all) {
|
|
|
381
381
|
if (missing.length === scripts.length) {
|
|
382
382
|
return mk('workflows', 'Workflows', 'warn',
|
|
383
383
|
'no workflow scripts installed — conversational commands only (the default path)',
|
|
384
|
-
'
|
|
384
|
+
'cohorte update (ships core/workflows/)');
|
|
385
385
|
}
|
|
386
386
|
if (missing.length) {
|
|
387
387
|
return mk('workflows', 'Workflows', 'warn', `missing script(s): ${missing.join(', ')}`,
|
|
388
|
-
'
|
|
388
|
+
'cohorte update (half-copied core)');
|
|
389
389
|
}
|
|
390
390
|
if (!exists(path.join(agentsDir, 'profile-reader.md'))) {
|
|
391
391
|
return mk('workflows', 'Workflows', 'warn',
|
|
392
392
|
'scripts present but the profile-reader agent (their phase 0) is missing',
|
|
393
|
-
'
|
|
393
|
+
'cohorte update (re-copies the fixed agents)');
|
|
394
394
|
}
|
|
395
395
|
return mk('workflows', 'Workflows', 'ok',
|
|
396
396
|
'scripts + profile-reader installed — opt-in per run; needs Claude Code ≥ 2.1.154 with ' +
|
|
@@ -38,9 +38,9 @@ const CACHE_MS = 5 * 60 * 1000;
|
|
|
38
38
|
// this once per tracked project, so the dashboard's 15s poll never finished.
|
|
39
39
|
const FAIL_CACHE_MS = 60 * 1000;
|
|
40
40
|
|
|
41
|
-
async function fetchRegistry() {
|
|
41
|
+
async function fetchRegistry(timeoutMs = 5000) {
|
|
42
42
|
const ctrl = new AbortController();
|
|
43
|
-
const t = setTimeout(() => ctrl.abort(),
|
|
43
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
44
44
|
try {
|
|
45
45
|
const res = await fetch('https://registry.npmjs.org/cohorte/latest', {
|
|
46
46
|
signal: ctrl.signal,
|
|
@@ -75,7 +75,12 @@ function npmView() {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
let _inflight = null;
|
|
78
|
-
|
|
78
|
+
// `opts` bounds what a CALLER is willing to pay for the answer. The CLI's staleness
|
|
79
|
+
// notice runs at the tail of an install, where the default 5s fetch + 8s `npm view`
|
|
80
|
+
// fallback would make an OFFLINE install appear to hang for 13 seconds after it had
|
|
81
|
+
// already succeeded — so it asks for a short fetch and no fallback. The cache is
|
|
82
|
+
// shared across callers either way: a cheap lookup still serves an expensive one.
|
|
83
|
+
async function latestNpm({ timeoutMs = 5000, fallback = true } = {}) {
|
|
79
84
|
const age = Date.now() - _cache.at;
|
|
80
85
|
if (_cache.at && age < (_cache.value ? CACHE_MS : FAIL_CACHE_MS)) return _cache.value;
|
|
81
86
|
// /api/fleet resolves N projects concurrently — without this, N lookups race and
|
|
@@ -83,7 +88,7 @@ async function latestNpm() {
|
|
|
83
88
|
if (_inflight) return _inflight;
|
|
84
89
|
_inflight = (async () => {
|
|
85
90
|
try {
|
|
86
|
-
const v = (await fetchRegistry()) || npmView();
|
|
91
|
+
const v = (await fetchRegistry(timeoutMs)) || (fallback ? npmView() : null);
|
|
87
92
|
_cache = { value: v || null, at: Date.now() };
|
|
88
93
|
return v || null;
|
|
89
94
|
} finally {
|
|
@@ -141,4 +146,4 @@ async function versions({ projectRoot, globalDir, cliVersion }) {
|
|
|
141
146
|
};
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
module.exports = { versions };
|
|
149
|
+
module.exports = { versions, latestNpm, cmpSemver };
|
package/install.ps1
CHANGED
|
@@ -122,7 +122,7 @@ cohorte needs Node >= 18 to install.
|
|
|
122
122
|
The pipeline's commands are rendered per coding agent (Claude Code, Codex, Cursor,
|
|
123
123
|
Gemini CLI, OpenCode) at install time; there is no PowerShell equivalent of that
|
|
124
124
|
step, and a raw copy would install prompts this runtime cannot follow.
|
|
125
|
-
Install Node, then:
|
|
125
|
+
Install Node, then: npm i -g cohorte; cohorte install$(if ($Global) { ' --global' })
|
|
126
126
|
"@
|
|
127
127
|
exit 1
|
|
128
128
|
|
|
@@ -257,7 +257,7 @@ cohorte needs Node >= 18 to install.
|
|
|
257
257
|
# pipeline capability config is USER-level (vault, Notion DB, kanban boards) — it lives in
|
|
258
258
|
# the user's .claude regardless of install scope. Seed only if neither the consolidated nor
|
|
259
259
|
# the legacy copy exists. Non-interactive here: seeds disabled defaults; /cohorte-init-pipeline +
|
|
260
|
-
# /cohorte-update-pipeline wire it (
|
|
260
|
+
# /cohorte-update-pipeline wire it (the npm CLI's installer offers a quick interview instead).
|
|
261
261
|
function Initialize-Config {
|
|
262
262
|
$userClaude = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME '.claude' }
|
|
263
263
|
$cfg = Join-Path $userClaude 'cohorte.config.yaml'
|
package/install.sh
CHANGED
|
@@ -42,8 +42,9 @@ install.sh — install the cohorte pipeline core.
|
|
|
42
42
|
sh install.sh --update --global
|
|
43
43
|
|
|
44
44
|
Honours $CLAUDE_CONFIG_DIR for the global destination and $PIPELINE_REPO for the
|
|
45
|
-
source when piped through curl. The npm CLI (`
|
|
46
|
-
thing and is the documented route; this script exists for Node-less
|
|
45
|
+
source when piped through curl. The npm CLI (`npm i -g cohorte` then `cohorte install`)
|
|
46
|
+
does the same thing and is the documented route; this script exists for Node-less
|
|
47
|
+
environments.
|
|
47
48
|
USAGE
|
|
48
49
|
exit 0 ;;
|
|
49
50
|
--) shift; break ;;
|
|
@@ -89,7 +90,7 @@ echo "error: cohorte needs Node ≥ 18 to install." >&2
|
|
|
89
90
|
echo " The pipeline's commands are rendered per coding agent (Claude Code, Codex, Cursor," >&2
|
|
90
91
|
echo " Gemini CLI, OpenCode) at install time; there is no shell equivalent of that step," >&2
|
|
91
92
|
echo " and a raw copy would install prompts this runtime cannot follow." >&2
|
|
92
|
-
echo " Install Node, then:
|
|
93
|
+
echo " Install Node, then: npm i -g cohorte && cohorte install$([ "$scope" = global ] && echo ' --global')" >&2
|
|
93
94
|
exit 1
|
|
94
95
|
|
|
95
96
|
# --- resolve the destination .claude dir ------------------------------------
|
|
@@ -218,7 +219,7 @@ scrub_research_questionnaire() {
|
|
|
218
219
|
# pipeline capability config is USER-level (vault, Notion DB, kanban boards) — it lives in
|
|
219
220
|
# ~/.claude regardless of install scope. Seed it only if neither the consolidated nor the
|
|
220
221
|
# legacy copy exists. This piped installer is non-interactive: it seeds disabled defaults;
|
|
221
|
-
# /cohorte-init-pipeline + /cohorte-update-pipeline wire it (
|
|
222
|
+
# /cohorte-init-pipeline + /cohorte-update-pipeline wire it (the npm CLI's installer offers a quick interview instead).
|
|
222
223
|
seed_config() {
|
|
223
224
|
base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
224
225
|
cfg="$base/cohorte.config.yaml"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code, Codex CLI, Cursor, Gemini CLI and OpenCode — install the core, run /cohorte-init-pipeline, and it adapts to your project's stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cohorte": "bin/cli.js"
|
|
@@ -481,6 +481,22 @@ function main(argv) {
|
|
|
481
481
|
agents: rows.reduce((n, r) => n + r.agents.total, 0),
|
|
482
482
|
};
|
|
483
483
|
|
|
484
|
+
// The headline figures as a Francois extension `stat-row` payload — four tiles, no
|
|
485
|
+
// table. Deliberately ahead of the --json branch: --panel is a shape, not a filter,
|
|
486
|
+
// and passing both should never print two documents.
|
|
487
|
+
// See github.com/TheBidouilleAgency/francois-plugin-cohorte.
|
|
488
|
+
if (flag('panel')) {
|
|
489
|
+
const window = opt('days') ? `last ${opt('days')} days` : since ? 'since ' + new Date(since).toISOString().slice(0, 10) : 'all time';
|
|
490
|
+
const top = rows[0];
|
|
491
|
+
process.stdout.write(JSON.stringify({ tiles: [
|
|
492
|
+
{ label: 'Cost', value: fmtUsd(totals.cost), sublabel: window },
|
|
493
|
+
{ label: 'Runs', value: String(totals.runs), sublabel: `${totals.sessions} sessions` },
|
|
494
|
+
{ label: 'Subagents', value: String(totals.agents) },
|
|
495
|
+
...(top ? [{ label: 'Priciest', value: top.command, sublabel: fmtUsd(top.cost.total) }] : []),
|
|
496
|
+
] }) + '\n');
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
484
500
|
if (flag('json')) {
|
|
485
501
|
const out = { generatedAt: new Date().toISOString(), projectRoot: root,
|
|
486
502
|
checkouts: [...checkouts], pricesUpdated: PRICES.updated, totals, commands: rows };
|
|
@@ -177,7 +177,7 @@ for (const c of KANBAN_STAGES) {
|
|
|
177
177
|
// Every scripts/*.sh must be copied by BOTH shell installers. Callers chain these
|
|
178
178
|
// with `|| true`, so one an installer forgets is a silent no-op forever — no kanban
|
|
179
179
|
// card moves, no error. CI is the only place this is loud.
|
|
180
|
-
// The third installer, bin/cli.js (what `
|
|
180
|
+
// The third installer, bin/cli.js (what the `cohorte` CLI runs), copies by rule rather
|
|
181
181
|
// than by name, so grepping for filenames can't see it — ci.yml dry-runs it into a
|
|
182
182
|
// scratch HOME and asserts the same postconditions instead. Both are needed: this
|
|
183
183
|
// check catches a forgotten name, that one catches a drifted rule.
|