cohorte 2.10.1 → 3.0.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/assets/manifest.json +193 -0
- package/assets/migrations/config/README.md +12 -0
- package/assets/migrations/state/0001_init.sql +154 -0
- package/assets/prompts/README.md +11 -0
- package/assets/prompts/agents/fixer.md +7 -0
- package/assets/prompts/agents/implementer.md +12 -0
- package/assets/prompts/agents/reviewer.md +8 -0
- package/assets/prompts/agents/security-reviewer.md +7 -0
- package/assets/prompts/phases/build.md +5 -0
- package/assets/prompts/phases/review.md +5 -0
- package/assets/prompts/phases/test.md +5 -0
- package/assets/prompts/system.md +11 -0
- package/assets/schemas/agent-output.schema.json +225 -0
- package/assets/schemas/auth-status.schema.json +87 -0
- package/assets/schemas/command-result.schema.json +192 -0
- package/assets/schemas/commands.schema.json +952 -0
- package/assets/schemas/config.schema.json +2259 -0
- package/assets/schemas/doctor-report.schema.json +104 -0
- package/assets/schemas/events.schema.json +8514 -0
- package/assets/schemas/fake-script.schema.json +549 -0
- package/assets/schemas/inspect.schema.json +3133 -0
- package/assets/schemas/manifest.schema.json +79 -0
- package/assets/schemas/ownership.schema.json +52 -0
- package/assets/schemas/policy-verdict.schema.json +282 -0
- package/assets/schemas/project-model.schema.json +783 -0
- package/assets/schemas/project-status.schema.json +217 -0
- package/assets/schemas/reconcile-plan.schema.json +154 -0
- package/assets/schemas/run-diff.schema.json +177 -0
- package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
- package/assets/schemas/run-state.schema.json +1755 -0
- package/assets/schemas/runtime-capabilities.schema.json +1545 -0
- package/assets/schemas/sandbox-capabilities.schema.json +116 -0
- package/assets/schemas/skill.schema.json +87 -0
- package/assets/schemas/spec.schema.json +202 -0
- package/assets/schemas/tool-catalogue.schema.json +655 -0
- package/assets/schemas/trust-record.schema.json +38 -0
- package/assets/skills/README.md +9 -0
- package/assets/skills/inspect-surface.md +4 -0
- package/dist/agent-host.mjs +2187 -0
- package/dist/agent-host.mjs.map +1 -0
- package/dist/bundle-manifest.json +104 -0
- package/dist/chunks/__host-DgGPyco9.mjs +18 -0
- package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
- package/dist/chunks/approve-Cv135UqI.mjs +19 -0
- package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
- package/dist/chunks/auth-BT5oVp76.mjs +132 -0
- package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
- package/dist/chunks/auth-CYOh60KG.mjs +93 -0
- package/dist/chunks/auth-CYOh60KG.mjs.map +1 -0
- package/dist/chunks/brainstorm-BwKA6yZP.mjs +7 -0
- package/dist/chunks/brainstorm-BwKA6yZP.mjs.map +1 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
- package/dist/chunks/compose-Dvxunpto.mjs +7307 -0
- package/dist/chunks/compose-Dvxunpto.mjs.map +1 -0
- package/dist/chunks/config-DefHA2Lw.mjs +96 -0
- package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
- package/dist/chunks/contract-BkMjU1kR.mjs +295 -0
- package/dist/chunks/contract-BkMjU1kR.mjs.map +1 -0
- package/dist/chunks/contract-bQ9FJl4N.mjs +123 -0
- package/dist/chunks/contract-bQ9FJl4N.mjs.map +1 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
- package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
- package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
- package/dist/chunks/discover-eS01-me1.mjs +21 -0
- package/dist/chunks/discover-eS01-me1.mjs.map +1 -0
- package/dist/chunks/doctor-B-H2UK5e.mjs +222 -0
- package/dist/chunks/doctor-B-H2UK5e.mjs.map +1 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
- package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
- package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
- package/dist/chunks/init-BUnzIkeE.mjs +63 -0
- package/dist/chunks/init-BUnzIkeE.mjs.map +1 -0
- package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
- package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
- package/dist/chunks/logs-Giq5sUau.mjs +25 -0
- package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
- package/dist/chunks/migrate-DybihKzZ.mjs +33 -0
- package/dist/chunks/migrate-DybihKzZ.mjs.map +1 -0
- package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
- package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
- package/dist/chunks/pause-BASBfKLO.mjs +16 -0
- package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
- package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
- package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
- package/dist/chunks/providers-Dx78imKj.mjs +19 -0
- package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
- package/dist/chunks/reconcile-Bhy3IIYo.mjs +50 -0
- package/dist/chunks/reconcile-Bhy3IIYo.mjs.map +1 -0
- package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
- package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
- package/dist/chunks/retry-CLrUem1H.mjs +15 -0
- package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
- package/dist/chunks/review-BDdjb6kB.mjs +17 -0
- package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
- package/dist/chunks/run-CgTWYlcd.mjs +82 -0
- package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
- package/dist/chunks/scan-8Gy_B6kI.mjs +122 -0
- package/dist/chunks/scan-8Gy_B6kI.mjs.map +1 -0
- package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
- package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
- package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
- package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
- package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
- package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
- package/dist/chunks/src-CId6kMsY.mjs +1689 -0
- package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
- package/dist/chunks/src-CfAOde7C.mjs +5403 -0
- package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
- package/dist/chunks/src-CmFjh4M0.mjs +928 -0
- package/dist/chunks/src-CmFjh4M0.mjs.map +1 -0
- package/dist/chunks/src-Cu3TxNob.mjs +783 -0
- package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
- package/dist/chunks/src-DcU4IXMe.mjs +117 -0
- package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
- package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
- package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
- package/dist/chunks/status-SMjiOMYy.mjs +29 -0
- package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
- package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
- package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
- package/dist/chunks/update-BXIppsbi.mjs +16 -0
- package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
- package/dist/cli.mjs +175 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +17 -32
- package/CHANGELOG.md +0 -1461
- package/bin/cli.js +0 -817
- package/bin/report.js +0 -141
- package/core/adapter/render.js +0 -415
- package/core/agents/implementer.template.md +0 -142
- package/core/agents/profile-reader.md +0 -50
- package/core/agents/release.md +0 -58
- package/core/agents/review.md +0 -179
- package/core/commands/cohorte-align-ds.md +0 -33
- package/core/commands/cohorte-audit.md +0 -58
- package/core/commands/cohorte-brainstorm.md +0 -70
- package/core/commands/cohorte-build.md +0 -219
- package/core/commands/cohorte-doctor.md +0 -194
- package/core/commands/cohorte-fix.md +0 -80
- package/core/commands/cohorte-fleet.md +0 -103
- package/core/commands/cohorte-init-pipeline.md +0 -31
- package/core/commands/cohorte-intake.md +0 -92
- package/core/commands/cohorte-patch.md +0 -118
- package/core/commands/cohorte-refactor.md +0 -59
- package/core/commands/cohorte-retro.md +0 -85
- package/core/commands/cohorte-review.md +0 -270
- package/core/commands/cohorte-ship.md +0 -122
- package/core/commands/cohorte-spec.md +0 -101
- package/core/commands/cohorte-update-pipeline.md +0 -165
- package/core/hooks/gate.py +0 -588
- package/core/runtimes/claude.json +0 -74
- package/core/runtimes/codex.json +0 -85
- package/core/runtimes/cursor.json +0 -76
- package/core/runtimes/gemini.json +0 -76
- package/core/runtimes/opencode.json +0 -73
- package/core/templates/agent-handoff.md +0 -34
- package/core/templates/brainstorm-return.md +0 -36
- package/core/templates/decisions.template.md +0 -42
- package/core/templates/design-brief.md +0 -44
- package/core/templates/patch.template.md +0 -86
- package/core/templates/pr-body.md +0 -25
- package/core/templates/review-feedback.md +0 -38
- package/core/templates/spec.template.md +0 -87
- package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
- package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
- package/core/templates/steps/init-pipeline/04-write-render.md +0 -145
- package/core/templates/steps/init-pipeline/05-report.md +0 -12
- package/core/workflows/audit.js +0 -240
- package/core/workflows/loop.js +0 -641
- package/core/workflows/refactor.js +0 -276
- package/core/workflows/review.js +0 -456
- package/install.ps1 +0 -103
- package/install.sh +0 -114
- package/lib/doctor.js +0 -534
- package/lib/runtime.js +0 -139
- package/lib/versions.js +0 -149
- package/lib/yaml.js +0 -129
- package/profile/PIPELINE.template.md +0 -259
- package/profile/SCHEMA.md +0 -735
- package/profile/cohorte.config.template.yaml +0 -43
- package/scripts/assert-gate-hook.mjs +0 -75
- package/scripts/kanban-move.sh +0 -313
- package/scripts/metrics/collect.mjs +0 -531
- package/scripts/metrics/prices.json +0 -42
- package/scripts/new-feature.sh.template +0 -98
- package/scripts/preflight.sh +0 -124
- package/scripts/remove-feature.sh.template +0 -56
- package/scripts/test-adapter.mjs +0 -437
- package/scripts/test-gate.mjs +0 -443
- package/scripts/test-kanban.mjs +0 -304
- package/scripts/test-lib.mjs +0 -356
- package/scripts/test-metrics.mjs +0 -156
- package/scripts/test-workflows.mjs +0 -758
- package/scripts/validate-core.mjs +0 -325
package/core/workflows/review.js
DELETED
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
// cohorte — /cohorte-review as a deterministic workflow (opt-in; the conversational
|
|
2
|
-
// /cohorte-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 — /cohorte-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
|
-
// The Workflow runtime hands `args` to a script verbatim, so a caller that passes a
|
|
30
|
-
// JSON-ENCODED STRING instead of a real object gets that string back here. The old
|
|
31
|
-
// `typeof args === 'string' ? args.trim()` then took the whole blob as the value — which
|
|
32
|
-
// is how a report landed on disk named `specs/reports/{"feature": "x"}.md`, and how
|
|
33
|
-
// maxRounds/smoke were silently dropped on the same run. Parse it back into the object
|
|
34
|
-
// it was meant to be; a bare slug stays valid shorthand.
|
|
35
|
-
const ARGS = (() => {
|
|
36
|
-
if (typeof args === 'string') {
|
|
37
|
-
const t = args.trim()
|
|
38
|
-
if (t.startsWith('{')) {
|
|
39
|
-
try { const o = JSON.parse(t); if (o && typeof o === 'object' && !Array.isArray(o)) return o } catch {}
|
|
40
|
-
}
|
|
41
|
-
return { feature: t, target: t }
|
|
42
|
-
}
|
|
43
|
-
return args && typeof args === 'object' ? args : {}
|
|
44
|
-
})()
|
|
45
|
-
// Approximate cost of this run from the runtime's own output-token counter — the one
|
|
46
|
-
// figure the conversational path cannot record (a lead cannot read a subagent's token
|
|
47
|
-
// count; SCHEMA.md §Measuring cost). Sampled at start, delta stamped into the metrics line.
|
|
48
|
-
const spentNow = () => {
|
|
49
|
-
try {
|
|
50
|
-
const v = (budget && typeof budget.spent === 'function') ? budget.spent() : 0
|
|
51
|
-
return Number.isFinite(v) ? v : 0 // a NaN here would poison the metrics JSON downstream
|
|
52
|
-
} catch { return 0 }
|
|
53
|
-
}
|
|
54
|
-
const spentStart = spentNow()
|
|
55
|
-
|
|
56
|
-
const isSlug = s => typeof s === 'string' && /^[A-Za-z0-9._-]+$/.test(s)
|
|
57
|
-
const feature = ARGS.feature
|
|
58
|
-
if (!feature) throw new Error('cohorte-review needs args = {feature: "<feature_id>"}')
|
|
59
|
-
if (!isSlug(feature)) {
|
|
60
|
-
throw new Error(`cohorte-review got a feature id that is not a slug: ${JSON.stringify(feature)}. ` +
|
|
61
|
-
'Pass args as a real object, e.g. {feature: "titlebar-project-switcher"} — not a JSON string.')
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// The profile-reader returns through a StructuredOutput tool call, and a haiku agent
|
|
65
|
-
// intermittently nests the whole profile as a JSON *string* under a single wrapper field
|
|
66
|
-
// ({"output": "{\"surfaces\": …}"}) instead of putting the profile's keys at the top level.
|
|
67
|
-
// The schema here used to be {type:'object', additionalProperties:true} — no declared
|
|
68
|
-
// properties, no required keys — so that wrapper validated cleanly and every field then read
|
|
69
|
-
// as undefined: `surfaces` fell back to [], parallel([]) dispatched zero agents, the
|
|
70
|
-
// dead-agent guard had no surfaces to find missing, and the run reported a verdict having
|
|
71
|
-
// done nothing. On the surface it is indistinguishable from a clean run with an empty diff.
|
|
72
|
-
// Declaring the shape gives the tool layer something to validate and the agent something to
|
|
73
|
-
// aim at; unwrapProfile() salvages a wrapped return that still gets through; and the
|
|
74
|
-
// zero-surface abort below makes the silent-success path impossible either way.
|
|
75
|
-
// See also the structured-output section of core/agents/profile-reader.md.
|
|
76
|
-
const PROFILE = {
|
|
77
|
-
type: 'object', additionalProperties: true,
|
|
78
|
-
properties: {
|
|
79
|
-
error: { type: 'string', description: 'set ONLY when PIPELINE.md is missing or unparseable' },
|
|
80
|
-
surfaces: {
|
|
81
|
-
type: 'array',
|
|
82
|
-
description: "one entry per surface, at the TOP LEVEL of this object — never a JSON string",
|
|
83
|
-
items: {
|
|
84
|
-
type: 'object', required: ['key'], additionalProperties: true,
|
|
85
|
-
properties: { key: { type: 'string' }, path: { type: 'string' }, agent: { type: 'string' } },
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Salvage a profile handed back as JSON text rather than as an object — either the whole
|
|
92
|
-
// return, or nested under a single wrapper field. Anything already shaped like a profile
|
|
93
|
-
// (has `surfaces`, or is the documented `{error}` failure shape) passes through untouched.
|
|
94
|
-
const unwrapProfile = p => {
|
|
95
|
-
if (typeof p === 'string') { try { return JSON.parse(p) } catch { return null } }
|
|
96
|
-
if (!p || typeof p !== 'object') return null
|
|
97
|
-
if (Array.isArray(p.surfaces) || p.error) return p
|
|
98
|
-
for (const v of Object.values(p)) {
|
|
99
|
-
if (typeof v !== 'string') continue
|
|
100
|
-
try {
|
|
101
|
-
const inner = JSON.parse(v)
|
|
102
|
-
if (inner && typeof inner === 'object' && !Array.isArray(inner)) return inner
|
|
103
|
-
} catch {}
|
|
104
|
-
}
|
|
105
|
-
return p
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const PREFLIGHT = {
|
|
109
|
-
type: 'object', required: ['pass'], additionalProperties: false,
|
|
110
|
-
properties: {
|
|
111
|
-
pass: { type: 'boolean' },
|
|
112
|
-
tail: { type: 'string', description: 'on failure: the raw last lines the script printed' },
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const STAGE = {
|
|
117
|
-
type: 'object', required: ['surfaces'], additionalProperties: false,
|
|
118
|
-
properties: {
|
|
119
|
-
surfaces: {
|
|
120
|
-
type: 'array',
|
|
121
|
-
items: {
|
|
122
|
-
type: 'object', required: ['key', 'diff', 'files'], additionalProperties: false,
|
|
123
|
-
properties: {
|
|
124
|
-
key: { type: 'string' },
|
|
125
|
-
diff: { type: 'string', description: 'path of the staged .diff file' },
|
|
126
|
-
files: { type: 'array', items: { type: 'string' }, description: 'changed files (from --stat)' },
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const FINDING = {
|
|
134
|
-
type: 'object', required: ['severity', 'file', 'line', 'kind', 'problem', 'fix'],
|
|
135
|
-
additionalProperties: false,
|
|
136
|
-
properties: {
|
|
137
|
-
severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
|
|
138
|
-
file: { type: 'string' }, line: { type: 'integer' },
|
|
139
|
-
kind: { enum: ['spec-violation', 'quality', 'security', 'complexity'] },
|
|
140
|
-
problem: { type: 'string', description: 'one line, no code excerpts' },
|
|
141
|
-
fix: { type: 'string', description: 'one concrete change, one line' },
|
|
142
|
-
},
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// A DEFERRED finding is real but out of this feature's scope (pre-existing code the
|
|
146
|
-
// diff never touched). It carries its own out-of-scope reason, counts in no severity
|
|
147
|
-
// row, is never cross-checked, and can never move the verdict — it is routed to
|
|
148
|
-
// specs/refactor-backlog.md so /cohorte-refactor owns it. See core/agents/review.md §Deferred.
|
|
149
|
-
const DEFERRED = {
|
|
150
|
-
type: 'object', required: ['severity', 'file', 'line', 'kind', 'problem', 'fix', 'outOfScope'],
|
|
151
|
-
additionalProperties: false,
|
|
152
|
-
properties: {
|
|
153
|
-
severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
|
|
154
|
-
file: { type: 'string' }, line: { type: 'integer' },
|
|
155
|
-
kind: { enum: ['quality', 'security', 'rule', 'complexity'] },
|
|
156
|
-
problem: { type: 'string', description: 'one line, no code excerpts' },
|
|
157
|
-
fix: { type: 'string', description: 'one concrete change, one line' },
|
|
158
|
-
outOfScope: { type: 'string', description: 'one line: why this feature does not own it' },
|
|
159
|
-
},
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const REPORT = {
|
|
163
|
-
type: 'object', required: ['verdict', 'findings'], additionalProperties: false,
|
|
164
|
-
properties: {
|
|
165
|
-
verdict: { enum: ['SHIP', 'REVISE', 'BLOCK'] },
|
|
166
|
-
findings: { type: 'array', maxItems: 20, items: FINDING },
|
|
167
|
-
deferred: { type: 'array', maxItems: 10, items: DEFERRED },
|
|
168
|
-
overflow: { type: 'integer', description: 'findings beyond the 20-item cap, if any' },
|
|
169
|
-
notes: { type: 'string', description: 'RBAC / mobile-first assessment only, when the profile enables them' },
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const VERDICT = {
|
|
174
|
-
type: 'object', required: ['refuted', 'reason'], additionalProperties: false,
|
|
175
|
-
properties: {
|
|
176
|
-
refuted: { type: 'boolean' },
|
|
177
|
-
reason: { type: 'string', description: 'one line: why the finding does or does not hold' },
|
|
178
|
-
},
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ── Phase 0 — profile ────────────────────────────────────────────────────────
|
|
182
|
-
phase('Profile')
|
|
183
|
-
const profile = unwrapProfile(await agent(
|
|
184
|
-
'Return this project\'s PIPELINE.md `yaml pipeline-profile` block as JSON, per your instructions.',
|
|
185
|
-
{ agentType: 'profile-reader', label: 'profile', schema: PROFILE, effort: 'low' },
|
|
186
|
-
))
|
|
187
|
-
if (!profile || profile.error) {
|
|
188
|
-
return { verdict: 'ABORTED', aborted: 'profile', reason: `profile unreadable: ${(profile && profile.error) || 'profile-reader returned nothing'}` }
|
|
189
|
-
}
|
|
190
|
-
const cmds = profile.commands || {}
|
|
191
|
-
const base = (profile.vcs && profile.vcs.default_branch) || 'main'
|
|
192
|
-
const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
|
|
193
|
-
// A profile with no surfaces cannot do this workflow's work, and every later
|
|
194
|
-
// guard compares against `surfaces` — an empty list makes them all vacuously
|
|
195
|
-
// pass. Fail loudly here instead of finishing with nothing done.
|
|
196
|
-
if (!surfaces.length) {
|
|
197
|
-
return { verdict: 'ABORTED', aborted: 'profile', reason: 'profile has no surfaces — nothing would be reviewed. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /cohorte-doctor' }
|
|
198
|
-
}
|
|
199
|
-
const quiet = (q, full) => (q && !String(q).startsWith('<') ? q : full ? `${full} 2>&1 | tail -40` : '')
|
|
200
|
-
const checks = [cmds.typecheck, quiet(cmds.lint_quiet, cmds.lint), quiet(cmds.test_quiet, cmds.test)]
|
|
201
|
-
.filter(c => c && !String(c).startsWith('<'))
|
|
202
|
-
|
|
203
|
-
// ── Phase 1 — deterministic preflight (abort while red, zero reviewers) ─────
|
|
204
|
-
phase('Preflight')
|
|
205
|
-
const pre = await agent(
|
|
206
|
-
`Run the cohorte deterministic pre-flight for feature ${feature} in ONE Bash call:\n` +
|
|
207
|
-
`<core>/pipeline/scripts/preflight.sh specs/reports/${feature}.preflight.txt ` +
|
|
208
|
-
checks.map(c => JSON.stringify(c)).join(' ') + '\n' +
|
|
209
|
-
'(<core> = .claude if .claude/pipeline/scripts/preflight.sh exists, else ~/.claude — probe with test -x. ' +
|
|
210
|
-
'Script absent on both: run the quoted commands yourself, each appended to the same report file, stopping at the first failure.) ' +
|
|
211
|
-
'Return pass=true only on a fully green run. On failure set pass=false, put the raw last 40 lines of the report ' +
|
|
212
|
-
'in `tail` — verbatim, no summarizing — and in the same Bash call write the degraded machine verdict the ' +
|
|
213
|
-
`conversational /cohorte-review §0 writes, so an automated driver gets a diagnosis rather than silence: ` +
|
|
214
|
-
`printf '{"id":"${feature}","phase":"review","ts":"%s","aborted":"preflight","verdict":"BLOCK","blocking":null}' ` +
|
|
215
|
-
`"$(date -u +%Y-%m-%dT%H:%M:%SZ)" > specs/reports/${feature}.verdict.json`,
|
|
216
|
-
{ model: 'haiku', label: 'preflight', schema: PREFLIGHT, effort: 'low' },
|
|
217
|
-
)
|
|
218
|
-
if (!pre || !pre.pass) {
|
|
219
|
-
return {
|
|
220
|
-
verdict: 'ABORTED', aborted: 'preflight',
|
|
221
|
-
reason: 'preflight red — fix the mechanical failures (or run /cohorte-fix) before any review; no reviewer was spawned',
|
|
222
|
-
failures: (pre && pre.tail) || 'preflight agent returned nothing',
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ── Phase 2 — stage the diff once ────────────────────────────────────────────
|
|
227
|
-
phase('Stage')
|
|
228
|
-
const surfaceList = surfaces.map(s => `${s.key} → ${s.path}`).join(' · ') || '(none declared)'
|
|
229
|
-
const staged = await agent(
|
|
230
|
-
`Stage the review inputs for cohorte feature ${feature}. Diff base: ${base}. Surfaces: ${surfaceList}.\n` +
|
|
231
|
-
`1. ONE call: git diff ${base} --stat > specs/reports/${feature}.stat.txt — never print the diff.\n` +
|
|
232
|
-
'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' +
|
|
233
|
-
`3. Per surface that has changed paths (ONLY those): git diff ${base} -- <surface.path> [<remainder pathspecs>] > specs/reports/${feature}.<key>.diff\n` +
|
|
234
|
-
'4. Return the touched surfaces with their staged diff path and changed-file list. No changed paths at all ⇒ return an empty surfaces array.',
|
|
235
|
-
{ model: 'haiku', label: 'stage-diff', schema: STAGE, effort: 'low' },
|
|
236
|
-
)
|
|
237
|
-
// A DEAD staging agent returns null, which is not the same fact as "the diff is
|
|
238
|
-
// empty" — conflating them handed back verdict SHIP ("nothing to review") for a
|
|
239
|
-
// feature nobody had looked at. Distinguish them.
|
|
240
|
-
if (!staged) {
|
|
241
|
-
return {
|
|
242
|
-
verdict: 'ABORTED', aborted: 'stage-diff',
|
|
243
|
-
reason: 'the diff-staging agent died — no reviewer was spawned and nothing was reviewed',
|
|
244
|
-
next: `re-run the review workflow, or /cohorte-review ${feature} conversationally`,
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const touched = staged.surfaces || []
|
|
248
|
-
if (!touched.length) {
|
|
249
|
-
// Nothing was reviewed, so nothing was certified: no DoD tick, no freshness stamp.
|
|
250
|
-
// `next` must say so — a driver relaying a bare "/cohorte-ship" here would point at
|
|
251
|
-
// a gate that (rightly) refuses. And verdict.json is written on EVERY run
|
|
252
|
-
// (cohorte-review.md §3) — leaving last round's REVISE on disk here would hand any
|
|
253
|
-
// driver reading the file a stale verdict.
|
|
254
|
-
await agent(
|
|
255
|
-
`Write EXACTLY this to specs/reports/${feature}.verdict.json (overwrite), substituting <ISO now> ` +
|
|
256
|
-
'with `date -u +%Y-%m-%dT%H:%M:%SZ`, then return the single word done:\n' +
|
|
257
|
-
JSON.stringify({
|
|
258
|
-
id: feature, phase: 'review', ts: '<ISO now>', verdict: 'SHIP', findings: 0, blocking: 0,
|
|
259
|
-
security: 0, deferred: 0, unreviewed: [], severity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
|
|
260
|
-
surfaces: {}, blocking_items: [], fingerprint: '',
|
|
261
|
-
}),
|
|
262
|
-
{ model: 'haiku', label: 'stage-verdict', effort: 'low' },
|
|
263
|
-
)
|
|
264
|
-
return {
|
|
265
|
-
verdict: 'SHIP', reason: `no diff against ${base} — nothing to review`,
|
|
266
|
-
findings: 0, blocking: 0, blockingItems: [], deferred: 0, unreviewedSurfaces: [],
|
|
267
|
-
next: `nothing to ship: the diff against ${base} is empty, so no review ran and no freshness stamp was written — check the branch/base (was the feature actually built here?)`,
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
log(`Touched surfaces: ${touched.map(s => s.key).join(', ')}`)
|
|
271
|
-
|
|
272
|
-
// ── Phases 3+4 — review each surface, cross-check its hard findings ─────────
|
|
273
|
-
// pipeline(): a fast surface's cross-check starts while a slow one still reviews.
|
|
274
|
-
const reviewed = await pipeline(
|
|
275
|
-
touched,
|
|
276
|
-
s => agent(
|
|
277
|
-
'Review one feature surface against its frozen spec, per your agent instructions (read the staged ' +
|
|
278
|
-
'diff FIRST; open a full source file only when a finding demands it; capped shape — max 20 findings, ' +
|
|
279
|
-
'one line each, no code excerpts). Put anything real but OUT of this feature\'s scope in `deferred` ' +
|
|
280
|
-
'(max 10, each with its out-of-scope reason) per your §Deferred rules — never in `findings`. — Variable slots: ' +
|
|
281
|
-
`feature ${feature} · scope: the ${s.key} surface only · spec: specs/${feature}.md · ` +
|
|
282
|
-
`staged diff: ${s.diff} · changed files: ${s.files.join(', ')}`,
|
|
283
|
-
{ agentType: 'review', label: `review:${s.key}`, phase: 'Review', schema: REPORT },
|
|
284
|
-
),
|
|
285
|
-
async (report, s) => {
|
|
286
|
-
if (!report) return null
|
|
287
|
-
const hard = report.findings.filter(f => f.severity === 'CRITICAL' || f.kind === 'security')
|
|
288
|
-
const rest = report.findings.filter(f => !hard.includes(f))
|
|
289
|
-
// Deferred items skip the cross-check entirely: refuting one would spend an agent
|
|
290
|
-
// to argue about something that cannot affect the verdict either way.
|
|
291
|
-
const deferred = report.deferred || []
|
|
292
|
-
if (!hard.length) return { key: s.key, report, kept: rest, refuted: [], deferred }
|
|
293
|
-
const votes = await parallel(hard.map(f => () => agent(
|
|
294
|
-
'Adversarially verify ONE review finding — your job is to REFUTE it if you can. Read the staged ' +
|
|
295
|
-
'diff and the exact file:line; refuted=true when the code, a guard, a test, or the spec shows the ' +
|
|
296
|
-
'finding does not hold. Uncertain ⇒ refuted=false (a real CRITICAL must not die on doubt). — ' +
|
|
297
|
-
`Finding: [${f.severity}/${f.kind}] ${f.file}:${f.line} — ${f.problem} (proposed fix: ${f.fix}). ` +
|
|
298
|
-
`Feature ${feature} · spec: specs/${feature}.md · staged diff: ${s.diff}`,
|
|
299
|
-
{ agentType: 'review', label: `verify:${s.key}`, phase: 'Cross-check', schema: VERDICT },
|
|
300
|
-
).then(v => ({ f, refuted: !!(v && v.refuted), reason: v ? v.reason : 'verifier died — kept' }))))
|
|
301
|
-
const checkedVotes = votes.filter(Boolean)
|
|
302
|
-
return {
|
|
303
|
-
key: s.key,
|
|
304
|
-
report,
|
|
305
|
-
kept: rest.concat(checkedVotes.filter(v => !v.refuted).map(v => v.f)),
|
|
306
|
-
refuted: checkedVotes.filter(v => v.refuted).map(v => ({ ...v.f, reason: v.reason })),
|
|
307
|
-
deferred,
|
|
308
|
-
}
|
|
309
|
-
},
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
const results = reviewed.filter(Boolean)
|
|
313
|
-
// A reviewer that DIED returns null — and a dead reviewer produces zero findings,
|
|
314
|
-
// which is byte-identical to a clean surface. Left unchecked, "every reviewer
|
|
315
|
-
// crashed" scores SHIP: the strongest possible verdict from the weakest possible
|
|
316
|
-
// evidence. Name the unreviewed surfaces and refuse to certify them.
|
|
317
|
-
const unreviewed = touched.filter(s => !results.some(r => r.key === s.key)).map(s => s.key)
|
|
318
|
-
if (unreviewed.length) log(`Reviewer died on: ${unreviewed.join(', ')} — those surfaces are NOT reviewed`)
|
|
319
|
-
const kept = results.flatMap(r => r.kept.map(f => ({ ...f, surface: r.key })))
|
|
320
|
-
const refuted = results.flatMap(r => r.refuted.map(f => ({ ...f, surface: r.key })))
|
|
321
|
-
// Deferred findings are deliberately kept OUT of `counts`, out of `verdict` and out
|
|
322
|
-
// of `clean`: they belong to /cohorte-refactor, not to this feature's fix loop.
|
|
323
|
-
const deferredAll = results.flatMap(r => (r.deferred || []).map(f => ({ ...f, surface: r.key })))
|
|
324
|
-
const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
|
|
325
|
-
for (const f of kept) counts[f.severity] = (counts[f.severity] || 0) + 1
|
|
326
|
-
// blocking = CRITICAL + security findings, each counted once — the conversational
|
|
327
|
-
// /cohorte-review §3's contract restated as a number, so blocking == 0 ⟺ verdict SHIP.
|
|
328
|
-
// blocking_items carry the finding's IDENTITY, not its wording: surface | file without
|
|
329
|
-
// `:line` (a fix that inserts lines shifts every line below it — a line-bearing identity
|
|
330
|
-
// would change every pass and drift detection would never fire) | the problem's first 8
|
|
331
|
-
// words, lowercased, runs of non-alphanumerics collapsed. Sorted, so two rounds with the
|
|
332
|
-
// same findings compare equal however the reviewers ordered them.
|
|
333
|
-
const blockingFindings = kept.filter(f => f.severity === 'CRITICAL' || f.kind === 'security')
|
|
334
|
-
const normProblem = p => String(p).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(' ').slice(0, 8).join(' ')
|
|
335
|
-
const blockingItems = [...new Set(blockingFindings.map(f =>
|
|
336
|
-
`${f.surface}|${String(f.file).replace(/:\d+$/, '')}|${normProblem(f.problem)}`))].sort()
|
|
337
|
-
// Verdict from the findings that SURVIVED the cross-check (a refuted CRITICAL
|
|
338
|
-
// must not force a fix loop): security ⇒ BLOCK, CRITICAL ⇒ REVISE, else SHIP —
|
|
339
|
-
// but never SHIP while a surface went unreviewed (absence of evidence, not
|
|
340
|
-
// evidence of absence).
|
|
341
|
-
const verdict = kept.some(f => f.kind === 'security') ? 'BLOCK'
|
|
342
|
-
: kept.some(f => f.severity === 'CRITICAL') ? 'REVISE'
|
|
343
|
-
: unreviewed.length ? 'REVISE' : 'SHIP'
|
|
344
|
-
// Only a SHIP whose leftovers are all LOW is a clean bill of health. The
|
|
345
|
-
// conversational /cohorte-review routes any surviving CRITICAL/HIGH/security to /cohorte-fix, so
|
|
346
|
-
// this path must not answer "/cohorte-ship" on a SHIP that still carries HIGH findings.
|
|
347
|
-
const clean = verdict === 'SHIP' && kept.every(f => f.severity === 'LOW')
|
|
348
|
-
|
|
349
|
-
// ── Phase 5 — stage the merged report; only the verdict leaves the workflow ──
|
|
350
|
-
phase('Merge')
|
|
351
|
-
const findingLine = f =>
|
|
352
|
-
`- [${f.severity}] \`${f.file}:${f.line}\` · ${f.kind} · ${f.problem} → **Fix:** ${f.fix}`
|
|
353
|
-
const reportBody = [
|
|
354
|
-
'# REVIEW REPORT', `feature_id: ${feature} · merged by cohorte-review workflow`, '',
|
|
355
|
-
'| Severity | Count |', '| -------- | ----- |',
|
|
356
|
-
...['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(s => `| ${s} | ${counts[s] || 0} |`), '',
|
|
357
|
-
`Verdict: ${verdict}`, '', '## Findings', '',
|
|
358
|
-
kept.length ? kept.map(findingLine).join('\n') : 'None.',
|
|
359
|
-
'', '## Deferred', '',
|
|
360
|
-
deferredAll.length
|
|
361
|
-
? deferredAll.map(f => `${findingLine(f)} · out of scope: ${f.outOfScope} · deferred:${feature}`).join('\n')
|
|
362
|
-
: 'None.',
|
|
363
|
-
...(unreviewed.length ? ['', '## NOT reviewed (reviewer died — no verdict on these)', '',
|
|
364
|
-
unreviewed.map(k => `- \`${k}\` — re-run /cohorte-review ${feature} (or the review workflow)`).join('\n')] : []),
|
|
365
|
-
...(refuted.length ? ['', '## Refuted by cross-check (no action needed)', '',
|
|
366
|
-
refuted.map(f => `- ${f.file}:${f.line} · ${f.problem} — refuted: ${f.reason}`).join('\n')] : []),
|
|
367
|
-
].join('\n')
|
|
368
|
-
// The machine-readable verdict the conversational /cohorte-review §3 guarantees — the
|
|
369
|
-
// ONLY contract between the pipeline and an automated driver (the loop workflow), which
|
|
370
|
-
// parses no prose. Composed HERE so the staging agent substitutes two tokens and can
|
|
371
|
-
// invent nothing; the sha256 fingerprint is computed in its Bash (scripts have no crypto).
|
|
372
|
-
const verdictJson = JSON.stringify({
|
|
373
|
-
id: feature, phase: 'review', ts: '<ISO now>', verdict,
|
|
374
|
-
findings: kept.length, blocking: blockingFindings.length,
|
|
375
|
-
security: kept.filter(f => f.kind === 'security').length,
|
|
376
|
-
deferred: deferredAll.length, unreviewed,
|
|
377
|
-
severity: counts,
|
|
378
|
-
surfaces: Object.fromEntries(results.map(r => [r.key, {
|
|
379
|
-
verdict: r.report.verdict, findings: r.kept.length,
|
|
380
|
-
blocking: r.kept.filter(f => f.severity === 'CRITICAL' || f.kind === 'security').length,
|
|
381
|
-
}])),
|
|
382
|
-
blocking_items: blockingItems, fingerprint: '<FP>',
|
|
383
|
-
})
|
|
384
|
-
const staging = await agent(
|
|
385
|
-
`Stage a cohorte review report and its metrics, mechanically:\n` +
|
|
386
|
-
`1. Write EXACTLY this content to specs/reports/${feature}.md (overwrite):\n<<<REPORT\n${reportBody}\nREPORT\n` +
|
|
387
|
-
`2. Write EXACTLY this to specs/reports/${feature}.verdict.json (overwrite), substituting <ISO now> with ` +
|
|
388
|
-
'`date -u +%Y-%m-%dT%H:%M:%SZ` and <FP> with the fingerprint — computed in Bash, never by hand: ' +
|
|
389
|
-
(blockingItems.length
|
|
390
|
-
// POSIX-escape embedded quotes ('\'') rather than stripping them: a stripped quote
|
|
391
|
-
// makes the fingerprint disagree with the blocking_items in the same file, and with
|
|
392
|
-
// a conversational re-run computing it per the §3 contract.
|
|
393
|
-
? `printf '%s\\n' ${blockingItems.map(i => `'${i.replace(/'/g, "'\\''")}'`).join(' ')} | LC_ALL=C sort | sha256sum | cut -c1-16 ` +
|
|
394
|
-
'(`shasum -a 256` then first 16 hex chars where there is no sha256sum):\n'
|
|
395
|
-
: 'the blocking list is empty, so <FP> is the empty string "":\n') +
|
|
396
|
-
`${verdictJson}\n` +
|
|
397
|
-
// Per-surface verdicts (not the merged one stamped on every row — one BLOCK used to
|
|
398
|
-
// mark ALL surfaces failed), and dead reviewers logged as "dead"
|
|
399
|
-
// per SCHEMA.md §Dead agents — an incomplete batch is the batch worth recording.
|
|
400
|
-
`3. Append one line to $(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl: ` +
|
|
401
|
-
`{"ts":"<ISO now>","feature":"${feature}","phase":"review","seconds":0,"tokens":${Math.max(0, spentNow() - spentStart)},"surfaces":{${
|
|
402
|
-
results.map(r => `"${r.key}":"${r.report.verdict}:${r.kept.length}"`)
|
|
403
|
-
.concat(unreviewed.map(k => `"${k}":"dead"`)).join(',')}}}\n` +
|
|
404
|
-
// Deferred findings must land in the backlog on EVERY verdict — parked only on a
|
|
405
|
-
// SHIP is parked nowhere the rest of the time, which is the leak this closes.
|
|
406
|
-
(deferredAll.length
|
|
407
|
-
? `4. Route the deferred findings to specs/refactor-backlog.md (create it if absent): for each line below, ` +
|
|
408
|
-
`append it under the \`## <domain>\` heading named in its prefix (create that heading if absent) — with \`>>\`, ` +
|
|
409
|
-
`never by rewriting the file, and skip any whose file path + first words already appear there (grep -F first, ` +
|
|
410
|
-
`they may be left from a prior round or an /cohorte-audit):\n` +
|
|
411
|
-
deferredAll.map(f =>
|
|
412
|
-
`${f.surface}||- [ ] ${f.severity} · ${f.file}:${f.line} · ${f.kind} · ${f.fix} · deferred:${feature}`).join('\n') +
|
|
413
|
-
'\n'
|
|
414
|
-
: '') +
|
|
415
|
-
// Stamp + tick only when nothing above LOW survived: the conversational /cohorte-review
|
|
416
|
-
// keeps the stamp only for LOW findings, and a SHIP verdict here can still carry
|
|
417
|
-
// HIGH/MEDIUM ones — certifying those for /cohorte-ship would ship known defects. A dead
|
|
418
|
-
// reviewer already forced the verdict off SHIP, so `clean` covers that too.
|
|
419
|
-
(clean
|
|
420
|
-
? `5. Stamp the freshness gate in specs/${feature}.md's front-matter, exactly as the conversational /cohorte-review §3 does ` +
|
|
421
|
-
`(so /cohorte-ship can prove the reviewed code is what ships): BASE=$(git merge-base ${base} HEAD); set reviewed_base: $BASE and ` +
|
|
422
|
-
`reviewed_digest: $(git diff $BASE -- . ':(exclude)specs/' | sha256sum | cut -c1-16) — shasum -a 256, first 16 hex, where sha256sum is absent (macOS). ` +
|
|
423
|
-
'6. Tick the spec DoD boxes this run verified (spec conformance + copy language — review SHIP; tests/lint/typecheck — green preflight); leave the rest unticked.\n'
|
|
424
|
-
: '') +
|
|
425
|
-
'Return the single word: done.',
|
|
426
|
-
{ model: 'haiku', label: 'stage-report', effort: 'low' },
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
// The staging agent writes the report, the metrics line, and (when clean) the
|
|
430
|
-
// freshness stamp + DoD ticks. If it died, none of that is on disk — returning
|
|
431
|
-
// `report: <path>` and "/cohorte-ship" would point the human at a file that does not
|
|
432
|
-
// exist and certify a stamp that was never written.
|
|
433
|
-
const staged_ok = staging != null && /done/i.test(String(staging))
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
verdict,
|
|
437
|
-
counts,
|
|
438
|
-
blocking: blockingFindings.length, // CRITICAL + security, each once — 0 ⟺ SHIP; what a driver reduces on
|
|
439
|
-
blockingItems, // the sorted identity list behind verdict.json's fingerprint
|
|
440
|
-
deferred: deferredAll.length, // parked in the backlog for /cohorte-refactor — never blocking
|
|
441
|
-
refutedByCrossCheck: refuted.length,
|
|
442
|
-
reportStaged: staged_ok,
|
|
443
|
-
unreviewedSurfaces: unreviewed, // reviewers that died — these carry NO verdict
|
|
444
|
-
criticals: kept.filter(f => f.severity === 'CRITICAL' || f.kind === 'security')
|
|
445
|
-
.map(f => `[${f.surface}] ${f.file}:${f.line} — ${f.problem}`),
|
|
446
|
-
report: staged_ok ? `specs/reports/${feature}.md` : '(NOT written — the staging agent died)',
|
|
447
|
-
next: !staged_ok
|
|
448
|
-
? `the report/metrics/freshness stamp were NEVER written (staging agent died) — the verdict above is real, but nothing is on disk: re-run the review workflow, or /cohorte-review ${feature}`
|
|
449
|
-
: unreviewed.length
|
|
450
|
-
? `re-run the review — no reviewer completed on: ${unreviewed.join(', ')}`
|
|
451
|
-
: clean
|
|
452
|
-
? `/cohorte-ship ${feature} (DoD ticked + freshness stamped)`
|
|
453
|
-
: verdict === 'SHIP'
|
|
454
|
-
? `/cohorte-fix ${feature} — SHIP verdict, but ${kept.length} finding(s) above LOW survived; park them in specs/refactor-backlog.md instead if you deliberately defer them`
|
|
455
|
-
: `/cohorte-fix ${feature}`,
|
|
456
|
-
}
|
package/install.ps1
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# install.ps1 — install the portable multi-agent pipeline (Windows).
|
|
2
|
-
# Works on Windows PowerShell 5.1 and PowerShell 7+. Mirrors install.sh.
|
|
3
|
-
#
|
|
4
|
-
# Per-project install (default — bundles the core into <target>\.claude, committable):
|
|
5
|
-
# .\install.ps1 [target_dir]
|
|
6
|
-
# irm <raw-url>/install.ps1 | iex
|
|
7
|
-
#
|
|
8
|
-
# Global install (one core in ~\.claude, shared by every repo on this machine):
|
|
9
|
-
# .\install.ps1 -Global
|
|
10
|
-
# & ([scriptblock]::Create((irm <raw-url>/install.ps1))) -Global
|
|
11
|
-
#
|
|
12
|
-
# Update the generic core in place (keeps any generated PIPELINE.md + rendered agents):
|
|
13
|
-
# .\install.ps1 -Update [target_dir]
|
|
14
|
-
# .\install.ps1 -Update -Global
|
|
15
|
-
#
|
|
16
|
-
# Per-project install copies the core into <target>\.claude; global install copies it once
|
|
17
|
-
# into ~\.claude and registers the gate hook there. Either way you then run `/cohorte-init-pipeline`
|
|
18
|
-
# in each repo to generate PIPELINE.md + render the surface agents. Update refreshes ONLY the
|
|
19
|
-
# stack-agnostic files; generated profiles, rendered agents, gate-config.json and any project
|
|
20
|
-
# settings.json are left untouched.
|
|
21
|
-
|
|
22
|
-
[CmdletBinding()]
|
|
23
|
-
param(
|
|
24
|
-
[switch]$Global,
|
|
25
|
-
[switch]$Update,
|
|
26
|
-
[Parameter(Position = 0)][string]$Target
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
$ErrorActionPreference = 'Stop'
|
|
30
|
-
|
|
31
|
-
$repoUrl = $env:PIPELINE_REPO
|
|
32
|
-
if (-not $repoUrl) { $repoUrl = 'https://github.com/TheBidouilleAgency/cohorte' }
|
|
33
|
-
|
|
34
|
-
if (-not $Target) { $Target = (Get-Location).Path }
|
|
35
|
-
|
|
36
|
-
# --- locate the source (this checkout, or clone if piped via irm|iex) --------
|
|
37
|
-
$tmp = $null
|
|
38
|
-
try {
|
|
39
|
-
$src = $null
|
|
40
|
-
if ($PSScriptRoot -and (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'core'))) {
|
|
41
|
-
$src = $PSScriptRoot
|
|
42
|
-
} else {
|
|
43
|
-
Write-Host "-> fetching pipeline from $repoUrl"
|
|
44
|
-
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
45
|
-
throw 'git is required to fetch the pipeline (or run install.ps1 from a checkout).'
|
|
46
|
-
}
|
|
47
|
-
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("cohorte-" + [Guid]::NewGuid().ToString('N'))
|
|
48
|
-
New-Item -ItemType Directory -Path $tmp | Out-Null
|
|
49
|
-
# PS 5.1 turns redirected native stderr into terminating errors under EAP=Stop;
|
|
50
|
-
# relax it around the clone and rely on the exit code instead.
|
|
51
|
-
$prevEAP = $ErrorActionPreference
|
|
52
|
-
$ErrorActionPreference = 'Continue'
|
|
53
|
-
git clone --depth 1 --quiet $repoUrl (Join-Path $tmp 'pipeline') 2>&1 | Out-Null
|
|
54
|
-
$ErrorActionPreference = $prevEAP
|
|
55
|
-
if ($LASTEXITCODE -ne 0) { throw "git clone of $repoUrl failed" }
|
|
56
|
-
$src = Join-Path $tmp 'pipeline'
|
|
57
|
-
}
|
|
58
|
-
if (-not (Test-Path -LiteralPath (Join-Path $src 'core'))) {
|
|
59
|
-
throw "pipeline source not found (no core/ in $src)"
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
# --- delegate to the Node CLI --------------------------------------------
|
|
63
|
-
# Since 2.2.0 the commands in core/ are runtime-NEUTRAL sources: they carry capability
|
|
64
|
-
# conditionals (`<!-- cohorte:if subagents -->`) and path tokens (`<core>`, `<state>`)
|
|
65
|
-
# that the adapter resolves per coding agent. Copying them verbatim, as this script used
|
|
66
|
-
# to, would install prompts full of unresolved markers — an install that looks successful
|
|
67
|
-
# and instructs the model with text meant for a different runtime. There is no PowerShell
|
|
68
|
-
# renderer, so hand the whole job to bin/cli.js, the documented route anyway.
|
|
69
|
-
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
70
|
-
if ($node) {
|
|
71
|
-
# An old Node fails DEEP into cli.js (fs.cpSync needs >= 16.7) after some files
|
|
72
|
-
# are already on disk — a half-install that reports as a crash. Refuse up front.
|
|
73
|
-
$nodeMajor = 0
|
|
74
|
-
try { $nodeMajor = [int](& $node.Source -p 'process.versions.node.split(".")[0]') } catch { }
|
|
75
|
-
if ($nodeMajor -lt 18) {
|
|
76
|
-
Write-Error "cohorte needs Node >= 18 — found $(& $node.Source --version). Upgrade Node, then re-run."
|
|
77
|
-
if ($PSCommandPath) { exit 1 }
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
$cliArgs = @((Join-Path $src 'bin\cli.js'), $(if ($Update) { 'update' } else { 'install' }))
|
|
81
|
-
if ($Global) { $cliArgs += '--global' } else { $cliArgs += $Target }
|
|
82
|
-
& $node.Source @cliArgs
|
|
83
|
-
# `exit` under `irm … | iex` terminates the user's interactive PowerShell
|
|
84
|
-
# session (there is no script file to exit from) — closing the console that
|
|
85
|
-
# just ran the documented one-liner. Exit only when running as a file.
|
|
86
|
-
if ($PSCommandPath) { exit $LASTEXITCODE }
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
Write-Error @"
|
|
90
|
-
cohorte needs Node >= 18 to install.
|
|
91
|
-
The pipeline's commands are rendered per coding agent (Claude Code, Codex, Cursor,
|
|
92
|
-
Gemini CLI, OpenCode) at install time; there is no PowerShell equivalent of that
|
|
93
|
-
step, and a raw copy would install prompts this runtime cannot follow.
|
|
94
|
-
Install Node, then: npm i -g cohorte; cohorte install$(if ($Global) { ' --global' })
|
|
95
|
-
"@
|
|
96
|
-
if ($PSCommandPath) { exit 1 }
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
} finally {
|
|
100
|
-
if ($tmp -and (Test-Path -LiteralPath $tmp)) {
|
|
101
|
-
Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
102
|
-
}
|
|
103
|
-
}
|