cohorte 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +15 -10
- package/bin/cli.js +15 -2
- package/core/agents/review.md +4 -2
- package/core/commands/cohorte-brainstorm.md +5 -1
- package/core/commands/cohorte-build.md +3 -1
- package/core/commands/cohorte-doctor.md +5 -3
- package/core/commands/cohorte-fix.md +6 -5
- package/core/commands/cohorte-fleet.md +104 -0
- package/core/commands/cohorte-intake.md +92 -0
- package/core/commands/cohorte-patch.md +6 -1
- package/core/commands/cohorte-retro.md +85 -0
- package/core/commands/cohorte-review.md +85 -23
- package/core/commands/cohorte-ship.md +2 -1
- package/core/commands/cohorte-spec.md +1 -1
- package/core/hooks/gate.py +10 -4
- package/core/workflows/audit.js +14 -2
- package/core/workflows/loop.js +641 -0
- package/core/workflows/refactor.js +21 -8
- package/core/workflows/review.js +92 -12
- package/dashboard/dist/assets/{index-D1rsbLat.js → index-vtFc6Gyc.js} +12 -12
- package/dashboard/dist/index.html +1 -1
- package/dashboard/server/doctor.js +8 -2
- package/dashboard/server/index.js +6 -1
- package/dashboard/server/kanban.js +15 -4
- package/dashboard/server/metrics.js +8 -1
- package/dashboard/server/runtime.js +20 -1
- package/install.ps1 +16 -333
- package/install.sh +27 -297
- package/package.json +1 -1
- package/profile/SCHEMA.md +45 -14
- package/profile/cohorte.config.template.yaml +1 -1
- package/scripts/kanban-move.sh +15 -5
- package/scripts/new-feature.sh.template +8 -1
- package/scripts/preflight.sh +10 -2
- package/scripts/remove-feature.sh.template +3 -1
- package/scripts/test-dashboard.mjs +53 -5
- package/scripts/test-gate.mjs +6 -0
- package/scripts/test-workflows.mjs +415 -6
- package/scripts/validate-core.mjs +68 -34
package/install.sh
CHANGED
|
@@ -43,8 +43,9 @@ install.sh — install the cohorte pipeline core.
|
|
|
43
43
|
|
|
44
44
|
Honours $CLAUDE_CONFIG_DIR for the global destination and $PIPELINE_REPO for the
|
|
45
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
|
|
47
|
-
|
|
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.
|
|
48
49
|
USAGE
|
|
49
50
|
exit 0 ;;
|
|
50
51
|
--) shift; break ;;
|
|
@@ -59,7 +60,12 @@ src=""
|
|
|
59
60
|
self="${0:-}"
|
|
60
61
|
self_dir=""
|
|
61
62
|
case "$self" in
|
|
62
|
-
|
|
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) ;;
|
|
63
69
|
esac
|
|
64
70
|
if [ -n "$self_dir" ] && [ -d "$self_dir/core" ]; then
|
|
65
71
|
src="$self_dir"
|
|
@@ -72,313 +78,37 @@ else
|
|
|
72
78
|
fi
|
|
73
79
|
[ -d "$src/core" ] || { echo "error: pipeline source not found (no core/ in $src)" >&2; exit 1; }
|
|
74
80
|
|
|
75
|
-
# --- delegate to the Node CLI
|
|
81
|
+
# --- require Node >= 18, then delegate to the Node CLI -----------------------
|
|
76
82
|
# Since 2.2.0 the commands in core/ are runtime-NEUTRAL sources: they carry capability
|
|
77
83
|
# conditionals (`<!-- cohorte:if subagents -->`) and path tokens (`<core>`, `<state>`) that
|
|
78
84
|
# the adapter resolves per coding agent. Copying them verbatim, as this script used to,
|
|
79
85
|
# would install prompts full of unresolved markers — an install that looks successful and
|
|
80
86
|
# instructs the model with text meant for a different runtime. There is no shell renderer,
|
|
81
|
-
# so
|
|
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.)
|
|
82
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
|
|
83
100
|
set -- install
|
|
84
101
|
[ "$mode" = "update" ] && set -- update
|
|
85
102
|
[ "$scope" = "global" ] && set -- "$@" --global
|
|
86
103
|
[ "$scope" = "project" ] && set -- "$@" "$target"
|
|
87
|
-
exec
|
|
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 $?
|
|
88
108
|
fi
|
|
89
|
-
echo "error: cohorte needs Node
|
|
109
|
+
echo "error: cohorte needs Node >= 18 to install." >&2
|
|
90
110
|
echo " The pipeline's commands are rendered per coding agent (Claude Code, Codex, Cursor," >&2
|
|
91
111
|
echo " Gemini CLI, OpenCode) at install time; there is no shell equivalent of that step," >&2
|
|
92
112
|
echo " and a raw copy would install prompts this runtime cannot follow." >&2
|
|
93
113
|
echo " Install Node, then: npm i -g cohorte && cohorte install$([ "$scope" = global ] && echo ' --global')" >&2
|
|
94
114
|
exit 1
|
|
95
|
-
|
|
96
|
-
# --- resolve the destination .claude dir ------------------------------------
|
|
97
|
-
if [ "$scope" = "global" ]; then
|
|
98
|
-
dest="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
99
|
-
else
|
|
100
|
-
dest="$target/.claude"
|
|
101
|
-
fi
|
|
102
|
-
mkdir -p "$dest"
|
|
103
|
-
|
|
104
|
-
# version stamp so a per-repo pointer can record which core it expects:
|
|
105
|
-
# the package.json semver, with the git sha for traceability on from-main installs
|
|
106
|
-
semver=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$src/package.json" 2>/dev/null | head -n 1)
|
|
107
|
-
sha=$(git -C "$src" rev-parse --short HEAD 2>/dev/null || true)
|
|
108
|
-
if [ -n "$semver" ] && [ -n "$sha" ]; then ver="$semver ($sha)"
|
|
109
|
-
elif [ -n "$semver" ]; then ver="$semver"
|
|
110
|
-
else ver="${sha:-unknown}"
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
|
-
copy_core() {
|
|
114
|
-
cp -R "$src/core/commands" "$dest/"
|
|
115
|
-
cp -R "$src/core/hooks" "$dest/"
|
|
116
|
-
cp -R "$src/core/templates" "$dest/"
|
|
117
|
-
cp -R "$src/core/workflows" "$dest/"
|
|
118
|
-
# A Python bytecode cache appears in a source checkout the moment anyone compiles
|
|
119
|
-
# or imports gate.py (CI does) and `cp -R` carries it along — machine- and
|
|
120
|
-
# interpreter-specific, and copy-over would never delete it later.
|
|
121
|
-
rm -rf "$dest/hooks/__pycache__"
|
|
122
|
-
# 0.1.19 renamed questionnaire-domain-brief.md → research-brief.md; drop the stale copy.
|
|
123
|
-
rm -f "$dest/templates/questionnaire-domain-brief.md"
|
|
124
|
-
mkdir -p "$dest/pipeline/scripts"
|
|
125
|
-
cp "$src/profile/PIPELINE.template.md" "$dest/pipeline/"
|
|
126
|
-
cp "$src/profile/SCHEMA.md" "$dest/pipeline/"
|
|
127
|
-
cp "$src/profile/cohorte.config.template.yaml" "$dest/pipeline/"
|
|
128
|
-
cp "$src"/scripts/*.template "$dest/pipeline/scripts/"
|
|
129
|
-
cp "$src/scripts/kanban-move.sh" "$dest/pipeline/scripts/"
|
|
130
|
-
cp "$src/scripts/preflight.sh" "$dest/pipeline/scripts/"
|
|
131
|
-
chmod +x "$dest/pipeline/scripts/kanban-move.sh" \
|
|
132
|
-
"$dest/pipeline/scripts/preflight.sh" 2>/dev/null || true
|
|
133
|
-
# 2.3.0 removed telemetry. Copy-over never deletes, so an existing install would keep an
|
|
134
|
-
# executable that still POSTs to the collector — scrub the script itself. The dead
|
|
135
|
-
# `telemetry:` block in the user's config is not this installer's to parse; the interactive
|
|
136
|
-
# /cohorte-update-pipeline deletes it (SCHEMA.md §Reconcile step 5).
|
|
137
|
-
rm -f "$dest/pipeline/scripts/telemetry-send.sh"
|
|
138
|
-
cp "$src/core/agents/implementer.template.md" "$dest/pipeline/"
|
|
139
|
-
[ -f "$src/CHANGELOG.md" ] && cp "$src/CHANGELOG.md" "$dest/pipeline/"
|
|
140
|
-
printf '%s\n' "$ver" > "$dest/pipeline/VERSION"
|
|
141
|
-
chmod +x "$dest/hooks/gate.py" 2>/dev/null || true
|
|
142
|
-
scrub_tdd_gate
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
# The TDD gate was removed in 0.1.6. Older installs have hooks/tdd_gate.py on disk and
|
|
146
|
-
# registered in settings.json — copy-over never deletes, and a registered hook whose file
|
|
147
|
-
# is gone errors on every Write/Edit, so scrub both.
|
|
148
|
-
scrub_tdd_gate() {
|
|
149
|
-
rm -f "$dest/hooks/tdd_gate.py"
|
|
150
|
-
[ -f "$dest/settings.json" ] || return 0
|
|
151
|
-
command -v python3 >/dev/null 2>&1 || return 0
|
|
152
|
-
python3 - "$dest/settings.json" <<'PY'
|
|
153
|
-
import json, sys
|
|
154
|
-
settings = sys.argv[1]
|
|
155
|
-
try:
|
|
156
|
-
with open(settings) as fh:
|
|
157
|
-
data = json.load(fh)
|
|
158
|
-
except Exception:
|
|
159
|
-
sys.exit(0)
|
|
160
|
-
pre = data.get("hooks", {}).get("PreToolUse")
|
|
161
|
-
if not isinstance(pre, list):
|
|
162
|
-
sys.exit(0)
|
|
163
|
-
kept = [e for e in pre if not any(
|
|
164
|
-
h.get("command", "").strip().endswith("tdd_gate.py") for h in e.get("hooks", []))]
|
|
165
|
-
if len(kept) != len(pre):
|
|
166
|
-
data["hooks"]["PreToolUse"] = kept
|
|
167
|
-
with open(settings, "w") as fh:
|
|
168
|
-
json.dump(data, fh, indent=2)
|
|
169
|
-
fh.write("\n")
|
|
170
|
-
print(" · removed the retired tdd_gate.py hook (file + settings registration)")
|
|
171
|
-
PY
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
# the fixed (non-rendered) agents: the dev review/release pipeline agents
|
|
175
|
-
copy_fixed_agents() {
|
|
176
|
-
mkdir -p "$dest/agents"
|
|
177
|
-
cp "$src/core/agents/review.md" "$src/core/agents/release.md" \
|
|
178
|
-
"$src/core/agents/profile-reader.md" \
|
|
179
|
-
"$dest/agents/"
|
|
180
|
-
# 1.5.0 removed the /smoke phase; copy-over never deletes, so scrub the orphan agent.
|
|
181
|
-
rm -f "$dest/agents/smoke.md" "$dest/commands/smoke.md"
|
|
182
|
-
# 1.4.0 removed /cycle and its workflow — and no installer ever scrubbed them, so every
|
|
183
|
-
# install since has kept offering a command that dispatches a workflow whose phases were
|
|
184
|
-
# later deleted. A dead command is worse than a missing one: the model can still fire it.
|
|
185
|
-
rm -f "$dest/commands/cycle.md" "$dest/workflows/cycle.js"
|
|
186
|
-
# 1.6.0 renamed /loop → /drive: Claude Code's own built-in /loop shadowed ours, so a leftover
|
|
187
|
-
# commands/loop.md is a command the user can never reach — scrub it rather than leave a decoy.
|
|
188
|
-
rm -f "$dest/commands/loop.md"
|
|
189
|
-
# 2.0.0 prefixed every command with `cohorte-`, which ends the shadowing problem for good.
|
|
190
|
-
# Copy-over never deletes, so all 13 bare names would survive an upgrade as decoys — and a
|
|
191
|
-
# stale /build is the worst kind: it still dispatches implementers, from a 1.x command file
|
|
192
|
-
# that knows nothing of this core's contract. /drive goes too (it became /cohorte-loop).
|
|
193
|
-
for c in align-ds audit brainstorm build doctor drive fix init-pipeline \
|
|
194
|
-
refactor review ship spec update-pipeline; do
|
|
195
|
-
rm -f "$dest/commands/$c.md"
|
|
196
|
-
done
|
|
197
|
-
# 0.1.19 split the bi-mode questionnaire-researcher into research-agent + questionnaire-architect;
|
|
198
|
-
# copy-over never deletes, so scrub the retired agent lest a dead subagent_type linger.
|
|
199
|
-
rm -f "$dest/agents/questionnaire-researcher.md"
|
|
200
|
-
scrub_research_questionnaire
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
# The research + questionnaire capability was removed. Older installs have its agents, commands,
|
|
204
|
-
# templates and template-step dirs on disk; copy-over never deletes, so scrub every orphan.
|
|
205
|
-
scrub_research_questionnaire() {
|
|
206
|
-
rm -f "$dest/agents/research-agent.md" \
|
|
207
|
-
"$dest/agents/questionnaire-architect.md" \
|
|
208
|
-
"$dest/agents/questionnaire-writer.md" \
|
|
209
|
-
"$dest/agents/questionnaire-validator.md" \
|
|
210
|
-
"$dest/commands/research.md" \
|
|
211
|
-
"$dest/commands/questionnaire.md" \
|
|
212
|
-
"$dest/templates/research-brief.md" \
|
|
213
|
-
"$dest/templates/questionnaire-blueprint.md" \
|
|
214
|
-
"$dest/templates/questionnaire-declaration.md" \
|
|
215
|
-
"$dest/templates/questionnaire-verdict.md"
|
|
216
|
-
rm -rf "$dest/templates/steps/research" "$dest/templates/steps/questionnaire"
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
# pipeline capability config is USER-level (vault, Notion DB, kanban boards) — it lives in
|
|
220
|
-
# ~/.claude regardless of install scope. Seed it only if neither the consolidated nor the
|
|
221
|
-
# legacy copy exists. This piped installer is non-interactive: it seeds disabled defaults;
|
|
222
|
-
# /cohorte-init-pipeline + /cohorte-update-pipeline wire it (the npm CLI's installer offers a quick interview instead).
|
|
223
|
-
seed_config() {
|
|
224
|
-
base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
225
|
-
cfg="$base/cohorte.config.yaml"
|
|
226
|
-
legacy=""
|
|
227
|
-
for n in thebidouille.config.yaml; do
|
|
228
|
-
[ -f "$base/$n" ] && { legacy="$base/$n"; break; }
|
|
229
|
-
done
|
|
230
|
-
if [ -f "$cfg" ]; then
|
|
231
|
-
echo " · kept your existing $cfg"
|
|
232
|
-
elif [ -n "$legacy" ]; then
|
|
233
|
-
echo " · found legacy $legacy — kept as-is (read as a fallback)."
|
|
234
|
-
echo " Run /cohorte-update-pipeline to migrate it into cohorte.config.yaml + wire the kanban."
|
|
235
|
-
else
|
|
236
|
-
mkdir -p "$base"
|
|
237
|
-
cp "$src/profile/cohorte.config.template.yaml" "$cfg"
|
|
238
|
-
echo " · seeded $cfg (disabled defaults — enable via /cohorte-init-pipeline or /cohorte-update-pipeline)"
|
|
239
|
-
fi
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
# Register the profile-driven gate hook in the GLOBAL settings.json. Idempotent: the
|
|
243
|
-
# hook reads each repo's own .claude/gate-config.json (and no-ops where absent),
|
|
244
|
-
# so one registration serves every project.
|
|
245
|
-
register_global_hook() {
|
|
246
|
-
python3 - "$dest/settings.json" "$dest/hooks/gate.py" <<'PY'
|
|
247
|
-
import json, sys
|
|
248
|
-
settings, gate = sys.argv[1], sys.argv[2]
|
|
249
|
-
# The matcher MUST cover Task as well as Bash: gate.py's preflight phase gate
|
|
250
|
-
# keys off tool_name == "Task" (the `preflight` block in a repo's
|
|
251
|
-
# gate-config.json). A Bash-only matcher never delivers a Task dispatch to the
|
|
252
|
-
# hook, so that gate silently never fires — it was dead code from 1.3.0 to 1.3.1.
|
|
253
|
-
MATCHER = "Bash|Task"
|
|
254
|
-
base = gate.rsplit("/", 1)[-1]
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def is_gate(entry):
|
|
258
|
-
# Trailing-quote tolerant: the Windows form is `py "C:\...\gate.py"`, and a
|
|
259
|
-
# bare .endswith() missed it — which is how repeat installs accumulated a
|
|
260
|
-
# duplicate registration every time (gate.py then ran once per copy).
|
|
261
|
-
return any(
|
|
262
|
-
(h.get("command") or "").strip().rstrip('"').endswith(base)
|
|
263
|
-
for h in entry.get("hooks", [])
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
try:
|
|
268
|
-
with open(settings) as fh:
|
|
269
|
-
data = json.load(fh)
|
|
270
|
-
if not isinstance(data, dict):
|
|
271
|
-
data = {}
|
|
272
|
-
except Exception:
|
|
273
|
-
data = {}
|
|
274
|
-
pre = data.setdefault("hooks", {}).setdefault("PreToolUse", [])
|
|
275
|
-
# Reconcile rather than append-if-absent: drop every existing gate.py
|
|
276
|
-
# registration, then add exactly one. Idempotent, collapses duplicates older
|
|
277
|
-
# installers left behind, and upgrades a stale "Bash"-only matcher in place —
|
|
278
|
-
# an append-if-absent would find the stale entry and skip, pinning the bug.
|
|
279
|
-
kept = [e for e in pre if not is_gate(e)]
|
|
280
|
-
kept.append({"matcher": MATCHER,
|
|
281
|
-
"hooks": [{"type": "command", "command": "python3 " + gate}]})
|
|
282
|
-
data["hooks"]["PreToolUse"] = kept
|
|
283
|
-
with open(settings, "w") as fh:
|
|
284
|
-
json.dump(data, fh, indent=2)
|
|
285
|
-
fh.write("\n")
|
|
286
|
-
print("ok")
|
|
287
|
-
PY
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
# Bump only the core_version in a repo's committed .claude/pipeline.json (bundled mode).
|
|
291
|
-
# Leaves every other field intact; no-ops if the pointer is absent or has no core_version.
|
|
292
|
-
bump_pointer_version() {
|
|
293
|
-
ptr="$1"; newver="$2"
|
|
294
|
-
[ -f "$ptr" ] || return 0
|
|
295
|
-
python3 - "$ptr" "$newver" <<'PY'
|
|
296
|
-
import json, sys
|
|
297
|
-
ptr, newver = sys.argv[1], sys.argv[2]
|
|
298
|
-
try:
|
|
299
|
-
with open(ptr) as fh:
|
|
300
|
-
data = json.load(fh)
|
|
301
|
-
except Exception:
|
|
302
|
-
sys.exit(0)
|
|
303
|
-
if isinstance(data, dict) and "core_version" in data:
|
|
304
|
-
data["core_version"] = newver
|
|
305
|
-
with open(ptr, "w") as fh:
|
|
306
|
-
json.dump(data, fh, indent=2, ensure_ascii=False)
|
|
307
|
-
fh.write("\n")
|
|
308
|
-
PY
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if [ "$scope" = "global" ]; then
|
|
312
|
-
if [ "$mode" = "install" ]; then
|
|
313
|
-
echo "→ installing pipeline core GLOBALLY into $dest"
|
|
314
|
-
else
|
|
315
|
-
echo "→ updating pipeline core GLOBALLY in $dest (keeping global settings.json)"
|
|
316
|
-
fi
|
|
317
|
-
copy_fixed_agents
|
|
318
|
-
copy_core
|
|
319
|
-
hook_state=$(register_global_hook || echo "skipped")
|
|
320
|
-
seed_config
|
|
321
|
-
cat <<EOF
|
|
322
|
-
|
|
323
|
-
✓ pipeline core installed globally into $dest (version $ver)
|
|
324
|
-
gate hook: $hook_state (reads each repo's .claude/gate-config.json; silent where absent)
|
|
325
|
-
|
|
326
|
-
The commands (/cohorte-init-pipeline, /cohorte-brainstorm, /cohorte-build …) and the review/release agents are now
|
|
327
|
-
available in EVERY project on this machine — nothing is copied per repo.
|
|
328
|
-
|
|
329
|
-
Per repo:
|
|
330
|
-
1. Open the project in Claude Code.
|
|
331
|
-
2. Run /cohorte-init-pipeline — it generates PIPELINE.md, renders the surface agents, writes
|
|
332
|
-
.claude/gate-config.json, and drops a committed .claude/pipeline.json pointer so
|
|
333
|
-
teammates know to install the global core ($REPO_URL).
|
|
334
|
-
3. Commit PIPELINE.md + .claude/, then /cohorte-brainstorm to start a feature.
|
|
335
|
-
|
|
336
|
-
Code retrieval (Serena — the default provider /cohorte-init-pipeline wires per repo):
|
|
337
|
-
uv tool install -p 3.13 serena-agent # once per machine
|
|
338
|
-
Make sure ~/.local/bin is on PATH (uv tool update-shell) — otherwise the
|
|
339
|
-
registered MCP server silently fails to start.
|
|
340
|
-
|
|
341
|
-
Global kanban config, user-scoped — optional:
|
|
342
|
-
· One consolidated file: ~/.claude/cohorte.config.yaml (don't hand-edit it).
|
|
343
|
-
· /cohorte-init-pipeline (new project) and /cohorte-update-pipeline (existing) wire it for you: creating +
|
|
344
|
-
syncing an Obsidian kanban board of the pipeline in your shared vault.
|
|
345
|
-
EOF
|
|
346
|
-
exit 0
|
|
347
|
-
fi
|
|
348
|
-
|
|
349
|
-
if [ "$mode" = "install" ]; then
|
|
350
|
-
echo "→ installing pipeline core into $dest"
|
|
351
|
-
copy_fixed_agents
|
|
352
|
-
copy_core
|
|
353
|
-
seed_config
|
|
354
|
-
mkdir -p "$target/specs"
|
|
355
|
-
[ -f "$target/specs/_template.md" ] || cp "$src/core/templates/spec.template.md" "$target/specs/_template.md"
|
|
356
|
-
cat <<EOF
|
|
357
|
-
|
|
358
|
-
✓ pipeline core installed into $dest (version $ver)
|
|
359
|
-
|
|
360
|
-
Next:
|
|
361
|
-
1. Open the project in Claude Code.
|
|
362
|
-
2. Run /cohorte-init-pipeline — it detects your stack, asks the gaps, and generates
|
|
363
|
-
PIPELINE.md + renders one implementer agent per surface.
|
|
364
|
-
3. Commit PIPELINE.md, then /cohorte-brainstorm to start a feature.
|
|
365
|
-
|
|
366
|
-
Code retrieval (Serena — the default provider /cohorte-init-pipeline wires per repo):
|
|
367
|
-
uv tool install -p 3.13 serena-agent # once per machine
|
|
368
|
-
Make sure ~/.local/bin is on PATH (uv tool update-shell) — otherwise the
|
|
369
|
-
registered MCP server silently fails to start.
|
|
370
|
-
|
|
371
|
-
Prefer one shared core across all your repos? Re-run with --global.
|
|
372
|
-
EOF
|
|
373
|
-
else
|
|
374
|
-
echo "→ updating pipeline core in $dest (keeping your PIPELINE.md + rendered agents)"
|
|
375
|
-
copy_core
|
|
376
|
-
copy_fixed_agents 2>/dev/null || true
|
|
377
|
-
seed_config
|
|
378
|
-
bump_pointer_version "$dest/pipeline.json" "$ver"
|
|
379
|
-
cat <<EOF
|
|
380
|
-
|
|
381
|
-
✓ core refreshed to $ver. Your PIPELINE.md, rendered surface agents, gate-config.json and
|
|
382
|
-
settings.json were left as-is. Re-run /cohorte-init-pipeline if your stack changed.
|
|
383
|
-
EOF
|
|
384
|
-
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code, Codex CLI, Cursor, Gemini CLI and OpenCode — install the core, run /cohorte-init-pipeline, and it adapts to your project's stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cohorte": "bin/cli.js"
|
package/profile/SCHEMA.md
CHANGED
|
@@ -185,8 +185,13 @@ proposing a split: split the surface that actually dominates wall-clock, not the
|
|
|
185
185
|
## Measuring cost — what's slow vs what's expensive
|
|
186
186
|
|
|
187
187
|
`pipeline-metrics.jsonl` records **wall-clock seconds** per phase batch (§Specialization) — it tells you
|
|
188
|
-
what's SLOW.
|
|
189
|
-
|
|
188
|
+
what's SLOW. Tokens are recorded only where they can be read honestly: the **workflow paths**
|
|
189
|
+
(`loop.js`, `review.js`) stamp an approximate `tokens` field per batch from the runtime's own
|
|
190
|
+
counter (`budget.spent()` deltas), and the loop's return carries a per-round breakdown in its
|
|
191
|
+
`history`. The **conversational** commands still record none — a lead cannot reliably read a
|
|
192
|
+
subagent's token count, and a guessed number is worse than a missing one. The dashboard sums
|
|
193
|
+
whatever is stamped (a token-less line aggregates as 0, rendered as absent, never as "free").
|
|
194
|
+
For exact spend, use Claude Code's own accounting:
|
|
190
195
|
|
|
191
196
|
- **`/cost`** (built-in, zero setup) — reports per-**subagent** and per-**slash-command** share of your usage
|
|
192
197
|
over the last 24 h / 7 d (e.g. _"Top subagents: frontend 7 %, backend 4 % · Top skills: /cohorte-build 1 %,
|
|
@@ -237,11 +242,15 @@ it, the dashboard boards on it, and the kanban backfill maps it to a column. Six
|
|
|
237
242
|
| `blocked` | a round gave up here (non-convergent, no verdict, not implementable) | an automated driver, if any | yes, with the reason named |
|
|
238
243
|
| `shipped` | the PR is open; the status flip is part of the release commit | `/cohorte-ship` | no |
|
|
239
244
|
|
|
240
|
-
**`in-progress` and `blocked` are
|
|
241
|
-
|
|
242
|
-
`
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
**`in-progress` and `blocked` are driver states.** No conversational command writes them — the
|
|
246
|
+
human-driven cycle moves `frozen` → `in-review` → `shipped`. Their producer is the **loop
|
|
247
|
+
workflow** (`core/workflows/loop.js`, `/cohorte-loop` — the successor of the 2.2.0-retired
|
|
248
|
+
conversational driver): it stamps `in-progress` at each round, `in-review` when a run ends at
|
|
249
|
+
zero blocking findings, and `blocked` when a round gives up (non-convergent, unreviewed
|
|
250
|
+
surfaces, dead implementers, a contract-change finding), with the reason in
|
|
251
|
+
`specs/reports/<id>.loop.json`. External drivers may write them too. Every reader routes on
|
|
252
|
+
them either way, and a stamp on a spec with no front-matter stays a silent no-op — no driver
|
|
253
|
+
dies over a status line.
|
|
245
254
|
|
|
246
255
|
**`kind` — feature (default) or `patch`.** Orthogonal to `status`, and the only other front-matter
|
|
247
256
|
field commands route on. `/cohorte-patch` freezes `specs/patch-<slug>.md` with `kind: patch` from
|
|
@@ -348,11 +357,14 @@ project has *decided*. Without somewhere for those, every `/cohorte-spec` re-dis
|
|
|
348
357
|
`specs/_decisions.md` (from `core/templates/decisions.template.md`) is that place, deliberately small:
|
|
349
358
|
|
|
350
359
|
- **Append-only, one line per decision, ≤ ~160 chars:**
|
|
351
|
-
`- <YYYY-MM-DD> · <area> · <decision> — because <reason> · <
|
|
360
|
+
`- <YYYY-MM-DD> · <area> · <decision> — because <reason> · <origin>`, where `<origin>` is the
|
|
361
|
+
`feature_id` that decided it — or the originating command (`retro`) when no single feature owns
|
|
362
|
+
it. Reversal never edits a line:
|
|
352
363
|
append a superseding one (`· supersedes <date> <area>`) and move the old one to `## Superseded`. When
|
|
353
364
|
`## Live` passes ~100 lines, sweep the superseded ones down.
|
|
354
365
|
- **Written by** `/cohorte-spec` at freeze (the decisions that outlive the feature — typically 0–3 lines, and
|
|
355
|
-
zero is a normal outcome)
|
|
366
|
+
zero is a normal outcome), `/cohorte-build` §1.5 when it adds or splits a surface, and
|
|
367
|
+
`/cohorte-retro` §4 when the human ratifies a convention rule (one line per adopted rule).
|
|
356
368
|
- **Read by the deciding stages only** — `/cohorte-brainstorm` (so the panel argues about the idea, not about
|
|
357
369
|
settled ground), `/cohorte-spec` (so a new spec does not silently un-decide something), `/cohorte-audit` (standing
|
|
358
370
|
decisions are part of the rulebook it audits against).
|
|
@@ -525,16 +537,21 @@ itself changes in ways `/cohorte-build` §1.5 can't auto-grow (e.g. package mana
|
|
|
525
537
|
|
|
526
538
|
## Workflows — deterministic multi-agent runs (opt-in)
|
|
527
539
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
540
|
+
Four scripts run under the Claude Code Workflow runtime instead of the lead reasoning out the
|
|
541
|
+
fan-out turn by turn: `<core>/workflows/review.js`, `audit.js`, `refactor.js` — each the
|
|
542
|
+
**workflow variant** of its same-named conversational command — plus `loop.js`
|
|
543
|
+
(`/cohorte-loop`), which has **no conversational form at all** (below). For the variant pairs,
|
|
544
|
+
the conversational commands (`/cohorte-review`, `/cohorte-audit`, `/cohorte-refactor`)
|
|
532
545
|
**remain the default path and the fallback** — a workflow runs only when the human explicitly asks
|
|
533
546
|
for it ("run the review workflow"), and requires Claude Code ≥ **2.1.154** with workflows
|
|
534
547
|
enabled.
|
|
535
548
|
`/cohorte-doctor` reports which path a session will take. The interactive commands (`/cohorte-init-pipeline`,
|
|
536
549
|
`/cohorte-brainstorm`, `/cohorte-spec`) and the dispatch-only ones (`/cohorte-build`, `/cohorte-ship`) have **no** workflow variant on
|
|
537
550
|
purpose: they're interviews or already a single parallel dispatch — a script adds nothing.
|
|
551
|
+
`/cohorte-loop` does not change that: it **consumes** `/cohorte-build`'s outputs (the frozen
|
|
552
|
+
spec, `readiness.json`, the lead-authored contract, `build.json`) — it is not a build variant,
|
|
553
|
+
and adding one would put the lead-only steps (§1.5 reconcile, §2 contract authoring) inside a
|
|
554
|
+
script that cannot ask.
|
|
538
555
|
|
|
539
556
|
Shared design, all four scripts:
|
|
540
557
|
|
|
@@ -564,10 +581,24 @@ Shared design, all four scripts:
|
|
|
564
581
|
- **`refactor.js`** — big domains only (it skips domains with a handful of open items — the
|
|
565
582
|
conversational `/cohorte-refactor` is cheaper there): `shared` first and alone, then the other domains'
|
|
566
583
|
implementers in parallel, each verified per-domain.
|
|
584
|
+
- **`loop.js`** (`/cohorte-loop`) — build → review → [fix → review]* for ONE feature, unattended and
|
|
585
|
+
resumable. Preconditions it verifies and refuses to work around: frozen/`in-review` spec, a fresh
|
|
586
|
+
`readiness.json` at `READY`/`RESERVATIONS`, the contract on disk, every readiness surface owned by
|
|
587
|
+
the profile. Round exits, in order: child abort relayed → `unreviewed` non-empty → `blocking == 0`
|
|
588
|
+
⇒ ship → same blocking-item identity two consecutive rounds ⇒ treading water → `maxRounds`
|
|
589
|
+
(default 5). A blocking finding on the contract file aborts (`contract-change` — lead-only, per
|
|
590
|
+
`/cohorte-fix` §1). It calls the review **workflow** per round and reads the same `verdict.json`
|
|
591
|
+
contract the conversational `/cohorte-review` §3 writes; it stamps `in-progress` while running,
|
|
592
|
+
`in-review` on ship, `blocked` on a give-up. **Workflow-only, no command file, ever**
|
|
593
|
+
(`validate-core.mjs` pins it): if the runtime is unavailable it refuses explicitly rather than
|
|
594
|
+
degrading to a lead re-reasoning the fan-out every round at session-model prices.
|
|
567
595
|
- **No input mid-run.** A workflow runs to completion without questions; anything interactive
|
|
568
596
|
(contract changes, human decisions) belongs to the conversational path. The gate hook still
|
|
569
597
|
fires on workflow subagents (see
|
|
570
|
-
§Preflight) — in unattended runs its asks become denies.
|
|
598
|
+
§Preflight) — in unattended runs its asks become denies. Know what that means for edits:
|
|
599
|
+
workflow subagents run in **`acceptEdits` whatever the session mode**, so for the length of a
|
|
600
|
+
run — and `loop.js` runs long, unattended stretches — `hooks/gate.py` is the only brake on
|
|
601
|
+
what agents write. That is stated here rather than discovered.
|
|
571
602
|
- **Permissions:** `/cohorte-init-pipeline` and `/cohorte-update-pipeline` extend the generated `settings.json`
|
|
572
603
|
`allow` list with what workflow agents need (the quiet commands, the shipped
|
|
573
604
|
`pipeline/scripts/*.sh`, read-only git incl. `git rev-parse`, and the retrieval provider's MCP
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ~/.claude/cohorte.config.yaml — GLOBAL, user-scoped config for the Cohorte
|
|
2
2
|
# pipeline capabilities. One machine, every project. NOT tied to any project's PIPELINE.md.
|
|
3
3
|
#
|
|
4
|
-
# Seeded once by the installer (
|
|
4
|
+
# Seeded once by the installer (cohorte install) and never clobbered on update. Wired
|
|
5
5
|
# interactively by /cohorte-init-pipeline (new project) and /cohorte-update-pipeline (existing project) —
|
|
6
6
|
# you should never need to hand-edit it.
|
|
7
7
|
#
|
package/scripts/kanban-move.sh
CHANGED
|
@@ -44,11 +44,17 @@ else
|
|
|
44
44
|
fi
|
|
45
45
|
pr=""; title=""; project=""; profile="PIPELINE.md"
|
|
46
46
|
while [ $# -gt 0 ]; do
|
|
47
|
+
# A flag with no value must exit 2 (usage) like every other usage error — a bare
|
|
48
|
+
# `shift 2` fails under set -e with exit 1, outside the 0/2/3 contract callers branch on.
|
|
47
49
|
case "$1" in
|
|
48
|
-
--pr)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
--pr|--title|--project|--profile)
|
|
51
|
+
[ $# -ge 2 ] || { echo "error: $1 needs a value" >&2; echo "$usage" >&2; exit 2; } ;;
|
|
52
|
+
esac
|
|
53
|
+
case "$1" in
|
|
54
|
+
--pr) pr="$2"; shift 2 ;;
|
|
55
|
+
--title) title="$2"; shift 2 ;;
|
|
56
|
+
--project) project="$2"; shift 2 ;;
|
|
57
|
+
--profile) profile="$2"; shift 2 ;;
|
|
52
58
|
*) echo "error: unknown flag $1" >&2; echo "$usage" >&2; exit 2 ;;
|
|
53
59
|
esac
|
|
54
60
|
done
|
|
@@ -246,7 +252,11 @@ ID="$id" COL="$col" PR="$pr" TITLE="${title:-$id}" awk '
|
|
|
246
252
|
pr = ENVIRON["PR"]; title = ENVIRON["TITLE"]
|
|
247
253
|
n = 0; card = ""; found = 0; colseen = 0; sn = 0; lastblank = 0
|
|
248
254
|
}
|
|
249
|
-
|
|
255
|
+
# Strip CR at capture: a CRLF board (Windows-synced vault) otherwise fails the
|
|
256
|
+
# `h == col` heading compare and exits 3 on a column that exists — while the
|
|
257
|
+
# config parser in this same script already strips \r. Emitting LF-only is fine
|
|
258
|
+
# (Obsidian reads both).
|
|
259
|
+
{ sub(/\r$/, ""); lines[++n] = $0 }
|
|
250
260
|
END {
|
|
251
261
|
# pass 1: extract the FIRST card block tagged #id; mark every block for removal
|
|
252
262
|
for (i = 1; i <= n; i++) {
|
|
@@ -53,8 +53,15 @@ web_port=$((__WEB_PORT_BASE__ + slot))
|
|
|
53
53
|
|
|
54
54
|
echo "→ feature '$id' → slot $slot (db $db_name · api :$api_port · web :$web_port)"
|
|
55
55
|
echo "→ creating worktree $worktree_dir on $branch (off __DEFAULT_BRANCH__)"
|
|
56
|
+
# Branch off the REMOTE ref when it exists: `git fetch origin <branch>` updates
|
|
57
|
+
# origin/<branch>, never the local one, so branching off the local ref silently
|
|
58
|
+
# started every feature from wherever the local default branch last sat.
|
|
56
59
|
git -C "$repo_root" fetch --quiet origin __DEFAULT_BRANCH__ || true
|
|
57
|
-
|
|
60
|
+
base=__DEFAULT_BRANCH__
|
|
61
|
+
if git -C "$repo_root" rev-parse --verify -q "origin/__DEFAULT_BRANCH__" >/dev/null 2>&1; then
|
|
62
|
+
base="origin/__DEFAULT_BRANCH__"
|
|
63
|
+
fi
|
|
64
|
+
git -C "$repo_root" worktree add -b "$branch" "$worktree_dir" "$base"
|
|
58
65
|
|
|
59
66
|
# Replace (or append) KEY=VALUE in a dotenv file — one definitive line per key.
|
|
60
67
|
set_env() {
|
package/scripts/preflight.sh
CHANGED
|
@@ -43,6 +43,9 @@ for cmd in "$@"; do
|
|
|
43
43
|
exit 1
|
|
44
44
|
fi
|
|
45
45
|
done
|
|
46
|
+
# All-empty arguments verified nothing — stamping green here would gate reviews on a
|
|
47
|
+
# check that never ran. Same failure as zero arguments, and the same exit.
|
|
48
|
+
[ "$n" -gt 0 ] || { echo "preflight: every command argument was empty — nothing was verified" >&2; exit 2; }
|
|
46
49
|
|
|
47
50
|
# Stamp for the gate.py phase gate: epoch + HEAD sha of the checkout we verified.
|
|
48
51
|
# The stamp MUST land where gate.py reads it: <main checkout>/.claude. CLAUDE_PROJECT_DIR
|
|
@@ -75,8 +78,13 @@ if [ -n "$tmpidx" ]; then
|
|
|
75
78
|
# same window or they compute different trees for the same content. `date -d` is GNU and
|
|
76
79
|
# `date -v` is BSD — try both, and if neither exists just skip the touch (the digest is
|
|
77
80
|
# still correct for anything not edited in the last few seconds).
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
# LOCAL time on purpose: `touch -t` interprets its stamp as local time, so a
|
|
82
|
+
# UTC-formatted stamp future-dates the index anywhere west of UTC — which makes
|
|
83
|
+
# git trust the stat cache of files edited THIS second, the exact race the
|
|
84
|
+
# backdate exists to prevent. (gate.py's side uses os.utime with an epoch, which
|
|
85
|
+
# has no timezone to get wrong.)
|
|
86
|
+
stamp=$(date -d '5 seconds ago' +%Y%m%d%H%M.%S 2>/dev/null \
|
|
87
|
+
|| date -v-5S +%Y%m%d%H%M.%S 2>/dev/null || echo "")
|
|
80
88
|
[ -n "$stamp" ] && touch -t "$stamp" "$tmpidx" 2>/dev/null || true
|
|
81
89
|
else
|
|
82
90
|
rm -f "$tmpidx" # a 0-byte index is a corrupt index
|
|
@@ -28,7 +28,9 @@ registry="$repo_root/.worktrees/slots.tsv"
|
|
|
28
28
|
branch="__BRANCH_PREFIX__${id}"
|
|
29
29
|
|
|
30
30
|
echo "→ removing worktree $worktree_dir"
|
|
31
|
-
|
|
31
|
+
# Porcelain + fixed-string + whole-line: the human-readable list matched any worktree
|
|
32
|
+
# whose path merely CONTAINS this one (auth vs auth-2fa), and dots in the path were regex.
|
|
33
|
+
if git -C "$repo_root" worktree list --porcelain | grep -Fxq "worktree $worktree_dir"; then
|
|
32
34
|
git -C "$repo_root" worktree remove "$worktree_dir" "${@:3}"
|
|
33
35
|
else
|
|
34
36
|
echo " (no such worktree registered — skipping)"
|