cohorte 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +27 -0
- package/bin/cli.js +55 -2
- package/bin/report.js +141 -0
- package/core/agents/implementer.template.md +27 -0
- package/core/agents/review.md +28 -4
- package/core/commands/cohorte-audit.md +8 -3
- package/core/commands/cohorte-review.md +1 -1
- package/core/workflows/review.js +2 -2
- package/package.json +1 -1
- package/scripts/metrics/collect.mjs +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,77 @@ 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.5.0 — 2026-08-14
|
|
11
|
+
|
|
12
|
+
- **Everything the pipeline knew about your repo, it would only say to a coding agent.**
|
|
13
|
+
`/cohorte-doctor` reads the health of an install and `specs/*.md` holds the board, but both
|
|
14
|
+
answers cost a session: you opened Claude Code, ran a command, and read prose. There was no
|
|
15
|
+
way to ask "is this repo's pipeline sound?" from a shell script, a CI job, or anything that
|
|
16
|
+
isn't an LLM.
|
|
17
|
+
|
|
18
|
+
Two read-only commands now answer without an agent in the loop:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
cohorte specs # the board: id · status · branch · title, from specs/*.md frontmatter
|
|
22
|
+
cohorte doctor # the /cohorte-doctor checks — exits 1 when any check is bad
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`doctor` **exits 1 on any bad check**, so it drops into CI as-is. Both reuse the dashboard's
|
|
26
|
+
own readers, so the board, the dashboard and the CLI can never give three answers about one
|
|
27
|
+
repo. `--porcelain` gives one record per line with `U+001F` between fields — a spec title
|
|
28
|
+
with a space in it never misaligns a column — and `--json` gives the native document.
|
|
29
|
+
|
|
30
|
+
- **The pipeline, in Francois.** A third shape, `--panel` (on `specs`, `doctor` and `metrics`),
|
|
31
|
+
emits the payload a [Francois](https://github.com/antoine-gmnz/francois) extension panel
|
|
32
|
+
validates against. It exists for
|
|
33
|
+
[**francois-plugin-cohorte**](https://github.com/TheBidouilleAgency/francois-plugin-cohorte):
|
|
34
|
+
a manifest-only extension — no code, no binary — that renders the 30-day cost, the doctor
|
|
35
|
+
report and the spec board as three panels beside your sessions, on any project with a
|
|
36
|
+
`PIPELINE.md`.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm i -g cohorte # a Francois extension may only spawn a
|
|
40
|
+
francois ext install TheBidouilleAgency/cohorte # bare binary on PATH — never npx, never a shell
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This is the only Francois-aware surface in the package, and it is one flag wide. Nothing in
|
|
44
|
+
the pipeline itself changed: no new agent, no new gate, no template to re-render — this
|
|
45
|
+
release needs no `/cohorte-update-pipeline`.
|
|
46
|
+
|
|
47
|
+
## 2.4.0 — 2026-08-13
|
|
48
|
+
|
|
49
|
+
- **The pipeline froze *what* to build, never *how much*.** A spec pins the contract and the
|
|
50
|
+
acceptance criteria, so the feature was right — but nothing ever asked an implementer whether the
|
|
51
|
+
helper it was about to write already existed three files over, whether the stdlib shipped it, or
|
|
52
|
+
whether the abstraction it was wrapping had exactly one caller. The diff came out correct and
|
|
53
|
+
fatter than it needed to be, and every extra line was then paid twice: once at build, once again at
|
|
54
|
+
`/cohorte-review`, whose cost is linear in the diff it reads.
|
|
55
|
+
|
|
56
|
+
Implementers now walk a **minimality ladder** before inventing anything: does it need to exist at
|
|
57
|
+
all → already in this repo → stdlib/framework → native platform feature → an already-installed
|
|
58
|
+
dependency → a few inline lines → only then the minimum the contract requires. It governs the
|
|
59
|
+
**how**, never the **what** — a contract field, an acceptance criterion, a test, a validation or an
|
|
60
|
+
authz check is out of its reach, by construction. It is bounded to **one lookup per candidate**, so
|
|
61
|
+
it costs a symbol search and not an exploration, and a shortcut kept on purpose surfaces in the
|
|
62
|
+
handoff's `## TODO / not done` with its ceiling and its upgrade trigger, instead of rotting in a
|
|
63
|
+
comment.
|
|
64
|
+
|
|
65
|
+
The `review` agent gains the matching axis — over-engineering, tagged `delete:` / `stdlib:` /
|
|
66
|
+
`native:` / `yagni:` / `shrink:`, always naming the cheaper replacement. It is deliberately the
|
|
67
|
+
weakest thing in the report: **5 findings max, MEDIUM ceiling, never a CRITICAL, never a REVISE,
|
|
68
|
+
never a BLOCK**. A diff whose only findings are `complexity` ships, and they park in the backlog
|
|
69
|
+
like any other nit. Tests, fixtures and anything the contract mandates are out of bounds — coverage
|
|
70
|
+
is not bloat. In audit mode the axis widens to the whole target (10 per domain, biggest cut first,
|
|
71
|
+
closing with `net: -N lines, -M deps possible.`), and `/cohorte-audit` files them under a new
|
|
72
|
+
`complexity` backlog tag: the cheapest wins in the file, since deleting code needs no new test.
|
|
73
|
+
|
|
74
|
+
Doctrine adapted from [ponytail](https://github.com/dietrichgebert/ponytail) (MIT) — the decision
|
|
75
|
+
ladder and the finding tags are theirs; the subordination to the frozen contract and the
|
|
76
|
+
can't-block severity ceiling are what make them safe inside a gated pipeline.
|
|
77
|
+
|
|
78
|
+
Run `/cohorte-update-pipeline` to re-render your surface agents — the ladder lives in the
|
|
79
|
+
implementer template, so it only reaches your agents through a reconcile.
|
|
80
|
+
|
|
10
81
|
## 2.3.0 — 2026-08-12
|
|
11
82
|
|
|
12
83
|
- **A bug fix had to pretend to be a feature.** The only way into the pipeline was
|
package/README.md
CHANGED
|
@@ -234,6 +234,33 @@ source lives in `dashboard/app/`, built to `dashboard/dist/` at publish time). T
|
|
|
234
234
|
are reimplemented in JS, so the dashboard needs no Claude session to compute state. See
|
|
235
235
|
[`dashboard/README.md`](dashboard/README.md) for the architecture.
|
|
236
236
|
|
|
237
|
+
## Reading a project without an agent
|
|
238
|
+
|
|
239
|
+
The two read-only halves of the pipeline, in the shell:
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
npx cohorte specs # the board: id · status · branch · title, from specs/*.md
|
|
243
|
+
npx cohorte doctor # the /cohorte-doctor checks — exits 1 when any check is bad
|
|
244
|
+
npx cohorte metrics --days=30 # cost + runtime per command, from Claude Code's transcripts
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`doctor`'s exit code makes it a CI step as-is. Add `--porcelain` for one record per line with
|
|
248
|
+
`U+001F` between fields (a spec title with a space in it never misaligns a column), or `--json`
|
|
249
|
+
for the native document. Both reuse the dashboard's own readers, so the board and the CLI can
|
|
250
|
+
never give two answers about one repo.
|
|
251
|
+
|
|
252
|
+
`--panel` — on `specs`, `doctor` and `metrics` — emits the payload shape a
|
|
253
|
+
[Francois](https://github.com/antoine-gmnz/francois) extension panel expects. It is the one
|
|
254
|
+
Francois-aware surface in the package, and it exists for
|
|
255
|
+
[**francois-plugin-cohorte**](https://github.com/TheBidouilleAgency/francois-plugin-cohorte): a
|
|
256
|
+
manifest-only extension that renders the spec board, the doctor report and the 30-day cost as
|
|
257
|
+
three panels beside your sessions.
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
npm i -g cohorte # a Francois extension may only spawn a bare
|
|
261
|
+
francois ext install TheBidouilleAgency/cohorte # binary on PATH — never npx, never a shell
|
|
262
|
+
```
|
|
263
|
+
|
|
237
264
|
## Releasing (maintainers)
|
|
238
265
|
|
|
239
266
|
Versions are tracked with npm semver — the published package is the release artifact.
|
package/bin/cli.js
CHANGED
|
@@ -37,6 +37,8 @@ Usage:
|
|
|
37
37
|
cohorte update [target] [--global] [--runtime=a,b | --all-runtimes]
|
|
38
38
|
cohorte dashboard [target] [--port=N] [--host=ADDR] [--open]
|
|
39
39
|
cohorte metrics [target] [--days=N] [--since=ISO] [--runs] [--json]
|
|
40
|
+
cohorte specs [target] [--porcelain | --json | --panel]
|
|
41
|
+
cohorte doctor [target] [--porcelain | --json | --panel]
|
|
40
42
|
cohorte version
|
|
41
43
|
|
|
42
44
|
Commands:
|
|
@@ -56,8 +58,18 @@ Commands:
|
|
|
56
58
|
~/.claude/projects — nothing to enable, and it covers runs that
|
|
57
59
|
already happened. Worktree-aware, so a feature adds up. --json for
|
|
58
60
|
the raw rollup, --runs to include every individual invocation.
|
|
61
|
+
specs The spec board of <target>: id, status, branch, title, read from the
|
|
62
|
+
frontmatter of specs/*.md. --porcelain for one record per line with
|
|
63
|
+
U+001F between fields, --json for the list.
|
|
64
|
+
doctor The same checks /cohorte-doctor runs (core, pointer, profile, agents,
|
|
65
|
+
hook, gate, retrieval, design, isolation, specs), without a coding
|
|
66
|
+
agent in the loop. Exits 1 when any check is bad, 0 otherwise, so it
|
|
67
|
+
drops into CI. --porcelain / --json as above.
|
|
59
68
|
version Print the installed CLI version.
|
|
60
69
|
|
|
70
|
+
--panel on specs, doctor and metrics emits the payload a Francois extension
|
|
71
|
+
panel expects (github.com/TheBidouilleAgency/francois-plugin-cohorte).
|
|
72
|
+
|
|
61
73
|
Runtimes (--runtime=): ${adapter.listRuntimes().join(', ')}
|
|
62
74
|
The pipeline's doctrine is one set of source prompts; the installer renders them into
|
|
63
75
|
whatever each coding agent reads (markdown + frontmatter, plain markdown, or TOML) and
|
|
@@ -86,13 +98,19 @@ const metricsFlags = [];
|
|
|
86
98
|
const isMetricsFlag = (a) =>
|
|
87
99
|
a === '--json' || a === '--runs' || a.startsWith('--days=') || a.startsWith('--since=');
|
|
88
100
|
|
|
101
|
+
// How `specs`, `doctor` and `metrics` render. One variable, not one flag per command,
|
|
102
|
+
// so `--porcelain` never means two different things depending on where it sits.
|
|
103
|
+
let format = 'human';
|
|
104
|
+
|
|
89
105
|
// Which coding agents to install for. Empty ⇒ resolved later (detect, then ask on a TTY,
|
|
90
106
|
// then fall back to claude — the only behaviour that existed before 2.2.0).
|
|
91
107
|
let wantRuntimes = [];
|
|
92
108
|
|
|
93
109
|
for (const a of args) {
|
|
94
|
-
if (a === 'install' || a === 'update' || a === 'dashboard' || a === 'metrics'
|
|
95
|
-
|
|
110
|
+
if (a === 'install' || a === 'update' || a === 'dashboard' || a === 'metrics'
|
|
111
|
+
|| a === 'specs' || a === 'doctor') mode = a;
|
|
112
|
+
else if (a === '--porcelain' || a === '--panel') format = a.slice(2);
|
|
113
|
+
else if (isMetricsFlag(a)) { if (a === '--json') format = 'json'; metricsFlags.push(a); }
|
|
96
114
|
else if (a === '--all-runtimes') wantRuntimes = adapter.listRuntimes();
|
|
97
115
|
else if (a === '--runtimes' || a === '--runtime') {
|
|
98
116
|
console.error('error: --runtime needs a value, e.g. --runtime=codex,cursor'); process.exit(2);
|
|
@@ -136,10 +154,45 @@ if (mode === 'dashboard') {
|
|
|
136
154
|
return;
|
|
137
155
|
}
|
|
138
156
|
|
|
157
|
+
// --- specs / doctor: read-only reports on <target> ---------------------------
|
|
158
|
+
// Both reuse the dashboard's own readers, so the board and the CLI can never drift
|
|
159
|
+
// into two answers about the same repo. Nothing here writes or spawns anything.
|
|
160
|
+
if (mode === 'specs' || mode === 'doctor') {
|
|
161
|
+
const report = require('./report.js');
|
|
162
|
+
const { state, scanSpecs } = require('../dashboard/server/doctor.js');
|
|
163
|
+
const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
164
|
+
|
|
165
|
+
if (mode === 'specs') {
|
|
166
|
+
const records = report.specRecords(target, scanSpecs);
|
|
167
|
+
if (format === 'json') console.log(JSON.stringify({ project: target, specs: records }, null, 2));
|
|
168
|
+
else if (format === 'porcelain') { if (records.length) console.log(report.specsPorcelain(records)); }
|
|
169
|
+
else if (format === 'panel') console.log(report.specsPanel(records));
|
|
170
|
+
else console.log(report.specsHuman(records));
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// `state` is async (it probes versions), so this branch owns the whole tail.
|
|
175
|
+
state({ projectRoot: target, globalDir, cliVersion: VERSION }).then((s) => {
|
|
176
|
+
const records = report.checkRecords(s);
|
|
177
|
+
if (format === 'json') console.log(JSON.stringify(s, null, 2));
|
|
178
|
+
else if (format === 'porcelain') { if (records.length) console.log(report.doctorPorcelain(records)); }
|
|
179
|
+
else if (format === 'panel') console.log(report.doctorPanel(records));
|
|
180
|
+
else console.log(report.doctorHuman(s, records));
|
|
181
|
+
// A bad check is a failure the shell can branch on; warn and skip are not.
|
|
182
|
+
process.exit(s.summary.bad > 0 && format !== 'panel' ? 1 : 0);
|
|
183
|
+
}).catch((e) => {
|
|
184
|
+
console.error(`error: ${e && e.message ? e.message : e}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
139
190
|
// --- metrics: cost + runtime per command ------------------------------------
|
|
140
191
|
// The collector is ESM and this CLI is CommonJS, so it runs as a child process rather
|
|
141
192
|
// than being required. stdio is inherited so --json stays pipeable.
|
|
142
193
|
if (mode === 'metrics') {
|
|
194
|
+
// `--panel` is not in isMetricsFlag (it is not metrics-specific), so forward it here.
|
|
195
|
+
if (format === 'panel') metricsFlags.push('--panel');
|
|
143
196
|
const script = path.join(pkgRoot, 'scripts', 'metrics', 'collect.mjs');
|
|
144
197
|
if (!fs.existsSync(script)) {
|
|
145
198
|
console.error(`error: metrics collector not found at ${script}`);
|
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
|
+
};
|
|
@@ -59,6 +59,33 @@ them over Grep/Glob + whole-file Reads**: locate code by symbol, read only the d
|
|
|
59
59
|
and trace references before changing any shared shape. Fall back to Grep/Read only when the retrieval
|
|
60
60
|
tools are unavailable or come up empty.
|
|
61
61
|
|
|
62
|
+
## How you choose what to write — the minimality ladder
|
|
63
|
+
|
|
64
|
+
The spec froze the **what**; this ladder governs only the **how**. It never licenses you to skip a
|
|
65
|
+
contract field, an acceptance criterion, a test, a validation, an authz check or an accessibility
|
|
66
|
+
attribute — those are the *what*, and they are not yours to trim.
|
|
67
|
+
|
|
68
|
+
Before writing any helper, utility, wrapper, abstraction or new dependency, walk down and stop at the
|
|
69
|
+
first hit:
|
|
70
|
+
|
|
71
|
+
1. **Does it need to exist at all?** An abstraction with one implementation, a config nobody sets, a
|
|
72
|
+
layer with one caller — don't write it. The second caller is when it earns its keep.
|
|
73
|
+
2. **Is it already in this repo?** One retrieval/Grep lookup by symbol name, not a survey — you are
|
|
74
|
+
checking, not exploring. Reuse beats re-implementing, and it keeps the convention.
|
|
75
|
+
3. **Is it in the standard library / framework?** Name it and use it.
|
|
76
|
+
4. **Is it a native platform feature?** (CSS, the HTTP layer, the DB, the runtime.) Prefer it over code.
|
|
77
|
+
5. **Is it in a dependency already installed?** Use that one. Adding a dependency for what tiers 3–5
|
|
78
|
+
already ship is a finding at review.
|
|
79
|
+
6. **Can it be a few lines inline?** Then it doesn't need a file, a class, or a name.
|
|
80
|
+
7. Only then: the **minimum implementation that satisfies the contract** — no speculative options, no
|
|
81
|
+
"we'll probably need" parameters, no premature generalisation.
|
|
82
|
+
|
|
83
|
+
Bound the cost: this is at most **one lookup per candidate**, and it applies to code you are inventing —
|
|
84
|
+
never to code the contract dictates. If a step would cost more searching than writing, write it.
|
|
85
|
+
|
|
86
|
+
Something you deliberately kept simple with a known ceiling goes in your handoff `## TODO / not done`
|
|
87
|
+
with its limit and what would trigger the upgrade — not in a comment, and not silently.
|
|
88
|
+
|
|
62
89
|
## How you work — strict TDD (red → green → refactor)
|
|
63
90
|
|
|
64
91
|
<!-- <SURFACE_TDD_STEP1> is a LEAD-IN paragraph, not a numbered item: it is filled only for a
|
package/core/agents/review.md
CHANGED
|
@@ -63,6 +63,20 @@ be precise and self-contained.
|
|
|
63
63
|
additive `sm:/md:/lg:`, no fixed widths that break on mobile. (You can't render; judge from the code.)
|
|
64
64
|
7. **TDD coverage.** Each surface's tests cover its slice of the contract (statuses, validation, auth,
|
|
65
65
|
behavior). Flag untested contract surface.
|
|
66
|
+
8. **Over-engineering (lowest priority, never blocking).** Code the diff *added* that didn't need to
|
|
67
|
+
exist. Tag each one and always name the replacement — a finding with no cheaper alternative is an
|
|
68
|
+
opinion, not a finding:
|
|
69
|
+
- `delete:` dead code, unused flexibility, a speculative feature nothing calls. Replacement: nothing.
|
|
70
|
+
- `stdlib:` hand-rolled thing the standard library or framework ships. Name the function.
|
|
71
|
+
- `native:` a dependency or code doing what the platform already does. Name the feature.
|
|
72
|
+
- `yagni:` abstraction with one implementation, config nobody sets, layer with one caller.
|
|
73
|
+
- `shrink:` same behaviour, materially fewer lines. Name the shorter form.
|
|
74
|
+
|
|
75
|
+
**Hard limits on this axis.** It is capped at **5 findings**, biggest cut first, and its severity
|
|
76
|
+
ceiling is **MEDIUM** — it can never produce CRITICAL, never REVISE, never BLOCK. Test code,
|
|
77
|
+
fixtures and anything the contract or an acceptance criterion mandates are **out of bounds**:
|
|
78
|
+
coverage is not bloat, and "simpler" is never a reason to drop a spec'd behaviour. Deduplication
|
|
79
|
+
that would cross a surface boundary is out of bounds too — that's an architecture call, not a review one.
|
|
66
80
|
|
|
67
81
|
## Language checks (apply only those matching the surfaces under review)
|
|
68
82
|
|
|
@@ -89,10 +103,19 @@ entry point / module with **no test**), and the lint/format/type debt staged at
|
|
|
89
103
|
dispatch names (`specs/reports/audit-gates.txt`). Emit a **prioritized refactor backlog grouped by
|
|
90
104
|
domain** (same finding-line shape) instead of a SHIP/REVISE/BLOCK verdict.
|
|
91
105
|
|
|
106
|
+
In audit mode the over-engineering axis (§8) widens: there is no diff, so the whole target is in
|
|
107
|
+
scope and the 5-finding cap lifts to **10 per domain**, ranked biggest cut first. Hunt the usual
|
|
108
|
+
shapes — deps the stdlib or platform already ships, single-implementation interfaces, factories with
|
|
109
|
+
one product, wrappers that only delegate, dead flags and config, hand-rolled stdlib. Close the
|
|
110
|
+
audit-mode report with one line: `net: -<N> lines, -<M> deps possible.` (`0`/`0` is a valid answer —
|
|
111
|
+
say it rather than inventing cuts).
|
|
112
|
+
|
|
92
113
|
## Severity & verdict
|
|
93
114
|
|
|
94
115
|
- **CRITICAL** — spec violation or correctness bug that must be fixed ⇒ verdict **REVISE**.
|
|
95
116
|
- **HIGH / MEDIUM / LOW** — quality/convention issues; note them.
|
|
117
|
+
- **Over-engineering (§8) caps at MEDIUM** and never drives the verdict — a diff whose only findings
|
|
118
|
+
are `complexity` ships. It is a cleanup signal, not a gate.
|
|
96
119
|
- Any **security vulnerability** ⇒ verdict **BLOCK**.
|
|
97
120
|
- No CRITICAL and no security issue ⇒ verdict **SHIP**.
|
|
98
121
|
|
|
@@ -117,7 +140,7 @@ they are **never lost and never cost a fix loop**.
|
|
|
117
140
|
## Your return — the REVIEW REPORT, exactly this shape
|
|
118
141
|
|
|
119
142
|
Every finding must be **self-sufficient for a stateless agent**: `file:line` · severity ·
|
|
120
|
-
`spec-violation | quality | security` · one concrete suggested fix — it gets appended verbatim to the
|
|
143
|
+
`spec-violation | quality | security | complexity` · one concrete suggested fix — it gets appended verbatim to the
|
|
121
144
|
spec's `## Remediation`. Your final message **is** the report. **The shape is capped:** at most
|
|
122
145
|
**20 findings**, ONE line each, **zero code excerpts** (the diff and the source are on disk — a
|
|
123
146
|
`file:line` is enough for a stateless fixer). More than 20? Keep every CRITICAL/HIGH/security
|
|
@@ -140,12 +163,13 @@ Verdict: <SHIP | REVISE | BLOCK>
|
|
|
140
163
|
|
|
141
164
|
## Findings
|
|
142
165
|
|
|
143
|
-
- **[<SEVERITY>]** `<file>:<line>` · <spec-violation|quality|security> · <problem> → **Fix:** <concrete change>
|
|
144
|
-
(order by severity; "None." if none; max 20 lines, one per finding, no code excerpts
|
|
166
|
+
- **[<SEVERITY>]** `<file>:<line>` · <spec-violation|quality|security|complexity> · <problem> → **Fix:** <concrete change>
|
|
167
|
+
(order by severity; "None." if none; max 20 lines, one per finding, no code excerpts.
|
|
168
|
+
`complexity` lines carry their §8 tag in the problem — `yagni: <what>` — and cap at 5)
|
|
145
169
|
|
|
146
170
|
## Deferred
|
|
147
171
|
|
|
148
|
-
- **[<SEVERITY>]** `<file>:<line>` · <quality|security> · <problem> → **Fix:** <concrete change> · out of scope: <why this feature does not own it>
|
|
172
|
+
- **[<SEVERITY>]** `<file>:<line>` · <quality|security|complexity> · <problem> → **Fix:** <concrete change> · out of scope: <why this feature does not own it>
|
|
149
173
|
(real but out of this feature's scope — see §Deferred; worst first; "None." if none; max 10 lines)
|
|
150
174
|
|
|
151
175
|
## Notes
|
|
@@ -32,8 +32,9 @@ bulk never sits in your history, then grep it for the `file:line` of every failu
|
|
|
32
32
|
|
|
33
33
|
Dispatch `review` (read-only; static prompt first, variable slot last — prompt-cache prefix):
|
|
34
34
|
"Audit a target against `PIPELINE.md` (no spec — **audit mode**). Check conventions (§Conventions
|
|
35
|
-
per surface), TDD coverage (untested entry points / modules per surface),
|
|
36
|
-
|
|
35
|
+
per surface), TDD coverage (untested entry points / modules per surface), over-engineering (§8,
|
|
36
|
+
widened per your audit-mode rules — ranked biggest cut first, closing `net:` line), and — if the
|
|
37
|
+
profile enables them — mobile-first + design-system usage. Mechanical findings from the gates: read
|
|
37
38
|
`specs/reports/audit-gates.txt`. Emit a prioritized refactor backlog (capped finding-line format
|
|
38
39
|
from your instructions), grouped by domain (one group per surface + shared). — Target: `$ARGUMENTS`
|
|
39
40
|
(default: whole repo)."
|
|
@@ -42,7 +43,11 @@ from your instructions), grouped by domain (one group per surface + shared). —
|
|
|
42
43
|
|
|
43
44
|
Merge mechanical + convention findings into one prioritized backlog and **write
|
|
44
45
|
`specs/refactor-backlog.md`**, grouped by domain, each item:
|
|
45
|
-
`- [ ] <SEVERITY> · <file:line> · <rule|tdd|lint|format|type|security> · <concrete fix>`
|
|
46
|
+
`- [ ] <SEVERITY> · <file:line> · <rule|tdd|lint|format|type|security|complexity> · <concrete fix>`
|
|
47
|
+
|
|
48
|
+
`complexity` items are the over-engineering cuts, ranked biggest first inside their domain — they are
|
|
49
|
+
the cheapest wins in the backlog (deleting code needs no new tests) but they never outrank a
|
|
50
|
+
security, correctness or `tdd` item.
|
|
46
51
|
|
|
47
52
|
**Carry over the deferred items** before overwriting: `grep -n 'deferred:' specs/refactor-backlog.md`
|
|
48
53
|
and re-emit every **open** (`- [ ]`) match verbatim under its domain, tag included. Those lines were
|
|
@@ -165,7 +165,7 @@ exactly the leak this step closes. Append each merged `## Deferred` item to
|
|
|
165
165
|
`/cohorte-refactor <domain>` picks them up with no extra plumbing):
|
|
166
166
|
|
|
167
167
|
```
|
|
168
|
-
- [ ] <SEVERITY> · <file:line> · <quality|security|rule> · <concrete fix> · deferred:$ARGUMENTS
|
|
168
|
+
- [ ] <SEVERITY> · <file:line> · <quality|security|rule|complexity> · <concrete fix> · deferred:$ARGUMENTS
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
- **Never into the spec's `## Remediation`** — that list is what `/cohorte-fix` re-dispatches, so a
|
package/core/workflows/review.js
CHANGED
|
@@ -125,7 +125,7 @@ const FINDING = {
|
|
|
125
125
|
properties: {
|
|
126
126
|
severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
|
|
127
127
|
file: { type: 'string' }, line: { type: 'integer' },
|
|
128
|
-
kind: { enum: ['spec-violation', 'quality', 'security'] },
|
|
128
|
+
kind: { enum: ['spec-violation', 'quality', 'security', 'complexity'] },
|
|
129
129
|
problem: { type: 'string', description: 'one line, no code excerpts' },
|
|
130
130
|
fix: { type: 'string', description: 'one concrete change, one line' },
|
|
131
131
|
},
|
|
@@ -141,7 +141,7 @@ const DEFERRED = {
|
|
|
141
141
|
properties: {
|
|
142
142
|
severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
|
|
143
143
|
file: { type: 'string' }, line: { type: 'integer' },
|
|
144
|
-
kind: { enum: ['quality', 'security', 'rule'] },
|
|
144
|
+
kind: { enum: ['quality', 'security', 'rule', 'complexity'] },
|
|
145
145
|
problem: { type: 'string', description: 'one line, no code excerpts' },
|
|
146
146
|
fix: { type: 'string', description: 'one concrete change, one line' },
|
|
147
147
|
outOfScope: { type: 'string', description: 'one line: why this feature does not own it' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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 };
|