cohorte 2.10.1 → 3.0.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/assets/manifest.json +193 -0
- package/assets/migrations/config/README.md +12 -0
- package/assets/migrations/state/0001_init.sql +154 -0
- package/assets/prompts/README.md +11 -0
- package/assets/prompts/agents/fixer.md +7 -0
- package/assets/prompts/agents/implementer.md +12 -0
- package/assets/prompts/agents/reviewer.md +8 -0
- package/assets/prompts/agents/security-reviewer.md +7 -0
- package/assets/prompts/phases/build.md +5 -0
- package/assets/prompts/phases/review.md +5 -0
- package/assets/prompts/phases/test.md +5 -0
- package/assets/prompts/system.md +11 -0
- package/assets/schemas/agent-output.schema.json +225 -0
- package/assets/schemas/auth-status.schema.json +87 -0
- package/assets/schemas/command-result.schema.json +192 -0
- package/assets/schemas/commands.schema.json +952 -0
- package/assets/schemas/config.schema.json +2259 -0
- package/assets/schemas/doctor-report.schema.json +104 -0
- package/assets/schemas/events.schema.json +8514 -0
- package/assets/schemas/fake-script.schema.json +549 -0
- package/assets/schemas/inspect.schema.json +3133 -0
- package/assets/schemas/manifest.schema.json +79 -0
- package/assets/schemas/ownership.schema.json +52 -0
- package/assets/schemas/policy-verdict.schema.json +282 -0
- package/assets/schemas/project-model.schema.json +783 -0
- package/assets/schemas/project-status.schema.json +217 -0
- package/assets/schemas/reconcile-plan.schema.json +155 -0
- package/assets/schemas/run-diff.schema.json +177 -0
- package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
- package/assets/schemas/run-state.schema.json +1755 -0
- package/assets/schemas/runtime-capabilities.schema.json +1545 -0
- package/assets/schemas/sandbox-capabilities.schema.json +116 -0
- package/assets/schemas/skill.schema.json +87 -0
- package/assets/schemas/spec.schema.json +202 -0
- package/assets/schemas/tool-catalogue.schema.json +655 -0
- package/assets/schemas/trust-record.schema.json +38 -0
- package/assets/skills/README.md +9 -0
- package/assets/skills/inspect-surface.md +4 -0
- package/dist/agent-host.mjs +50 -0
- package/dist/agent-host.mjs.map +1 -0
- package/dist/bundle-manifest.json +102 -0
- package/dist/chunks/__host-DgGPyco9.mjs +18 -0
- package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
- package/dist/chunks/approve-Cv135UqI.mjs +19 -0
- package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
- package/dist/chunks/auth-BT5oVp76.mjs +132 -0
- package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
- package/dist/chunks/auth-D8rCVY33.mjs +84 -0
- package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs.map +1 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
- package/dist/chunks/compose-Cr0hP0qm.mjs +7303 -0
- package/dist/chunks/compose-Cr0hP0qm.mjs.map +1 -0
- package/dist/chunks/config-DefHA2Lw.mjs +96 -0
- package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
- package/dist/chunks/contract-B0ra2-bm.mjs +295 -0
- package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs.map +1 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
- package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
- package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
- package/dist/chunks/discover-30Ciya_0.mjs +20 -0
- package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs.map +1 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
- package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
- package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
- package/dist/chunks/init-Okz4mlhW.mjs +30 -0
- package/dist/chunks/init-Okz4mlhW.mjs.map +1 -0
- package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
- package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
- package/dist/chunks/logs-Giq5sUau.mjs +25 -0
- package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
- package/dist/chunks/migrate-DRoFUg53.mjs +19 -0
- package/dist/chunks/migrate-DRoFUg53.mjs.map +1 -0
- package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
- package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
- package/dist/chunks/pause-BASBfKLO.mjs +16 -0
- package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
- package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
- package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
- package/dist/chunks/providers-Dx78imKj.mjs +19 -0
- package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
- package/dist/chunks/reconcile-BfD785gd.mjs +26 -0
- package/dist/chunks/reconcile-BfD785gd.mjs.map +1 -0
- package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
- package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
- package/dist/chunks/retry-CLrUem1H.mjs +15 -0
- package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
- package/dist/chunks/review-BDdjb6kB.mjs +17 -0
- package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
- package/dist/chunks/run-CgTWYlcd.mjs +82 -0
- package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
- package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
- package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
- package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
- package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
- package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
- package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
- package/dist/chunks/src-CId6kMsY.mjs +1689 -0
- package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
- package/dist/chunks/src-CfAOde7C.mjs +5403 -0
- package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
- package/dist/chunks/src-Cu3TxNob.mjs +783 -0
- package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
- package/dist/chunks/src-DcU4IXMe.mjs +117 -0
- package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
- package/dist/chunks/src-JxawBQBn.mjs +446 -0
- package/dist/chunks/src-JxawBQBn.mjs.map +1 -0
- package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
- package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
- package/dist/chunks/status-SMjiOMYy.mjs +29 -0
- package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
- package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
- package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
- package/dist/chunks/update-BXIppsbi.mjs +16 -0
- package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
- package/dist/cli.mjs +175 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +17 -32
- package/CHANGELOG.md +0 -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/install.sh
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
#
|
|
3
|
-
# install.sh — install the portable multi-agent pipeline.
|
|
4
|
-
# POSIX sh (works with dash/bash/zsh).
|
|
5
|
-
#
|
|
6
|
-
# Per-project install (default — bundles the core into <target>/.claude, committable):
|
|
7
|
-
# sh install.sh [target_dir]
|
|
8
|
-
# curl -fsSL <raw-url>/install.sh | sh
|
|
9
|
-
#
|
|
10
|
-
# Global install (one core in ~/.claude, shared by every repo on this machine):
|
|
11
|
-
# sh install.sh --global
|
|
12
|
-
# curl -fsSL <raw-url>/install.sh | sh -s -- --global
|
|
13
|
-
#
|
|
14
|
-
# Update the generic core in place (keeps any generated PIPELINE.md + rendered agents):
|
|
15
|
-
# sh install.sh --update [target_dir]
|
|
16
|
-
# sh install.sh --update --global
|
|
17
|
-
#
|
|
18
|
-
# Per-project install copies the core into <target>/.claude; global install copies it once
|
|
19
|
-
# into ~/.claude and registers the gate hook there. Either way you then run `/cohorte-init-pipeline`
|
|
20
|
-
# in each repo to generate PIPELINE.md + render the surface agents. Update refreshes ONLY the
|
|
21
|
-
# stack-agnostic files; generated profiles, rendered agents, gate-config.json and any project
|
|
22
|
-
# settings.json are left untouched.
|
|
23
|
-
|
|
24
|
-
set -eu
|
|
25
|
-
|
|
26
|
-
REPO_URL="${PIPELINE_REPO:-https://github.com/TheBidouilleAgency/cohorte}"
|
|
27
|
-
|
|
28
|
-
mode="install"
|
|
29
|
-
scope="project"
|
|
30
|
-
positional=""
|
|
31
|
-
while [ $# -gt 0 ]; do
|
|
32
|
-
case "$1" in
|
|
33
|
-
--update) mode="update"; shift ;;
|
|
34
|
-
--global) scope="global"; shift ;;
|
|
35
|
-
-h|--help)
|
|
36
|
-
cat <<'USAGE'
|
|
37
|
-
install.sh — install the cohorte pipeline core.
|
|
38
|
-
|
|
39
|
-
sh install.sh [target_dir] per-project: bundle the core into <target>/.claude
|
|
40
|
-
sh install.sh --global one shared core in ~/.claude (recommended)
|
|
41
|
-
sh install.sh --update [target] refresh the core in place, keep every generated file
|
|
42
|
-
sh install.sh --update --global
|
|
43
|
-
|
|
44
|
-
Honours $CLAUDE_CONFIG_DIR for the global destination and $PIPELINE_REPO for the
|
|
45
|
-
source when piped through curl. The npm CLI (`npm i -g cohorte` then `cohorte install`)
|
|
46
|
-
does the same thing and is the documented route; this script exists for npm-less
|
|
47
|
-
setups (curl straight from the repo). Node itself is still required — the commands
|
|
48
|
-
are rendered per coding agent at install time and there is no shell renderer.
|
|
49
|
-
USAGE
|
|
50
|
-
exit 0 ;;
|
|
51
|
-
--) shift; break ;;
|
|
52
|
-
-*) echo "error: unknown flag: $1 (try --help)" >&2; exit 2 ;;
|
|
53
|
-
*) positional="$1"; shift ;;
|
|
54
|
-
esac
|
|
55
|
-
done
|
|
56
|
-
target="${positional:-$PWD}"
|
|
57
|
-
|
|
58
|
-
# --- locate the source (this checkout, or clone if piped via curl) ----------
|
|
59
|
-
src=""
|
|
60
|
-
self="${0:-}"
|
|
61
|
-
self_dir=""
|
|
62
|
-
case "$self" in
|
|
63
|
-
# `sh install.sh` from inside a checkout hands $0 with no slash — the old */*-only
|
|
64
|
-
# case missed it and silently CLONED the remote instead of installing the local
|
|
65
|
-
# tree the human was standing in (dirname of a bare name is `.`, which is exactly
|
|
66
|
-
# right here; the piped-stdin case stays `sh`, matches neither arm, and clones).
|
|
67
|
-
*/*) self_dir=$(CDPATH= cd -- "$(dirname -- "$self")" && pwd) ;;
|
|
68
|
-
*install.sh*) self_dir=$(pwd) ;;
|
|
69
|
-
esac
|
|
70
|
-
if [ -n "$self_dir" ] && [ -d "$self_dir/core" ]; then
|
|
71
|
-
src="$self_dir"
|
|
72
|
-
else
|
|
73
|
-
echo "→ fetching pipeline from $REPO_URL"
|
|
74
|
-
tmp=$(mktemp -d)
|
|
75
|
-
trap 'rm -rf "$tmp"' EXIT
|
|
76
|
-
git clone --depth 1 "$REPO_URL" "$tmp/pipeline" >/dev/null 2>&1
|
|
77
|
-
src="$tmp/pipeline"
|
|
78
|
-
fi
|
|
79
|
-
[ -d "$src/core" ] || { echo "error: pipeline source not found (no core/ in $src)" >&2; exit 1; }
|
|
80
|
-
|
|
81
|
-
# --- require Node >= 18, then delegate to the Node CLI -----------------------
|
|
82
|
-
# Since 2.2.0 the commands in core/ are runtime-NEUTRAL sources: they carry capability
|
|
83
|
-
# conditionals (`<!-- cohorte:if subagents -->`) and path tokens (`<core>`, `<state>`) that
|
|
84
|
-
# the adapter resolves per coding agent. Copying them verbatim, as this script used to,
|
|
85
|
-
# would install prompts full of unresolved markers — an install that looks successful and
|
|
86
|
-
# instructs the model with text meant for a different runtime. There is no shell renderer,
|
|
87
|
-
# so the whole job goes to bin/cli.js, which is the documented route anyway. (The legacy
|
|
88
|
-
# copy-verbatim shell path was removed in 2.7.0 — it had been unreachable dead code since
|
|
89
|
-
# 2.2.0, and its text was what validate-core's copy checks were vacuously matching.)
|
|
90
|
-
if command -v node >/dev/null 2>&1; then
|
|
91
|
-
# An old Node fails DEEP into cli.js (fs.cpSync needs >= 16.7) after some files are
|
|
92
|
-
# already on disk — a half-install that reports as a crash. Refuse up front instead.
|
|
93
|
-
node_major=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)
|
|
94
|
-
case "$node_major" in *[!0-9]*) node_major=0 ;; esac
|
|
95
|
-
if [ "$node_major" -lt 18 ]; then
|
|
96
|
-
echo "error: cohorte needs Node >= 18 — found $(node --version 2>/dev/null || echo '?')." >&2
|
|
97
|
-
echo " Upgrade Node, then re-run this script (or: npm i -g cohorte && cohorte install$([ "$scope" = global ] && echo ' --global'))." >&2
|
|
98
|
-
exit 1
|
|
99
|
-
fi
|
|
100
|
-
set -- install
|
|
101
|
-
[ "$mode" = "update" ] && set -- update
|
|
102
|
-
[ "$scope" = "global" ] && set -- "$@" --global
|
|
103
|
-
[ "$scope" = "project" ] && set -- "$@" "$target"
|
|
104
|
-
# Not `exec`: exec replaces this shell, so the curl-path EXIT trap (rm -rf "$tmp")
|
|
105
|
-
# never fires and every piped install leaks the shallow clone in $TMPDIR.
|
|
106
|
-
node "$src/bin/cli.js" "$@"
|
|
107
|
-
exit $?
|
|
108
|
-
fi
|
|
109
|
-
echo "error: cohorte needs Node >= 18 to install." >&2
|
|
110
|
-
echo " The pipeline's commands are rendered per coding agent (Claude Code, Codex, Cursor," >&2
|
|
111
|
-
echo " Gemini CLI, OpenCode) at install time; there is no shell equivalent of that step," >&2
|
|
112
|
-
echo " and a raw copy would install prompts this runtime cannot follow." >&2
|
|
113
|
-
echo " Install Node, then: npm i -g cohorte && cohorte install$([ "$scope" = global ] && echo ' --global')" >&2
|
|
114
|
-
exit 1
|
package/lib/doctor.js
DELETED
|
@@ -1,534 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
// Programmatic port of the /cohorte-doctor checks (core/commands/cohorte-doctor.md), so
|
|
3
|
-
// `cohorte doctor` can report without a coding agent in the loop.
|
|
4
|
-
// Read-only: inspects files only. Checks that need a live process (MCP connectivity,
|
|
5
|
-
// git worktree state, DesignSync) are reported as `skip` with a note — the node server
|
|
6
|
-
// can't run them, and honest "not checked here" beats a false green.
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const { layouts, stateDirs } = require('./runtime.js');
|
|
11
|
-
const { parseProfileBlock } = require('./yaml');
|
|
12
|
-
const { versions } = require('./versions');
|
|
13
|
-
|
|
14
|
-
// Rendered surface agents live alongside these fixed (non-surface) agents; exclude them
|
|
15
|
-
// from the orphan check so they're never mistaken for a stray surface agent.
|
|
16
|
-
const FIXED_AGENTS = new Set([
|
|
17
|
-
'review', 'release', 'profile-reader',
|
|
18
|
-
// retired (1.5.0) — still excluded so a stale install's leftover file isn't
|
|
19
|
-
// reported as a stray surface agent.
|
|
20
|
-
'smoke',
|
|
21
|
-
'implementer.template',
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
// The spec lifecycle (SCHEMA.md §Spec status). `in-progress` and `blocked` stay valid after
|
|
25
|
-
// /cohorte-loop was retired in 2.2.0: specs in existing repos carry them, and an external driver
|
|
26
|
-
// still needs somewhere to record "a round is under way" / "a round gave up".
|
|
27
|
-
const VALID_STATUS = ['draft', 'frozen', 'in-progress', 'in-review', 'shipped', 'blocked'];
|
|
28
|
-
|
|
29
|
-
// Artifacts the pipeline itself writes into specs/ that are NOT feature specs and have no
|
|
30
|
-
// front-matter status. `/cohorte-audit` writes specs/refactor-backlog.md by design, so scanning it
|
|
31
|
-
// as a spec made /cohorte-doctor warn about a file cohorte had just created — a false positive that
|
|
32
|
-
// fired in every project that had ever run /cohorte-audit. `_`-prefixed files (e.g. _template.md)
|
|
33
|
-
// are already skipped by the reader below.
|
|
34
|
-
const NON_SPEC_FILES = new Set(['refactor-backlog.md']);
|
|
35
|
-
|
|
36
|
-
const exists = p => { try { return fs.existsSync(p); } catch { return false; } };
|
|
37
|
-
const readText = p => { try { return fs.readFileSync(p, 'utf8'); } catch { return null; } };
|
|
38
|
-
const readJson = p => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };
|
|
39
|
-
const isTrue = v => v === true;
|
|
40
|
-
const sameSet = (a, b) => {
|
|
41
|
-
const A = new Set(a || []), B = new Set(b || []);
|
|
42
|
-
if (A.size !== B.size) return false;
|
|
43
|
-
for (const x of A) if (!B.has(x)) return false;
|
|
44
|
-
return true;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Paths are reported to a human looking at a repo: show them relative to it.
|
|
48
|
-
const rel = (projectRoot) => (p) => {
|
|
49
|
-
const r = path.relative(projectRoot, p);
|
|
50
|
-
return r && !r.startsWith('..') ? r : p;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function mk(id, label, status, detail, fix) {
|
|
54
|
-
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function acrossRuntimes(all, check) {
|
|
58
|
-
const distinct = all.filter((l, i) => all.findIndex(other => other.id === l.id) === i);
|
|
59
|
-
const results = (distinct.length ? distinct : [null]).map(check);
|
|
60
|
-
if (results.length === 1) return results[0];
|
|
61
|
-
const rank = { skip: 0, ok: 1, warn: 2, bad: 3 };
|
|
62
|
-
const worst = results.reduce((a, b) => rank[b.status] > rank[a.status] ? b : a);
|
|
63
|
-
return { ...worst, detail: results.map((r, i) => `${distinct[i].label}: ${r.detail}`).join(' · ') };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// --- individual checks -------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
function checkCore(v) {
|
|
69
|
-
if (v.installMode === 'none') {
|
|
70
|
-
return mk('core', 'Core & pointer', 'bad', 'no pipeline core installed for this project',
|
|
71
|
-
'cohorte install (or --global)');
|
|
72
|
-
}
|
|
73
|
-
if (v.pointer.present && v.pointer.core_version && v.installedVersion &&
|
|
74
|
-
v.pointer.core_version !== v.installedVersion) {
|
|
75
|
-
return mk('core', 'Core & pointer', 'warn',
|
|
76
|
-
`pointer says core ${v.pointer.core_version} but installed core is ${v.installedVersion}`,
|
|
77
|
-
'cohorte update (reconcile the pointer)');
|
|
78
|
-
}
|
|
79
|
-
if (v.freshness === -1) {
|
|
80
|
-
return mk('core', 'Core & pointer', 'warn',
|
|
81
|
-
`core ${v.installedVersion} installed (${v.installMode}); npm latest is ${v.latest}`,
|
|
82
|
-
'/cohorte-update-pipeline (or cohorte update)');
|
|
83
|
-
}
|
|
84
|
-
const tail = v.latest ? `, npm latest ${v.latest}` : ', npm unreachable';
|
|
85
|
-
return mk('core', 'Core & pointer', 'ok', `core ${v.installedVersion} (${v.installMode})${tail}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function checkProfile(profile, hasPipelineMd) {
|
|
89
|
-
if (!hasPipelineMd) {
|
|
90
|
-
return mk('profile', 'Profile (PIPELINE.md)', 'bad', 'PIPELINE.md not found',
|
|
91
|
-
'/cohorte-init-pipeline (generate the project profile)');
|
|
92
|
-
}
|
|
93
|
-
if (!profile) {
|
|
94
|
-
return mk('profile', 'Profile (PIPELINE.md)', 'bad',
|
|
95
|
-
'PIPELINE.md present but its `yaml pipeline-profile` block is missing or unparseable',
|
|
96
|
-
'/cohorte-init-pipeline (or fix the fenced yaml block)');
|
|
97
|
-
}
|
|
98
|
-
const n = (profile.surfaces || []).length;
|
|
99
|
-
return mk('profile', 'Profile (PIPELINE.md)', n ? 'ok' : 'warn',
|
|
100
|
-
`${profile.name || 'unnamed'} · ${n} surface${n === 1 ? '' : 's'}`,
|
|
101
|
-
n ? undefined : 'add at least one surface to §surfaces');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function checkAgents(profile, projectRoot, layout) {
|
|
105
|
-
if (!profile || !(profile.surfaces || []).length) {
|
|
106
|
-
return mk('agents', 'Surfaces ↔ agents', 'skip', 'no surfaces to reconcile');
|
|
107
|
-
}
|
|
108
|
-
if (!layout) {
|
|
109
|
-
return mk('agents', 'Surfaces ↔ agents', 'skip', 'no core installed — nothing renders agents yet');
|
|
110
|
-
}
|
|
111
|
-
const agentsDir = layout.surfaceAgents || layout.agents;
|
|
112
|
-
const ext = layout.agentExt || (layout.id === 'codex' ? '.toml' : '.md');
|
|
113
|
-
const surfaceAgents = profile.surfaces.map(s => s.agent).filter(Boolean);
|
|
114
|
-
|
|
115
|
-
const missing = surfaceAgents.filter(a => !exists(path.join(agentsDir, `${a}${ext}`)));
|
|
116
|
-
|
|
117
|
-
let files = [];
|
|
118
|
-
try { files = fs.readdirSync(agentsDir).filter(f => f.endsWith(ext)).map(f => f.slice(0, -ext.length)); }
|
|
119
|
-
catch { /* dir absent → handled by `missing` */ }
|
|
120
|
-
// Orphan detection only makes sense against a PROJECT agents dir. A global install's
|
|
121
|
-
// agents dir (~/.claude/agents) is the user's shared Claude Code space — their personal
|
|
122
|
-
// agents and other cohorte projects' surface agents live there legitimately, and
|
|
123
|
-
// flagging them sent humans deleting files that were not this project's to judge.
|
|
124
|
-
const orphans = layout.scope === 'global' && layout.id !== 'codex'
|
|
125
|
-
? []
|
|
126
|
-
: files.filter(f => !FIXED_AGENTS.has(f) && !surfaceAgents.includes(f));
|
|
127
|
-
|
|
128
|
-
if (missing.length) {
|
|
129
|
-
return mk('agents', 'Surfaces ↔ agents', 'bad',
|
|
130
|
-
`surface(s) with no rendered agent: ${missing.join(', ')}`,
|
|
131
|
-
'/cohorte-init-pipeline (re-render surface agents)');
|
|
132
|
-
}
|
|
133
|
-
if (orphans.length) {
|
|
134
|
-
return mk('agents', 'Surfaces ↔ agents', 'warn',
|
|
135
|
-
`agent file(s) with no owning surface: ${orphans.join(', ')}`,
|
|
136
|
-
'remove the stale agent file, or add its surface to PIPELINE.md');
|
|
137
|
-
}
|
|
138
|
-
return mk('agents', 'Surfaces ↔ agents', 'ok',
|
|
139
|
-
`${surfaceAgents.length} surface agent(s) all rendered, no orphans (${layout.label})`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function checkGate(profile, projectRoot, stateDirs) {
|
|
143
|
-
const gate = profile && profile.gate;
|
|
144
|
-
if (!gate || (!gate.deny && !gate.ask && !gate.ask_on_default_branch && !gate.preflight)) {
|
|
145
|
-
return mk('gate', 'Gate config', 'skip', 'no gate block in the profile');
|
|
146
|
-
}
|
|
147
|
-
// One gate config per PROJECT, wherever the installed runtime keeps it. A repo driven from
|
|
148
|
-
// two agents shares it deliberately — they must not disagree about what is gated.
|
|
149
|
-
const cfgPath = stateDirs.map(d => path.join(d, 'gate-config.json')).find(exists);
|
|
150
|
-
const cfg = cfgPath ? readJson(cfgPath) : null;
|
|
151
|
-
if (!cfg) {
|
|
152
|
-
return mk('gate', 'Gate config', 'bad',
|
|
153
|
-
`gate-config.json missing or unreadable (looked in ${stateDirs.map(rel(projectRoot)).join(', ')})`,
|
|
154
|
-
'/cohorte-init-pipeline (regenerate gate-config.json from the gate block)');
|
|
155
|
-
}
|
|
156
|
-
const drifted = [];
|
|
157
|
-
if (!sameSet(cfg.deny, gate.deny)) drifted.push('deny');
|
|
158
|
-
if (!sameSet(cfg.ask, gate.ask)) drifted.push('ask');
|
|
159
|
-
if (!sameSet(cfg.ask_on_default_branch, gate.ask_on_default_branch)) drifted.push('ask_on_default_branch');
|
|
160
|
-
if ((cfg.default_branch || 'main') !== (gate.default_branch || 'main')) drifted.push('default_branch');
|
|
161
|
-
// The phase gate (1.3.0) lives in the same file: a profile that enables
|
|
162
|
-
// gate.preflight with a pre-1.3.0 gate-config.json silently never fires it.
|
|
163
|
-
const wantPf = gate.preflight || {};
|
|
164
|
-
const havePf = (cfg.preflight && typeof cfg.preflight === 'object') ? cfg.preflight : {};
|
|
165
|
-
if (!!wantPf.enabled !== !!havePf.enabled
|
|
166
|
-
|| !sameSet(wantPf.agents || ['review'], havePf.agents || ['review'])
|
|
167
|
-
|| Number(wantPf.max_age_minutes || 30) !== Number(havePf.max_age_minutes || 30)) {
|
|
168
|
-
drifted.push('preflight');
|
|
169
|
-
}
|
|
170
|
-
if (drifted.length) {
|
|
171
|
-
return mk('gate', 'Gate config', 'warn',
|
|
172
|
-
`gate-config.json drifted from PIPELINE.md gate block (${drifted.join(', ')})`,
|
|
173
|
-
`regenerate ${rel(projectRoot)(cfgPath)} to mirror the gate block`);
|
|
174
|
-
}
|
|
175
|
-
const branchGated = (gate.ask_on_default_branch || []).length;
|
|
176
|
-
return mk('gate', 'Gate config', 'ok',
|
|
177
|
-
`mirrors the profile (${(gate.deny || []).length} deny, ${(gate.ask || []).length} ask` +
|
|
178
|
-
(branchGated ? `, ${branchGated} gated on ${gate.default_branch || 'main'}` : '') + ')');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Local-only pipeline artifacts, named relative to whichever `<state>` dir the installed
|
|
182
|
-
// runtime uses (`.claude` on a Claude install, `.cohorte` on the others). `preflight.ok` is the
|
|
183
|
-
// one that BREAKS things when versioned rather than merely being noise: the stamp names the tree
|
|
184
|
-
// it verified, committing it moves HEAD past that, and the committed copy then rides into every
|
|
185
|
-
// fresh clone and worktree — the phase gate either blocks a clean tree or greens one it never
|
|
186
|
-
// checked.
|
|
187
|
-
function localArtifacts(stateRels) {
|
|
188
|
-
const out = [];
|
|
189
|
-
for (const d of stateRels) {
|
|
190
|
-
out.push({ path: `${d}/preflight.ok`, stamp: true,
|
|
191
|
-
why: 'the phase-gate stamp — a versioned one breaks the gate in every clone and worktree' });
|
|
192
|
-
out.push({ path: `${d}/pipeline-metrics.jsonl`,
|
|
193
|
-
why: 'the per-dispatch metrics sink (local, append-only)' });
|
|
194
|
-
}
|
|
195
|
-
out.push({ path: 'specs/reports/',
|
|
196
|
-
why: 'the /cohorte-review report buffer (derived, regenerated each run)' });
|
|
197
|
-
return out;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Does any .gitignore in play cover `rel`? Deliberately literal — it recognizes the exact
|
|
201
|
-
// path, its basename, a trailing-slash dir prefix and a `*.<ext>` glob, which is every form
|
|
202
|
-
// /cohorte-init-pipeline writes. Anything more clever would need a real gitignore engine.
|
|
203
|
-
function ignoreCovers(projectRoot, rel) {
|
|
204
|
-
const base = rel.replace(/\/$/, '').split('/').pop();
|
|
205
|
-
const ext = base.includes('.') ? '*' + base.slice(base.lastIndexOf('.')) : null;
|
|
206
|
-
const files = [
|
|
207
|
-
path.join(projectRoot, '.gitignore'),
|
|
208
|
-
path.join(projectRoot, '.claude', '.gitignore'),
|
|
209
|
-
];
|
|
210
|
-
for (const f of files) {
|
|
211
|
-
const text = readText(f);
|
|
212
|
-
if (!text) continue;
|
|
213
|
-
for (let line of text.split('\n')) {
|
|
214
|
-
line = line.trim();
|
|
215
|
-
if (!line || line.startsWith('#')) continue;
|
|
216
|
-
const p = line.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
217
|
-
if (!p) continue;
|
|
218
|
-
if (p === rel.replace(/\/$/, '') || p === base || p === ext) return true;
|
|
219
|
-
// A directory rule covers everything under it (`.claude/` ignores the stamp too).
|
|
220
|
-
if (rel.startsWith(p + '/')) return true;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function checkLocalArtifacts(projectRoot, stateRels) {
|
|
227
|
-
const artifacts = localArtifacts(stateRels);
|
|
228
|
-
const unignored = artifacts.filter(a => !ignoreCovers(projectRoot, a.path));
|
|
229
|
-
if (!unignored.length) {
|
|
230
|
-
return mk('artifacts', 'Local artifacts', 'ok',
|
|
231
|
-
`${artifacts.length} local-only artifact path(s) all gitignored`);
|
|
232
|
-
}
|
|
233
|
-
const stamp = unignored.find(a => a.stamp);
|
|
234
|
-
return mk('artifacts', 'Local artifacts', stamp ? 'bad' : 'warn',
|
|
235
|
-
`not gitignored: ${unignored.map(a => a.path).join(', ')} — ${unignored[0].why}`,
|
|
236
|
-
`add ${unignored.map(a => a.path).join(' + ')} to .gitignore`
|
|
237
|
-
+ (stamp ? `, then \`git rm --cached --ignore-unmatch ${stamp.path}\`` : ''));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Gate registrations in one config file. Three shapes across the runtimes that host the hook:
|
|
241
|
-
// Claude Code and Codex use the matcher-group form under `PreToolUse`, Gemini the same shape
|
|
242
|
-
// under `BeforeTool`, Cursor a flat `[{command}]` under `beforeShellExecution`. A registration
|
|
243
|
-
// read with the wrong shape looks absent, which is how a healthy install gets reported broken.
|
|
244
|
-
function gateRegs(settingsPath, event) {
|
|
245
|
-
const data = readJson(settingsPath);
|
|
246
|
-
const list = data && data.hooks && Array.isArray(data.hooks[event]) ? data.hooks[event] : [];
|
|
247
|
-
// Trailing-quote tolerant, like the installers since 1.3.2: the Windows form is
|
|
248
|
-
// `py "C:\…\gate.py"` — a bare .endsWith() reports every healthy Windows install
|
|
249
|
-
// as "not registered".
|
|
250
|
-
// Strip the trailing `--runtime <id>` BEFORE the quotes: the registered command is
|
|
251
|
-
// `python3 "/path/gate.py" --runtime cursor`, so taking the quotes off first leaves
|
|
252
|
-
// `…gate.py"` and the match fails — a healthy registration read as missing.
|
|
253
|
-
const isOurs = (c) => typeof c === 'string'
|
|
254
|
-
&& c.trim().replace(/\s+--runtime[= ]\S+$/, '').replace(/"+$/, '').endsWith('gate.py');
|
|
255
|
-
return list.filter(e => isOurs(e && e.command)
|
|
256
|
-
|| ((e && e.hooks) || []).some(h => isOurs(h && h.command)));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
260
|
-
// A registration can exist without a discoverable core — a repo whose core lives in a global
|
|
261
|
-
// dir the caller was not pointed at, or one predating `runtimes.json`. Falling through to
|
|
262
|
-
// "no runtime installed" there would hide a real double-registration, so assume the Claude
|
|
263
|
-
// layout: before the adapter it was the only one, and it is the only one whose hook can be
|
|
264
|
-
// registered outside its own core dir.
|
|
265
|
-
const known = all.length ? all : [{
|
|
266
|
-
id: 'claude', label: 'Claude Code', scope: installMode === 'bundled' ? 'project' : 'global',
|
|
267
|
-
core: path.join(projectRoot, '.claude'),
|
|
268
|
-
hooksConfig: path.join(projectRoot, '.claude', 'settings.json'),
|
|
269
|
-
capabilities: { subagents: true, hooks: true, workflows: true },
|
|
270
|
-
}];
|
|
271
|
-
const hosts = known.filter(l => l.capabilities && l.capabilities.hooks && l.hooksConfig);
|
|
272
|
-
const advisory = known.filter(l => !(l.capabilities && l.capabilities.hooks));
|
|
273
|
-
|
|
274
|
-
if (!hosts.length) {
|
|
275
|
-
if (advisory.length) {
|
|
276
|
-
// OpenCode extends via plugins, not a blocking hook. The gate still runs — the rendered
|
|
277
|
-
// commands call `gate.py --check` — but it is advisory, and saying ✅ here would claim an
|
|
278
|
-
// enforcement property that genuinely is not there.
|
|
279
|
-
return mk('hooks', 'Gate hook', 'warn',
|
|
280
|
-
`${advisory.map(l => l.label).join(', ')} has no blocking hook — the gate runs as an explicit --check the agent must call`,
|
|
281
|
-
'nothing to fix; run /cohorte-doctor inside that runtime for the full picture');
|
|
282
|
-
}
|
|
283
|
-
return mk('hooks', 'Gate hook', 'warn', 'no runtime installed to register the gate hook against',
|
|
284
|
-
'cohorte install (or --global)');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const problems = [];
|
|
288
|
-
const okLines = [];
|
|
289
|
-
for (const l of hosts.filter((h, i) => hosts.findIndex(other => other.id === h.id) === i)) {
|
|
290
|
-
const event = (l.id === 'cursor') ? 'beforeShellExecution'
|
|
291
|
-
: (l.id === 'gemini') ? 'BeforeTool' : 'PreToolUse';
|
|
292
|
-
// A Claude registration serves the project from EITHER scope: bundled repos get it from
|
|
293
|
-
// /cohorte-init-pipeline in project settings, but on a machine with the global core it
|
|
294
|
-
// usually lives (correctly, exactly once) in global settings — warning there would
|
|
295
|
-
// prescribe a re-registration that double-prompts.
|
|
296
|
-
const paths = l.id === 'claude'
|
|
297
|
-
? (installMode === 'global'
|
|
298
|
-
? [path.join(globalDir, 'settings.json'), path.join(projectRoot, '.claude', 'settings.json')]
|
|
299
|
-
: [path.join(projectRoot, '.claude', 'settings.json'), path.join(globalDir, 'settings.json')])
|
|
300
|
-
: l.id === 'codex'
|
|
301
|
-
? [...new Set([...hosts.filter(h => h.id === 'codex').map(h => h.hooksConfig),
|
|
302
|
-
path.join(projectRoot, '.codex', 'hooks.json')])]
|
|
303
|
-
: [l.hooksConfig];
|
|
304
|
-
const found = paths.map(p2 => ({ path: p2, regs: gateRegs(p2, event) })).filter(f => f.regs.length);
|
|
305
|
-
if (!found.length) {
|
|
306
|
-
problems.push(`${l.label}: not registered (${event})`);
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
const total = found.reduce((n, f) => n + f.regs.length, 0);
|
|
310
|
-
if (total > 1) {
|
|
311
|
-
problems.push(`${l.label}: registered ${total}× — it will double-prompt`);
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
// Validate the regex against the host's real tool names, including dispatch aliases.
|
|
315
|
-
const matcher = String(found[0].regs[0].matcher || '');
|
|
316
|
-
const matches = name => {
|
|
317
|
-
if (!matcher || matcher === '*') return true;
|
|
318
|
-
try { return new RegExp(matcher).test(name); } catch { return false; }
|
|
319
|
-
};
|
|
320
|
-
const dispatch = l.id === 'codex' ? ['spawn_agent', 'Agent'] : ['Task'];
|
|
321
|
-
if (['claude', 'codex'].includes(l.id)
|
|
322
|
-
&& (!matches('Bash') || !dispatch.some(matches))) {
|
|
323
|
-
problems.push(`${l.label}: matcher "${matcher}" must cover Bash and ${dispatch.join('/')}`);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
okLines.push(`${l.label} (${event}${matcher ? `, ${matcher}` : ''})`);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (problems.length) {
|
|
330
|
-
// Name the healthy ones too. With several runtimes installed, "Claude Code: not registered"
|
|
331
|
-
// alone reads as if the gate were off everywhere, when four of five may be fine.
|
|
332
|
-
const tail = okLines.length ? ` (ok: ${okLines.join(', ')})` : '';
|
|
333
|
-
return mk('hooks', 'Gate hook', 'warn', problems.join(' · ') + tail,
|
|
334
|
-
'npm i -g cohorte@latest && cohorte update (re-registers the gate hook for every installed runtime)');
|
|
335
|
-
}
|
|
336
|
-
const tail = advisory.length ? ` · ${advisory.map(l => l.label).join(', ')}: advisory --check only` : '';
|
|
337
|
-
return mk('hooks', 'Gate hook', 'ok', `registered once for ${okLines.join(', ')}${tail}`);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function checkRetrieval(profile, projectRoot, layout) {
|
|
341
|
-
const provider = profile && profile.retrieval && profile.retrieval.provider;
|
|
342
|
-
if (!provider || provider === 'none' || String(provider).startsWith('<')) {
|
|
343
|
-
return mk('retrieval', 'Code retrieval', 'skip', 'provider: none');
|
|
344
|
-
}
|
|
345
|
-
if (layout?.id === 'codex') {
|
|
346
|
-
const file = '.codex/config.toml';
|
|
347
|
-
const config = readText(path.join(projectRoot, file)) || '';
|
|
348
|
-
// Static registration check only, as for the JSON path below. Connectivity requires
|
|
349
|
-
// a live session. Anchor to table declarations so prose/comments cannot count as wired.
|
|
350
|
-
const tables = [...config.matchAll(/^\s*\[mcp_servers\.([^\]\n]+)\]\s*(?:#.*)?$/gm)]
|
|
351
|
-
.map(m => m[1].trim().replace(/^["']|["']$/g, ''));
|
|
352
|
-
const wired = tables.includes(String(provider));
|
|
353
|
-
return mk('retrieval', 'Code retrieval', wired ? 'ok' : 'warn', wired
|
|
354
|
-
? `provider: ${provider} — table present in ${file} (validity/connectivity need in-session verification)`
|
|
355
|
-
: `profile says provider: ${provider} but ${file} has no matching server table`,
|
|
356
|
-
wired ? undefined : '$cohorte-update-pipeline (merge the project MCP table)');
|
|
357
|
-
}
|
|
358
|
-
// The profile alone isn't proof the provider was ever wired: /cohorte-init-pipeline
|
|
359
|
-
// registers it at project scope in .mcp.json. Verify the entry exists on disk;
|
|
360
|
-
// live connectivity still needs a session — note it, don't fake green.
|
|
361
|
-
const mcp = readJson(path.join(projectRoot, '.mcp.json'));
|
|
362
|
-
const servers = (mcp && mcp.mcpServers) || {};
|
|
363
|
-
const wired = Object.keys(servers).some(k => k.toLowerCase().includes(String(provider).toLowerCase()));
|
|
364
|
-
if (!wired) {
|
|
365
|
-
return mk('retrieval', 'Code retrieval', 'warn',
|
|
366
|
-
`profile says provider: ${provider} but .mcp.json has no matching server entry`,
|
|
367
|
-
'/cohorte-init-pipeline or /cohorte-update-pipeline (re-wire the retrieval provider)');
|
|
368
|
-
}
|
|
369
|
-
return mk('retrieval', 'Code retrieval', 'ok',
|
|
370
|
-
`provider: ${provider} — registered in .mcp.json (connectivity needs /cohorte-doctor in-session)`);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function checkDesign(profile, projectRoot) {
|
|
374
|
-
const d = profile && profile.design;
|
|
375
|
-
if (!d || !isTrue(d.enabled)) return mk('design', 'Design system', 'skip', 'design.enabled: false');
|
|
376
|
-
const targets = [['snapshot_dir', d.snapshot_dir], ['ui_kit_path', d.ui_kit_path], ['tokens_path', d.tokens_path]];
|
|
377
|
-
const missing = targets.filter(([, p]) => p && !exists(path.join(projectRoot, p))).map(([k]) => k);
|
|
378
|
-
if (missing.length) {
|
|
379
|
-
return mk('design', 'Design system', 'warn', `missing path(s): ${missing.join(', ')}`,
|
|
380
|
-
'create the missing design paths or fix them in PIPELINE.md §design');
|
|
381
|
-
}
|
|
382
|
-
return mk('design', 'Design system', 'ok', `provider: ${d.provider} — DS paths present`);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function checkIsolation(profile, projectRoot) {
|
|
386
|
-
const iso = profile && profile.isolation;
|
|
387
|
-
if (!iso || !isTrue(iso.enabled)) return mk('isolation', 'Isolation', 'skip', 'isolation.enabled: false');
|
|
388
|
-
const scripts = ['scripts/new-feature.sh', 'scripts/remove-feature.sh'];
|
|
389
|
-
const problems = [];
|
|
390
|
-
for (const rel of scripts) {
|
|
391
|
-
const txt = readText(path.join(projectRoot, rel));
|
|
392
|
-
if (txt == null) problems.push(`${rel} missing`);
|
|
393
|
-
else if (/__[A-Z_]+__/.test(txt)) problems.push(`${rel} has unrendered __TOKEN__`);
|
|
394
|
-
}
|
|
395
|
-
if (problems.length) {
|
|
396
|
-
return mk('isolation', 'Isolation', 'warn', problems.join('; '),
|
|
397
|
-
'/cohorte-init-pipeline (re-render the isolation scripts)');
|
|
398
|
-
}
|
|
399
|
-
return mk('isolation', 'Isolation', 'ok', 'feature scripts rendered (worktree state not checked here)');
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Workflow variants (review/audit/refactor as deterministic multi-agent runs) are opt-in;
|
|
403
|
-
// the conversational commands stay the default path, so nothing here is ever 'bad'.
|
|
404
|
-
// Whether the session has workflows ENABLED needs a live Claude session — /cohorte-doctor
|
|
405
|
-
// in-session checks that; here we only check what's on disk.
|
|
406
|
-
function checkWorkflows(projectRoot, globalDir, installMode, all) {
|
|
407
|
-
if (installMode === 'none') return mk('workflows', 'Workflows', 'skip', 'no core installed');
|
|
408
|
-
// The Workflow engine is Claude Code's; the other runtimes are not shipped the scripts at
|
|
409
|
-
// all, and reporting them as "missing" would flag a deliberate omission as a defect.
|
|
410
|
-
const cc = all.find(l => l.id === 'claude');
|
|
411
|
-
if (!cc) {
|
|
412
|
-
return mk('workflows', 'Workflows', 'skip',
|
|
413
|
-
`not available on ${all.map(l => l.label).join(', ') || 'this runtime'} — conversational commands only`);
|
|
414
|
-
}
|
|
415
|
-
const dir = path.join(cc.core, 'workflows');
|
|
416
|
-
const agentsDir = cc.agents;
|
|
417
|
-
const scripts = ['review.js', 'audit.js', 'refactor.js', 'loop.js'];
|
|
418
|
-
const missing = scripts.filter(s => !exists(path.join(dir, s)));
|
|
419
|
-
if (missing.length === scripts.length) {
|
|
420
|
-
return mk('workflows', 'Workflows', 'warn',
|
|
421
|
-
'no workflow scripts installed — conversational commands only (the default path)',
|
|
422
|
-
'cohorte update (ships core/workflows/)');
|
|
423
|
-
}
|
|
424
|
-
if (missing.length) {
|
|
425
|
-
return mk('workflows', 'Workflows', 'warn', `missing script(s): ${missing.join(', ')}`,
|
|
426
|
-
'cohorte update (half-copied core)');
|
|
427
|
-
}
|
|
428
|
-
if (!exists(path.join(agentsDir, 'profile-reader.md'))) {
|
|
429
|
-
return mk('workflows', 'Workflows', 'warn',
|
|
430
|
-
'scripts present but the profile-reader agent (their phase 0) is missing',
|
|
431
|
-
'cohorte update (re-copies the fixed agents)');
|
|
432
|
-
}
|
|
433
|
-
return mk('workflows', 'Workflows', 'ok',
|
|
434
|
-
'scripts + profile-reader installed — opt-in per run; needs Claude Code ≥ 2.1.154 with ' +
|
|
435
|
-
'workflows enabled (run /cohorte-doctor in-session to check the live half). Inline design ' +
|
|
436
|
-
'(design.inline) has its own, higher floor of ≥ 2.1.234 — it is a separate prerequisite and ' +
|
|
437
|
-
'does not gate this check either way');
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function scanSpecs(projectRoot) {
|
|
441
|
-
const dir = path.join(projectRoot, 'specs');
|
|
442
|
-
const specs = [];
|
|
443
|
-
let files = [];
|
|
444
|
-
try { files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('_') && !NON_SPEC_FILES.has(f)); }
|
|
445
|
-
catch { return specs; }
|
|
446
|
-
for (const f of files) {
|
|
447
|
-
const txt = readText(path.join(dir, f)) || '';
|
|
448
|
-
const fm = txt.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
449
|
-
const body = fm ? fm[1] : '';
|
|
450
|
-
const get = k => { const m = body.match(new RegExp(`^${k}:\\s*(.*)$`, 'm')); return m ? m[1].trim() : null; };
|
|
451
|
-
const status = get('status');
|
|
452
|
-
// Legacy resume state from the retired /cohorte-loop driver. Nothing writes it any more;
|
|
453
|
-
// it is still read so a spec left mid-flight by an older core still shows why on the board.
|
|
454
|
-
const pass = parseInt(get('loop_pass'), 10);
|
|
455
|
-
const phase = get('loop_phase');
|
|
456
|
-
specs.push({
|
|
457
|
-
file: f,
|
|
458
|
-
id: get('feature_id') || f.replace(/\.md$/, ''),
|
|
459
|
-
title: get('title'),
|
|
460
|
-
status: status ? status.split('#')[0].trim() : null,
|
|
461
|
-
branch: get('branch'),
|
|
462
|
-
loop: pass > 0 ? { pass, phase: phase && phase !== 'done' ? phase : null } : null,
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
return specs;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function checkSpecs(specs) {
|
|
469
|
-
if (!specs.length) return mk('specs', 'Specs', 'skip', 'no specs yet');
|
|
470
|
-
const bad = specs.filter(s => !VALID_STATUS.includes(s.status));
|
|
471
|
-
const counts = {};
|
|
472
|
-
for (const s of specs) counts[s.status || '?'] = (counts[s.status || '?'] || 0) + 1;
|
|
473
|
-
const summary = VALID_STATUS.filter(st => counts[st]).map(st => `${counts[st]} ${st}`).join(', ');
|
|
474
|
-
if (bad.length) {
|
|
475
|
-
return mk('specs', 'Specs', 'warn',
|
|
476
|
-
`invalid status in: ${bad.map(s => s.file).join(', ')}`,
|
|
477
|
-
`set status to one of: ${VALID_STATUS.join(' · ')}`);
|
|
478
|
-
}
|
|
479
|
-
return mk('specs', 'Specs', 'ok', `${specs.length} spec(s) — ${summary}`);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// --- orchestrator ------------------------------------------------------------
|
|
483
|
-
|
|
484
|
-
async function state({ projectRoot, globalDir, cliVersion }) {
|
|
485
|
-
const v = await versions({ projectRoot, globalDir, cliVersion });
|
|
486
|
-
|
|
487
|
-
const pipelineMd = readText(path.join(projectRoot, 'PIPELINE.md'));
|
|
488
|
-
const profile = pipelineMd ? parseProfileBlock(pipelineMd) : null;
|
|
489
|
-
const specs = scanSpecs(projectRoot);
|
|
490
|
-
|
|
491
|
-
// Which coding agent(s) this repo is wired for. Every path-dependent check below reads from
|
|
492
|
-
// this rather than assuming `.claude/` — on a Cursor-only repo that assumption reported the
|
|
493
|
-
// core, the agents, the artifacts and the hook as all broken, and every one of them was fine.
|
|
494
|
-
const all = layouts({ projectRoot, globalDir });
|
|
495
|
-
const stateAbs = stateDirs(all, projectRoot);
|
|
496
|
-
const stateRels = stateAbs.map(rel(projectRoot));
|
|
497
|
-
|
|
498
|
-
const checks = [
|
|
499
|
-
checkCore(v),
|
|
500
|
-
checkProfile(profile, pipelineMd != null),
|
|
501
|
-
acrossRuntimes(all, layout => checkAgents(profile, projectRoot, layout)),
|
|
502
|
-
checkGate(profile, projectRoot, stateAbs),
|
|
503
|
-
checkLocalArtifacts(projectRoot, stateRels),
|
|
504
|
-
checkHooks(projectRoot, globalDir, v.installMode, all),
|
|
505
|
-
acrossRuntimes(all, layout => checkRetrieval(profile, projectRoot, layout)),
|
|
506
|
-
checkDesign(profile, projectRoot),
|
|
507
|
-
checkIsolation(profile, projectRoot),
|
|
508
|
-
checkWorkflows(projectRoot, globalDir, v.installMode, all),
|
|
509
|
-
checkSpecs(specs),
|
|
510
|
-
];
|
|
511
|
-
|
|
512
|
-
const summary = { ok: 0, warn: 0, bad: 0, skip: 0 };
|
|
513
|
-
for (const c of checks) summary[c.status]++;
|
|
514
|
-
|
|
515
|
-
return {
|
|
516
|
-
project: projectRoot,
|
|
517
|
-
runtimes: all.map(l => ({ id: l.id, label: l.label, scope: l.scope,
|
|
518
|
-
capabilities: l.capabilities, legacy: !!l.legacy })),
|
|
519
|
-
versions: v,
|
|
520
|
-
profile: profile ? {
|
|
521
|
-
name: profile.name,
|
|
522
|
-
one_liner: profile.one_liner,
|
|
523
|
-
surfaces: (profile.surfaces || []).map(s => ({
|
|
524
|
-
key: s.key, label: s.label, agent: s.agent, path: s.path,
|
|
525
|
-
model: s.model, uses_design: s.uses_design, tools: s.tools,
|
|
526
|
-
})),
|
|
527
|
-
} : null,
|
|
528
|
-
specs,
|
|
529
|
-
checks,
|
|
530
|
-
summary,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
module.exports = { state, scanSpecs };
|