cohorte 1.2.5 → 1.3.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 +58 -0
- package/README.md +44 -9
- package/bin/cli.js +13 -3
- package/core/agents/implementer.template.md +27 -14
- package/core/agents/profile-reader.md +28 -0
- package/core/agents/review.md +7 -3
- package/core/agents/smoke.md +4 -2
- package/core/commands/audit.md +17 -9
- package/core/commands/doctor.md +21 -5
- package/core/commands/refactor.md +8 -3
- package/core/commands/review.md +46 -18
- package/core/commands/smoke.md +23 -9
- package/core/commands/update-pipeline.md +10 -4
- package/core/hooks/gate.py +95 -6
- package/core/templates/agent-handoff.md +2 -1
- package/core/templates/review-feedback.md +4 -1
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +7 -0
- package/core/templates/steps/init-pipeline/04-write-render.md +16 -8
- package/core/workflows/audit.js +144 -0
- package/core/workflows/cycle.js +397 -0
- package/core/workflows/refactor.js +187 -0
- package/core/workflows/review.js +215 -0
- package/dashboard/dist/assets/{index-YvkzH-yF.js → index-BxgA_mz1.js} +10 -10
- package/dashboard/dist/index.html +1 -1
- package/dashboard/server/doctor.js +35 -1
- package/dashboard/server/index.js +4 -2
- package/install.ps1 +4 -1
- package/install.sh +5 -2
- package/package.json +1 -1
- package/profile/PIPELINE.template.md +17 -0
- package/profile/SCHEMA.md +113 -1
- package/scripts/preflight.sh +48 -0
- package/scripts/validate-core.mjs +39 -3
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// cohorte — /review as a deterministic workflow (opt-in; the conversational
|
|
2
|
+
// /review command remains the default path and the fallback).
|
|
3
|
+
//
|
|
4
|
+
// Invoke with args = {feature: "<feature_id>"} (or the bare feature id string).
|
|
5
|
+
// Requires Claude Code >= 2.1.154 with workflows enabled — /doctor reports this.
|
|
6
|
+
//
|
|
7
|
+
// Shape (SCHEMA.md §Workflows): phase 0 reads the profile through the
|
|
8
|
+
// profile-reader agent (scripts have no filesystem or shell), the deterministic
|
|
9
|
+
// preflight aborts while red (zero reviewers spawned), the diff is staged ONCE
|
|
10
|
+
// per touched surface, one reviewer runs per surface in parallel, an adversarial
|
|
11
|
+
// cross-check tries to refute every CRITICAL/security finding, and only the
|
|
12
|
+
// merged verdict comes back — the full report is staged to specs/reports/.
|
|
13
|
+
// Mechanical phases run on haiku; reviewers run the pinned `review` agent (sonnet).
|
|
14
|
+
|
|
15
|
+
export const meta = {
|
|
16
|
+
name: 'cohorte-review',
|
|
17
|
+
description: 'Review a cohorte feature: preflight gate, one reviewer per touched surface, adversarial cross-check, merged verdict only',
|
|
18
|
+
whenToUse: 'Only when the human explicitly asks for the review workflow of a cohorte feature. args = {feature: "<feature_id>"}.',
|
|
19
|
+
phases: [
|
|
20
|
+
{ title: 'Profile', detail: 'PIPELINE.md → JSON via profile-reader', model: 'haiku' },
|
|
21
|
+
{ title: 'Preflight', detail: 'typecheck + lint + tests — abort while red', model: 'haiku' },
|
|
22
|
+
{ title: 'Stage', detail: 'git diff --stat once, per-surface hunks to disk', model: 'haiku' },
|
|
23
|
+
{ title: 'Review', detail: 'one review agent per touched surface' },
|
|
24
|
+
{ title: 'Cross-check', detail: 'adversarial refutation of CRITICAL/security findings' },
|
|
25
|
+
{ title: 'Merge', detail: 'stage merged report + metrics; return the verdict', model: 'haiku' },
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const feature = typeof args === 'string' ? args.trim() : args && args.feature
|
|
30
|
+
if (!feature) throw new Error('cohorte-review needs args = {feature: "<feature_id>"}')
|
|
31
|
+
|
|
32
|
+
const PROFILE = { type: 'object', additionalProperties: true }
|
|
33
|
+
|
|
34
|
+
const PREFLIGHT = {
|
|
35
|
+
type: 'object', required: ['pass'], additionalProperties: false,
|
|
36
|
+
properties: {
|
|
37
|
+
pass: { type: 'boolean' },
|
|
38
|
+
tail: { type: 'string', description: 'on failure: the raw last lines the script printed' },
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const STAGE = {
|
|
43
|
+
type: 'object', required: ['surfaces'], additionalProperties: false,
|
|
44
|
+
properties: {
|
|
45
|
+
surfaces: {
|
|
46
|
+
type: 'array',
|
|
47
|
+
items: {
|
|
48
|
+
type: 'object', required: ['key', 'diff', 'files'], additionalProperties: false,
|
|
49
|
+
properties: {
|
|
50
|
+
key: { type: 'string' },
|
|
51
|
+
diff: { type: 'string', description: 'path of the staged .diff file' },
|
|
52
|
+
files: { type: 'array', items: { type: 'string' }, description: 'changed files (from --stat)' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const FINDING = {
|
|
60
|
+
type: 'object', required: ['severity', 'file', 'line', 'kind', 'problem', 'fix'],
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
properties: {
|
|
63
|
+
severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
|
|
64
|
+
file: { type: 'string' }, line: { type: 'integer' },
|
|
65
|
+
kind: { enum: ['spec-violation', 'quality', 'security'] },
|
|
66
|
+
problem: { type: 'string', description: 'one line, no code excerpts' },
|
|
67
|
+
fix: { type: 'string', description: 'one concrete change, one line' },
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const REPORT = {
|
|
72
|
+
type: 'object', required: ['verdict', 'findings'], additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
verdict: { enum: ['SHIP', 'REVISE', 'BLOCK'] },
|
|
75
|
+
findings: { type: 'array', maxItems: 20, items: FINDING },
|
|
76
|
+
overflow: { type: 'integer', description: 'findings beyond the 20-item cap, if any' },
|
|
77
|
+
notes: { type: 'string', description: 'RBAC / mobile-first assessment only, when the profile enables them' },
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const VERDICT = {
|
|
82
|
+
type: 'object', required: ['refuted', 'reason'], additionalProperties: false,
|
|
83
|
+
properties: {
|
|
84
|
+
refuted: { type: 'boolean' },
|
|
85
|
+
reason: { type: 'string', description: 'one line: why the finding does or does not hold' },
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Phase 0 — profile ────────────────────────────────────────────────────────
|
|
90
|
+
phase('Profile')
|
|
91
|
+
const profile = await agent(
|
|
92
|
+
'Return this project\'s PIPELINE.md `yaml pipeline-profile` block as JSON, per your instructions.',
|
|
93
|
+
{ agentType: 'profile-reader', label: 'profile', schema: PROFILE, effort: 'low' },
|
|
94
|
+
)
|
|
95
|
+
if (!profile || profile.error) {
|
|
96
|
+
return { verdict: 'ABORTED', reason: `profile unreadable: ${(profile && profile.error) || 'profile-reader returned nothing'}` }
|
|
97
|
+
}
|
|
98
|
+
const cmds = profile.commands || {}
|
|
99
|
+
const base = (profile.vcs && profile.vcs.default_branch) || 'main'
|
|
100
|
+
const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
|
|
101
|
+
const quiet = (q, full) => (q && !String(q).startsWith('<') ? q : full ? `${full} 2>&1 | tail -40` : '')
|
|
102
|
+
const checks = [cmds.typecheck, quiet(cmds.lint_quiet, cmds.lint), quiet(cmds.test_quiet, cmds.test)]
|
|
103
|
+
.filter(c => c && !String(c).startsWith('<'))
|
|
104
|
+
|
|
105
|
+
// ── Phase 1 — deterministic preflight (abort while red, zero reviewers) ─────
|
|
106
|
+
phase('Preflight')
|
|
107
|
+
const pre = await agent(
|
|
108
|
+
`Run the cohorte deterministic pre-flight for feature ${feature} in ONE Bash call:\n` +
|
|
109
|
+
`<core>/pipeline/scripts/preflight.sh specs/reports/${feature}.preflight.txt ` +
|
|
110
|
+
checks.map(c => JSON.stringify(c)).join(' ') + '\n' +
|
|
111
|
+
'(<core> = .claude if .claude/pipeline/scripts/preflight.sh exists, else ~/.claude — probe with test -x. ' +
|
|
112
|
+
'Script absent on both: run the quoted commands yourself, each appended to the same report file, stopping at the first failure.) ' +
|
|
113
|
+
'Return pass=true only on a fully green run. On failure set pass=false and put the raw last 40 lines of the report in `tail` — verbatim, no summarizing.',
|
|
114
|
+
{ model: 'haiku', label: 'preflight', schema: PREFLIGHT, effort: 'low' },
|
|
115
|
+
)
|
|
116
|
+
if (!pre || !pre.pass) {
|
|
117
|
+
return {
|
|
118
|
+
verdict: 'ABORTED',
|
|
119
|
+
reason: 'preflight red — fix the mechanical failures (or run /fix) before any review; no reviewer was spawned',
|
|
120
|
+
failures: (pre && pre.tail) || 'preflight agent returned nothing',
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Phase 2 — stage the diff once ────────────────────────────────────────────
|
|
125
|
+
phase('Stage')
|
|
126
|
+
const surfaceList = surfaces.map(s => `${s.key} → ${s.path}`).join(' · ') || '(none declared)'
|
|
127
|
+
const staged = await agent(
|
|
128
|
+
`Stage the review inputs for cohorte feature ${feature}. Diff base: ${base}. Surfaces: ${surfaceList}.\n` +
|
|
129
|
+
`1. ONE call: git diff ${base} --stat > specs/reports/${feature}.stat.txt — never print the diff.\n` +
|
|
130
|
+
'2. Group the changed paths by surface path prefix; paths under no surface are the `shared` remainder — attach them to the most relevant surface.\n' +
|
|
131
|
+
`3. Per surface that has changed paths (ONLY those): git diff ${base} -- <surface.path> [<remainder pathspecs>] > specs/reports/${feature}.<key>.diff\n` +
|
|
132
|
+
'4. Return the touched surfaces with their staged diff path and changed-file list. No changed paths at all ⇒ return an empty surfaces array.',
|
|
133
|
+
{ model: 'haiku', label: 'stage-diff', schema: STAGE, effort: 'low' },
|
|
134
|
+
)
|
|
135
|
+
const touched = (staged && staged.surfaces) || []
|
|
136
|
+
if (!touched.length) return { verdict: 'SHIP', reason: `no diff against ${base} — nothing to review`, findings: 0 }
|
|
137
|
+
log(`Touched surfaces: ${touched.map(s => s.key).join(', ')}`)
|
|
138
|
+
|
|
139
|
+
// ── Phases 3+4 — review each surface, cross-check its hard findings ─────────
|
|
140
|
+
// pipeline(): a fast surface's cross-check starts while a slow one still reviews.
|
|
141
|
+
const reviewed = await pipeline(
|
|
142
|
+
touched,
|
|
143
|
+
s => agent(
|
|
144
|
+
'Review one feature surface against its frozen spec, per your agent instructions (read the staged ' +
|
|
145
|
+
'diff FIRST; open a full source file only when a finding demands it; capped shape — max 20 findings, ' +
|
|
146
|
+
'one line each, no code excerpts). — Variable slots: ' +
|
|
147
|
+
`feature ${feature} · scope: the ${s.key} surface only · spec: specs/${feature}.md · ` +
|
|
148
|
+
`staged diff: ${s.diff} · changed files: ${s.files.join(', ')}`,
|
|
149
|
+
{ agentType: 'review', label: `review:${s.key}`, phase: 'Review', schema: REPORT },
|
|
150
|
+
),
|
|
151
|
+
async (report, s) => {
|
|
152
|
+
if (!report) return null
|
|
153
|
+
const hard = report.findings.filter(f => f.severity === 'CRITICAL' || f.kind === 'security')
|
|
154
|
+
const rest = report.findings.filter(f => !hard.includes(f))
|
|
155
|
+
if (!hard.length) return { key: s.key, report, kept: rest, refuted: [] }
|
|
156
|
+
const votes = await parallel(hard.map(f => () => agent(
|
|
157
|
+
'Adversarially verify ONE review finding — your job is to REFUTE it if you can. Read the staged ' +
|
|
158
|
+
'diff and the exact file:line; refuted=true when the code, a guard, a test, or the spec shows the ' +
|
|
159
|
+
'finding does not hold. Uncertain ⇒ refuted=false (a real CRITICAL must not die on doubt). — ' +
|
|
160
|
+
`Finding: [${f.severity}/${f.kind}] ${f.file}:${f.line} — ${f.problem} (proposed fix: ${f.fix}). ` +
|
|
161
|
+
`Feature ${feature} · spec: specs/${feature}.md · staged diff: ${s.diff}`,
|
|
162
|
+
{ agentType: 'review', label: `verify:${s.key}`, phase: 'Cross-check', schema: VERDICT },
|
|
163
|
+
).then(v => ({ f, refuted: !!(v && v.refuted), reason: v ? v.reason : 'verifier died — kept' }))))
|
|
164
|
+
const checkedVotes = votes.filter(Boolean)
|
|
165
|
+
return {
|
|
166
|
+
key: s.key,
|
|
167
|
+
report,
|
|
168
|
+
kept: rest.concat(checkedVotes.filter(v => !v.refuted).map(v => v.f)),
|
|
169
|
+
refuted: checkedVotes.filter(v => v.refuted).map(v => ({ ...v.f, reason: v.reason })),
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const results = reviewed.filter(Boolean)
|
|
175
|
+
const kept = results.flatMap(r => r.kept.map(f => ({ ...f, surface: r.key })))
|
|
176
|
+
const refuted = results.flatMap(r => r.refuted.map(f => ({ ...f, surface: r.key })))
|
|
177
|
+
const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
|
|
178
|
+
for (const f of kept) counts[f.severity] = (counts[f.severity] || 0) + 1
|
|
179
|
+
// Verdict from the findings that SURVIVED the cross-check (a refuted CRITICAL
|
|
180
|
+
// must not force a fix loop): security ⇒ BLOCK, CRITICAL ⇒ REVISE, else SHIP.
|
|
181
|
+
const verdict = kept.some(f => f.kind === 'security') ? 'BLOCK'
|
|
182
|
+
: kept.some(f => f.severity === 'CRITICAL') ? 'REVISE' : 'SHIP'
|
|
183
|
+
|
|
184
|
+
// ── Phase 5 — stage the merged report; only the verdict leaves the workflow ──
|
|
185
|
+
phase('Merge')
|
|
186
|
+
const findingLine = f =>
|
|
187
|
+
`- [${f.severity}] \`${f.file}:${f.line}\` · ${f.kind} · ${f.problem} → **Fix:** ${f.fix}`
|
|
188
|
+
const reportBody = [
|
|
189
|
+
'# REVIEW REPORT', `feature_id: ${feature} · merged by cohorte-review workflow`, '',
|
|
190
|
+
'| Severity | Count |', '| -------- | ----- |',
|
|
191
|
+
...['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(s => `| ${s} | ${counts[s] || 0} |`), '',
|
|
192
|
+
`Verdict: ${verdict}`, '', '## Findings', '',
|
|
193
|
+
kept.length ? kept.map(findingLine).join('\n') : 'None.',
|
|
194
|
+
...(refuted.length ? ['', '## Refuted by cross-check (no action needed)', '',
|
|
195
|
+
refuted.map(f => `- ${f.file}:${f.line} · ${f.problem} — refuted: ${f.reason}`).join('\n')] : []),
|
|
196
|
+
].join('\n')
|
|
197
|
+
await agent(
|
|
198
|
+
`Stage a cohorte review report and its metrics, mechanically:\n` +
|
|
199
|
+
`1. Write EXACTLY this content to specs/reports/${feature}.md (overwrite):\n<<<REPORT\n${reportBody}\nREPORT\n` +
|
|
200
|
+
`2. Append one line to $(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl: ` +
|
|
201
|
+
`{"ts":"<ISO now>","feature":"${feature}","phase":"review","seconds":0,"surfaces":{${results.map(r => `"${r.key}":"${verdict}:${r.kept.length}"`).join(',')}}}\n` +
|
|
202
|
+
`3. Chain the opt-in usage ping: <core>/pipeline/scripts/telemetry-send.sh review "${feature}" 0 "${verdict}:${kept.length}" || true\n` +
|
|
203
|
+
'Return the single word: done.',
|
|
204
|
+
{ model: 'haiku', label: 'stage-report', effort: 'low' },
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
verdict,
|
|
209
|
+
counts,
|
|
210
|
+
refutedByCrossCheck: refuted.length,
|
|
211
|
+
criticals: kept.filter(f => f.severity === 'CRITICAL' || f.kind === 'security')
|
|
212
|
+
.map(f => `[${f.surface}] ${f.file}:${f.line} — ${f.problem}`),
|
|
213
|
+
report: `specs/reports/${feature}.md`,
|
|
214
|
+
next: verdict === 'SHIP' ? `/ship ${feature} (after DoD ticks)` : `/fix ${feature}`,
|
|
215
|
+
}
|