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/scripts/preflight.sh
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
#
|
|
3
|
-
# preflight.sh — deterministic phase gate for /cohorte-review.
|
|
4
|
-
#
|
|
5
|
-
# Runs the profile's mechanical checks (typecheck, lint, tests — whatever the caller
|
|
6
|
-
# passes) BEFORE any agent is spawned. A red gate means the caller aborts and relays
|
|
7
|
-
# the raw failure — dispatching reviewers onto code that doesn't even compile burns
|
|
8
|
-
# their whole run on noise a compiler already printed for free.
|
|
9
|
-
#
|
|
10
|
-
# preflight.sh <report-file> "<cmd>" ["<cmd>"...]
|
|
11
|
-
#
|
|
12
|
-
# - Each command runs through `sh -c`, all output appended to <report-file> — the bulk
|
|
13
|
-
# never enters the calling agent's context.
|
|
14
|
-
# - First failure: prints the last 40 lines of the report raw to stderr and exits 1.
|
|
15
|
-
# The caller must stop there — no agents.
|
|
16
|
-
# - All green: writes `<project>/.claude/preflight.ok` ("<epoch> <HEAD sha> <tree digest>")
|
|
17
|
-
# — the stamp `hooks/gate.py` checks (gate-config.json `preflight` block) before letting
|
|
18
|
-
# review agents dispatch.
|
|
19
|
-
#
|
|
20
|
-
# The stamp is a LOCAL, GITIGNORED artifact. Versioning it breaks the gate outright: the
|
|
21
|
-
# stamp records the pre-commit sha, committing it moves HEAD, so the checked-out stamp can
|
|
22
|
-
# never match its own commit — every dispatch then reports "HEAD moved" on a clean tree.
|
|
23
|
-
# A tracked stamp also rides into every new worktree, handing it a green it never earned.
|
|
24
|
-
|
|
25
|
-
set -u
|
|
26
|
-
|
|
27
|
-
report="${1:?usage: preflight.sh <report-file> \"<cmd>\" [\"<cmd>\"...]}"
|
|
28
|
-
shift
|
|
29
|
-
[ "$#" -gt 0 ] || { echo "preflight: no commands given" >&2; exit 2; }
|
|
30
|
-
|
|
31
|
-
mkdir -p "$(dirname "$report")" 2>/dev/null || true
|
|
32
|
-
: > "$report"
|
|
33
|
-
|
|
34
|
-
n=0
|
|
35
|
-
for cmd in "$@"; do
|
|
36
|
-
[ -n "$cmd" ] || continue
|
|
37
|
-
n=$((n + 1))
|
|
38
|
-
printf '\n$ %s\n' "$cmd" >> "$report"
|
|
39
|
-
if ! sh -c "$cmd" >> "$report" 2>&1; then
|
|
40
|
-
echo "PREFLIGHT FAIL — $cmd" >&2
|
|
41
|
-
echo "--- last 40 lines of $report ---" >&2
|
|
42
|
-
tail -40 "$report" >&2
|
|
43
|
-
exit 1
|
|
44
|
-
fi
|
|
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; }
|
|
49
|
-
|
|
50
|
-
# Stamp for the gate.py phase gate: epoch + HEAD sha of the checkout we verified.
|
|
51
|
-
# The stamp MUST land where gate.py reads it: <main checkout>/.claude. CLAUDE_PROJECT_DIR
|
|
52
|
-
# is only exported to hooks — in an agent's Bash call it is unset, and a bare `.` would
|
|
53
|
-
# drop the stamp in the feature worktree where the gate never looks. git-common-dir
|
|
54
|
-
# resolves to the MAIN checkout's .git from any worktree.
|
|
55
|
-
proj="${CLAUDE_PROJECT_DIR:-$(dirname "$(git rev-parse --git-common-dir 2>/dev/null || echo ./.git)")}"
|
|
56
|
-
sha=$(git rev-parse HEAD 2>/dev/null || echo none)
|
|
57
|
-
|
|
58
|
-
# Content digest of the state we actually verified: the git TREE id of the working tree,
|
|
59
|
-
# built in a throwaway index so nothing touches the real one. The sha alone is the wrong
|
|
60
|
-
# key — the reviewed tree is normally DIRTY (uncommitted feature work), so committing it
|
|
61
|
-
# invalidates the stamp without changing a line of code, while an implementer's edit
|
|
62
|
-
# changes every line without moving HEAD. A tree id is content-addressed: it survives a
|
|
63
|
-
# commit of the same content and dies on any real edit (including new untracked files).
|
|
64
|
-
# `.claude`/`.cohorte` (stamps, metrics) and `specs` (DoD ticks, report buffer) are excluded — the
|
|
65
|
-
# pipeline writes those itself between the preflight and the dispatch it must not invalidate.
|
|
66
|
-
# gate.py recomputes this identically; any change here must land there too.
|
|
67
|
-
digest=none
|
|
68
|
-
tmpidx=$(mktemp 2>/dev/null || echo "")
|
|
69
|
-
if [ -n "$tmpidx" ]; then
|
|
70
|
-
# Seed from the real index (not `read-tree HEAD`) so its stat cache is preserved and
|
|
71
|
-
# `add` only re-hashes files that actually changed.
|
|
72
|
-
idx=$(git rev-parse --git-path index 2>/dev/null || echo "")
|
|
73
|
-
if [ -n "$idx" ] && [ -f "$idx" ]; then
|
|
74
|
-
cp "$idx" "$tmpidx" 2>/dev/null || true
|
|
75
|
-
# Backdate the copy, for the reason spelled out in gate.py's tree_digest(): git trusts an
|
|
76
|
-
# entry's cached stat data only when its mtime predates the index file's, so a copy stamped
|
|
77
|
-
# `now` makes a file edited in this same second look clean. Both sides must age it by the
|
|
78
|
-
# same window or they compute different trees for the same content. `date -d` is GNU and
|
|
79
|
-
# `date -v` is BSD — try both, and if neither exists just skip the touch (the digest is
|
|
80
|
-
# still correct for anything not edited in the last few seconds).
|
|
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 "")
|
|
88
|
-
[ -n "$stamp" ] && touch -t "$stamp" "$tmpidx" 2>/dev/null || true
|
|
89
|
-
else
|
|
90
|
-
rm -f "$tmpidx" # a 0-byte index is a corrupt index
|
|
91
|
-
fi
|
|
92
|
-
# Drop the excluded paths from the throwaway index entirely: an `add` exclude only stops
|
|
93
|
-
# them being *updated*, so anything already tracked there (a committed stamp, a spec)
|
|
94
|
-
# would still land in the tree and shift the digest.
|
|
95
|
-
GIT_INDEX_FILE="$tmpidx" git rm --cached -r -q --ignore-unmatch -- .claude .cohorte specs > /dev/null 2>&1 || true
|
|
96
|
-
if GIT_INDEX_FILE="$tmpidx" git add -A -- . ':(exclude).claude' ':(exclude).cohorte' ':(exclude)specs' > /dev/null 2>&1; then
|
|
97
|
-
digest=$(GIT_INDEX_FILE="$tmpidx" git write-tree 2>/dev/null || echo none)
|
|
98
|
-
fi
|
|
99
|
-
rm -f "$tmpidx" "$tmpidx.lock" 2>/dev/null || true
|
|
100
|
-
fi
|
|
101
|
-
[ -n "$digest" ] || digest=none
|
|
102
|
-
# Stamp BOTH the main checkout and the cwd: gate.py reads COHORTE_PROJECT_DIR /
|
|
103
|
-
# CLAUDE_PROJECT_DIR, which is the worktree when the session was opened there and the
|
|
104
|
-
# main checkout when it wasn't — the two disagree, and either layout is supported.
|
|
105
|
-
# Stamp every state dir that EXISTS (`.cohorte` on a non-Claude runtime, `.claude` on a
|
|
106
|
-
# Claude one, both where a repo is driven from both), falling back to `.claude` when the
|
|
107
|
-
# repo has neither yet — gate.py's state_path() probes in that same order.
|
|
108
|
-
now=$(date +%s)
|
|
109
|
-
last=""
|
|
110
|
-
for d in "$proj" "$(pwd)"; do
|
|
111
|
-
[ "$d" = "$last" ] && continue # same dir twice in the main checkout
|
|
112
|
-
last="$d"
|
|
113
|
-
wrote=0
|
|
114
|
-
for s in .cohorte .claude; do
|
|
115
|
-
[ -d "$d/$s" ] || continue
|
|
116
|
-
printf '%s %s %s\n' "$now" "$sha" "$digest" > "$d/$s/preflight.ok" 2>/dev/null && wrote=1
|
|
117
|
-
done
|
|
118
|
-
if [ "$wrote" -eq 0 ]; then
|
|
119
|
-
mkdir -p "$d/.claude" 2>/dev/null || true
|
|
120
|
-
printf '%s %s %s\n' "$now" "$sha" "$digest" > "$d/.claude/preflight.ok" 2>/dev/null || true
|
|
121
|
-
fi
|
|
122
|
-
done
|
|
123
|
-
|
|
124
|
-
echo "PREFLIGHT PASS ($n checks green) — full log: $report"
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# remove-feature.sh — tear down a feature's isolated worktree. Rendered from a
|
|
4
|
-
# template by /cohorte-init-pipeline (double-underscore tokens from PIPELINE.md §isolation —
|
|
5
|
-
# spelled out here so /cohorte-doctor's unrendered-token check never trips on this comment).
|
|
6
|
-
#
|
|
7
|
-
# Usage: scripts/remove-feature.sh <feature_id> [--drop-db]
|
|
8
|
-
#
|
|
9
|
-
# Removes the ../__PROJECT_SLUG__-<id> worktree, frees its slot, and (with
|
|
10
|
-
# --drop-db) drops its database. The db is kept by default. The shared stack and
|
|
11
|
-
# the main checkout are never touched.
|
|
12
|
-
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
|
|
15
|
-
id="${1:-}"
|
|
16
|
-
drop_db="no"
|
|
17
|
-
[[ "${2:-}" == "--drop-db" ]] && drop_db="yes"
|
|
18
|
-
|
|
19
|
-
if [[ -z "$id" ]]; then
|
|
20
|
-
echo "usage: scripts/remove-feature.sh <feature_id> [--drop-db]" >&2
|
|
21
|
-
exit 1
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
repo_root="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)"
|
|
25
|
-
worktree_dir="$(dirname "$repo_root")/__PROJECT_SLUG__-${id}"
|
|
26
|
-
db_name="__PROJECT_SLUG___$(echo "$id" | tr -c 'a-zA-Z0-9' '_' | sed 's/_*$//')"
|
|
27
|
-
registry="$repo_root/.worktrees/slots.tsv"
|
|
28
|
-
branch="__BRANCH_PREFIX__${id}"
|
|
29
|
-
|
|
30
|
-
echo "→ removing worktree $worktree_dir"
|
|
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
|
|
34
|
-
git -C "$repo_root" worktree remove "$worktree_dir" "${@:3}"
|
|
35
|
-
else
|
|
36
|
-
echo " (no such worktree registered — skipping)"
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/$branch"; then
|
|
40
|
-
if git -C "$repo_root" branch -d "$branch" 2>/dev/null; then
|
|
41
|
-
echo "→ deleted merged branch $branch"
|
|
42
|
-
else
|
|
43
|
-
echo " ! branch $branch is not fully merged — kept it." >&2
|
|
44
|
-
echo " delete manually when ready: git branch -D $branch" >&2
|
|
45
|
-
fi
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
if [[ -f "$registry" ]]; then
|
|
49
|
-
tmp="$(mktemp)"
|
|
50
|
-
awk -F'\t' -v id="$id" '$1!=id' "$registry" >"$tmp" && mv "$tmp" "$registry"
|
|
51
|
-
echo "→ freed slot for '$id'"
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
# __DB_DROP_STANZA__
|
|
55
|
-
|
|
56
|
-
echo "✓ feature '$id' torn down"
|
package/scripts/test-adapter.mjs
DELETED
|
@@ -1,437 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Behavioural tests for the runtime adapter — core/adapter/render.js + core/runtimes/*.json.
|
|
3
|
-
//
|
|
4
|
-
// The adapter is where a single set of source prompts becomes N runtime-specific ones. Its
|
|
5
|
-
// failure mode is silent and expensive: a dropped conditional ships a Claude-only instruction
|
|
6
|
-
// to a runtime that cannot follow it, a leaked marker turns doctrine into visible noise, and a
|
|
7
|
-
// wrong frontmatter key is read by the model as prose. None of that raises an error anywhere —
|
|
8
|
-
// it just makes the pipeline quietly wrong on four runtimes out of five.
|
|
9
|
-
//
|
|
10
|
-
// node scripts/test-adapter.mjs
|
|
11
|
-
|
|
12
|
-
import { readFileSync, readdirSync, mkdtempSync, mkdirSync, rmSync, existsSync } from "node:fs";
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { createRequire } from "node:module";
|
|
18
|
-
|
|
19
|
-
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
20
|
-
const require = createRequire(import.meta.url);
|
|
21
|
-
const adapter = require(join(root, "core", "adapter", "render.js"));
|
|
22
|
-
|
|
23
|
-
let failures = 0;
|
|
24
|
-
const check = (name, cond, detail = "") => {
|
|
25
|
-
if (cond) console.log(` ✓ ${name}`);
|
|
26
|
-
else { failures++; console.error(` ✗ ${name}${detail ? ` — ${detail}` : ""}`); }
|
|
27
|
-
};
|
|
28
|
-
const group = (name) => console.log(name);
|
|
29
|
-
const throws = (fn) => { try { fn(); return false; } catch { return true; } };
|
|
30
|
-
|
|
31
|
-
const RUNTIMES = adapter.listRuntimes();
|
|
32
|
-
const tmps = [];
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------- registry ---
|
|
35
|
-
group("registry — every runtime declares what the renderer reads");
|
|
36
|
-
|
|
37
|
-
check("at least the five supported runtimes ship", RUNTIMES.length >= 5, RUNTIMES.join(","));
|
|
38
|
-
for (const id of RUNTIMES) {
|
|
39
|
-
const rt = adapter.loadRuntime(id);
|
|
40
|
-
const ok = rt.id === id
|
|
41
|
-
&& typeof rt.label === "string"
|
|
42
|
-
&& rt.scopes && rt.scopes.global && rt.scopes.project
|
|
43
|
-
&& rt.command && ["md", "toml", "skill"].includes(rt.command.format)
|
|
44
|
-
&& Array.isArray(rt.command.frontmatter)
|
|
45
|
-
&& rt.capabilities && typeof rt.capabilities.subagents === "boolean"
|
|
46
|
-
&& typeof rt.capabilities.hooks === "boolean"
|
|
47
|
-
&& typeof rt.capabilities.workflows === "boolean"
|
|
48
|
-
&& typeof rt.capabilities.tool_restriction === "boolean";
|
|
49
|
-
check(`${id}: complete and well-typed`, ok);
|
|
50
|
-
}
|
|
51
|
-
check("unknown runtime is an error, not a silent default",
|
|
52
|
-
throws(() => adapter.loadRuntime("nope")));
|
|
53
|
-
|
|
54
|
-
// A runtime that claims a capability it cannot back is the one mistake the whole design rests
|
|
55
|
-
// on: every `cohorte:if` branch trusts these booleans literally, and an over-claim ships the
|
|
56
|
-
// strict doctrine to a runtime that cannot enforce it. Pinned against the vendor docs — see
|
|
57
|
-
// each runtime's `docs` field; revisit these three lines whenever one of them is re-read.
|
|
58
|
-
const withCap = (c) => RUNTIMES.filter((id) => adapter.loadRuntime(id).capabilities[c]).sort().join();
|
|
59
|
-
check("hooks claimed everywhere but OpenCode (plugins are not a blocking hook)",
|
|
60
|
-
withCap("hooks") === "claude,codex,cursor,gemini", withCap("hooks"));
|
|
61
|
-
check("only Claude Code claims workflows", withCap("workflows") === "claude");
|
|
62
|
-
// Not a capability to branch on: a HARD requirement. The pipeline's isolation guarantee is the
|
|
63
|
-
// subagent boundary, so a runtime without them cannot be supported — and must be refused loudly
|
|
64
|
-
// rather than rendered into a pipeline whose central promise is silently absent.
|
|
65
|
-
check("every target runtime has real subagents",
|
|
66
|
-
withCap("subagents") === RUNTIMES.slice().sort().join(), withCap("subagents"));
|
|
67
|
-
check("a runtime declaring no subagents is refused, not degraded",
|
|
68
|
-
throws(() => adapter.assertSupported({ id: "x", capabilities: { subagents: false } })));
|
|
69
|
-
check("…and one that has them passes the same guard",
|
|
70
|
-
!throws(() => adapter.assertSupported({ id: "x", capabilities: { subagents: true } })));
|
|
71
|
-
|
|
72
|
-
// A hook runtime must declare how to talk to it, and a no-ask runtime must be flagged: gate.py
|
|
73
|
-
// escalates ask→deny there, and getting this backwards silently lets a gated command run.
|
|
74
|
-
for (const id of RUNTIMES) {
|
|
75
|
-
const rt = adapter.loadRuntime(id);
|
|
76
|
-
if (!rt.capabilities.hooks) { check(`${id}: declares no hook contract`, !rt.hook); continue; }
|
|
77
|
-
check(`${id}: hook contract is complete`, !!rt.hook && !!rt.hook.event
|
|
78
|
-
&& ["claude", "cursor", "gemini"].includes(rt.hook.format)
|
|
79
|
-
&& typeof rt.hook.supports_ask === "boolean"
|
|
80
|
-
&& !!rt.scopes.project.hooks_config);
|
|
81
|
-
}
|
|
82
|
-
check("the ask tier is claimed only where the runtime honours it",
|
|
83
|
-
RUNTIMES.filter((id) => (adapter.loadRuntime(id).hook || {}).supports_ask).sort().join()
|
|
84
|
-
=== "claude,cursor");
|
|
85
|
-
|
|
86
|
-
// ------------------------------------------------------------ conditionals ---
|
|
87
|
-
group("conditionals — the branch that survives is the branch that is true");
|
|
88
|
-
|
|
89
|
-
// Synthetic capability sets, not real runtimes: the branch logic must stay correct however the
|
|
90
|
-
// vendors' feature matrix moves, and a unit test of the parser should not depend on which
|
|
91
|
-
// runtimes happen to ship today.
|
|
92
|
-
const rich = { id: "rich", capabilities: { subagents: true, hooks: true, workflows: true, tool_restriction: true } };
|
|
93
|
-
const bare = { id: "bare", capabilities: { subagents: false, hooks: false, workflows: false, tool_restriction: false } };
|
|
94
|
-
|
|
95
|
-
const basic = ["<!-- cohorte:if hooks -->", "H", "<!-- cohorte:else -->", "NOH", "<!-- cohorte:endif -->"].join("\n");
|
|
96
|
-
check("if/else keeps the taken branch", adapter.applyConditionals(basic, rich).trim() === "H");
|
|
97
|
-
check("if/else keeps the else branch", adapter.applyConditionals(basic, bare).trim() === "NOH");
|
|
98
|
-
|
|
99
|
-
const neg = ["<!-- cohorte:if !hooks -->", "ADVISORY", "<!-- cohorte:endif -->"].join("\n");
|
|
100
|
-
check("negation works", adapter.applyConditionals(neg, bare).trim() === "ADVISORY"
|
|
101
|
-
&& adapter.applyConditionals(neg, rich).trim() === "");
|
|
102
|
-
|
|
103
|
-
const byId = ["<!-- cohorte:if runtime:claude -->", "CC", "<!-- cohorte:endif -->"].join("\n");
|
|
104
|
-
check("runtime:<id> targets one runtime",
|
|
105
|
-
adapter.applyConditionals(byId, adapter.loadRuntime("claude")).trim() === "CC"
|
|
106
|
-
&& adapter.applyConditionals(byId, adapter.loadRuntime("cursor")).trim() === "");
|
|
107
|
-
|
|
108
|
-
const or = ["<!-- cohorte:if hooks workflows -->", "X", "<!-- cohorte:endif -->"].join("\n");
|
|
109
|
-
check("a multi-term condition is an OR", adapter.applyConditionals(or, rich).trim() === "X");
|
|
110
|
-
|
|
111
|
-
const nested = [
|
|
112
|
-
"<!-- cohorte:if subagents -->", "A",
|
|
113
|
-
"<!-- cohorte:if hooks -->", "B", "<!-- cohorte:else -->", "C", "<!-- cohorte:endif -->",
|
|
114
|
-
"<!-- cohorte:endif -->",
|
|
115
|
-
].join("\n");
|
|
116
|
-
check("nesting resolves inner branches inside a taken outer one",
|
|
117
|
-
adapter.applyConditionals(nested, rich).trim().split("\n").join() === "A,B");
|
|
118
|
-
check("a dropped outer branch drops its inner branches whole",
|
|
119
|
-
adapter.applyConditionals(nested, bare).trim() === "");
|
|
120
|
-
|
|
121
|
-
check("an unknown capability is an error, not a silently-false branch",
|
|
122
|
-
throws(() => adapter.applyConditionals("<!-- cohorte:if telepathy -->\nx\n<!-- cohorte:endif -->", rich)));
|
|
123
|
-
check("an unclosed if is an error", throws(() => adapter.applyConditionals("<!-- cohorte:if hooks -->\nx", rich)));
|
|
124
|
-
check("a stray endif is an error", throws(() => adapter.applyConditionals("<!-- cohorte:endif -->", rich)));
|
|
125
|
-
check("two elses in one if is an error", throws(() => adapter.applyConditionals(
|
|
126
|
-
["<!-- cohorte:if hooks -->", "<!-- cohorte:else -->", "<!-- cohorte:else -->", "<!-- cohorte:endif -->"].join("\n"), rich)));
|
|
127
|
-
|
|
128
|
-
// Every marker in the real source must be resolvable for EVERY runtime — an unknown term in
|
|
129
|
-
// a command nobody rendered yet would surface as an install-time crash for one runtime only.
|
|
130
|
-
group("source prompts — every marker resolves for every runtime");
|
|
131
|
-
const sources = [
|
|
132
|
-
...readdirSync(join(root, "core", "commands")).map((f) => ["commands", f]),
|
|
133
|
-
...readdirSync(join(root, "core", "agents")).map((f) => ["agents", f]),
|
|
134
|
-
].filter(([, f]) => f.endsWith(".md"));
|
|
135
|
-
for (const id of RUNTIMES) {
|
|
136
|
-
const rt = adapter.loadRuntime(id);
|
|
137
|
-
let bad = null;
|
|
138
|
-
for (const [dir, f] of sources) {
|
|
139
|
-
const src = readFileSync(join(root, "core", dir, f), "utf8");
|
|
140
|
-
try { adapter.applyConditionals(adapter.parseFrontmatter(src).body, rt); }
|
|
141
|
-
catch (e) { bad = `${dir}/${f}: ${e.message}`; break; }
|
|
142
|
-
}
|
|
143
|
-
check(`${id}: all ${sources.length} source files render`, !bad, bad || "");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ------------------------------------------------------------ full install ---
|
|
147
|
-
group("install — what each runtime actually gets on disk");
|
|
148
|
-
|
|
149
|
-
const home = mkdtempSync(join(tmpdir(), "cohorte-home-"));
|
|
150
|
-
const proj = mkdtempSync(join(tmpdir(), "cohorte-proj-"));
|
|
151
|
-
tmps.push(home, proj);
|
|
152
|
-
spawnSync("git", ["init", "-q", "."], { cwd: proj });
|
|
153
|
-
const run = spawnSync(process.execPath, [join(root, "bin", "cli.js"), "install",
|
|
154
|
-
`--runtime=${RUNTIMES.join(",")}`], {
|
|
155
|
-
cwd: proj, env: { ...process.env, HOME: home, CLAUDE_CONFIG_DIR: "" }, encoding: "utf8",
|
|
156
|
-
});
|
|
157
|
-
check("the installer exits clean for every runtime at once", run.status === 0,
|
|
158
|
-
(run.stderr || "").slice(0, 400));
|
|
159
|
-
|
|
160
|
-
for (const id of RUNTIMES) {
|
|
161
|
-
const rt = adapter.loadRuntime(id);
|
|
162
|
-
// resolvePaths reads ~ from the real homedir, so re-point it at the sandbox.
|
|
163
|
-
const p = adapter.resolvePaths(rt, "project", proj);
|
|
164
|
-
const fix = (s) => s && s.replace(process.env.HOME, home);
|
|
165
|
-
const cmdDir = fix(p.commands);
|
|
166
|
-
const buildFile = join(cmdDir, `cohorte-build${rt.command.ext}`);
|
|
167
|
-
check(`${id}: commands landed in ${rt.scopes.project.commands}`, existsSync(buildFile));
|
|
168
|
-
if (!existsSync(buildFile)) continue;
|
|
169
|
-
const build = readFileSync(buildFile, "utf8");
|
|
170
|
-
|
|
171
|
-
check(`${id}: no unresolved marker leaked into the output`, !/cohorte:(if|else|endif)/.test(build));
|
|
172
|
-
check(`${id}: the runtime preamble is present`, build.includes(`**Runtime: ${rt.label}.**`));
|
|
173
|
-
|
|
174
|
-
// Parallel dispatch is the doctrine on every runtime now; the sequential-persona fallback was
|
|
175
|
-
// removed in 2.2.0 along with any suggestion that a lead can simulate the boundary by hand.
|
|
176
|
-
check(`${id}: dispatches surfaces in parallel`, build.includes("IN PARALLEL"));
|
|
177
|
-
check(`${id}: no trace of the removed persona fallback`,
|
|
178
|
-
!build.includes("ONE PERSONA AT A TIME") && !build.includes("adopt it verbatim"));
|
|
179
|
-
|
|
180
|
-
// A path the runtime does not have is a path the model will fail to read, silently.
|
|
181
|
-
if (id !== "claude") {
|
|
182
|
-
check(`${id}: no hardcoded .claude path left in the prose`,
|
|
183
|
-
!/(?<![\w/.-])~?\/?\.claude\//.test(build), (build.match(/.{0,60}\.claude\/.{0,40}/) || [""])[0]);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// The preamble must describe the gate the way it actually works here — a blocking hook that
|
|
187
|
-
// fires regardless, or an advisory check the agent has to call. Getting this backwards is the
|
|
188
|
-
// worst single error the adapter can make: it tells the model a safety property holds when it
|
|
189
|
-
// does not.
|
|
190
|
-
if (rt.capabilities.hooks) {
|
|
191
|
-
check(`${id}: the gate is described as a blocking ${rt.hook.event} hook`,
|
|
192
|
-
build.includes("is registered as a blocking") && build.includes(rt.hook.event));
|
|
193
|
-
check(`${id}: the missing confirmation tier is stated`,
|
|
194
|
-
build.includes("no confirmation tier") === !rt.hook.supports_ask);
|
|
195
|
-
} else {
|
|
196
|
-
check(`${id}: the gate is described as an explicit check`, build.includes("gate.py --check"));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Frontmatter the runtime does not understand is prose the model reads as instruction.
|
|
200
|
-
if (rt.command.format === "md" || rt.command.format === "skill") {
|
|
201
|
-
const fm = adapter.parseFrontmatter(build).keys.map(([k]) => k);
|
|
202
|
-
check(`${id}: only supported frontmatter keys survive`,
|
|
203
|
-
fm.every((k) => rt.command.frontmatter.includes(k)), fm.join(","));
|
|
204
|
-
if (rt.command.format === "skill") {
|
|
205
|
-
// A skill is matched on its frontmatter `name`, both for explicit invocation and for
|
|
206
|
-
// implicit selection. Without it the file installs and is simply never reachable.
|
|
207
|
-
check(`${id}: the skill carries the name it is invoked by`,
|
|
208
|
-
fm.includes("name") && /^name: cohorte-build$/m.test(build));
|
|
209
|
-
check(`${id}: skills are repo-scoped, so a clone gets the commands`,
|
|
210
|
-
rt.scopes.project.commands.startsWith(".agents/"));
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
check(`${id}: emitted as ${rt.command.format}, not markdown frontmatter`,
|
|
214
|
-
!build.startsWith("---\n") && /^description = "/m.test(build) && /^prompt = '''/m.test(build));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Placeholder substitution: a token the runtime never expands must be explained, not left
|
|
218
|
-
// to look like it works.
|
|
219
|
-
if (rt.command.args && rt.command.args !== "$ARGUMENTS") {
|
|
220
|
-
check(`${id}: $ARGUMENTS rewritten to ${rt.command.args}`,
|
|
221
|
-
build.includes(rt.command.args) && !build.includes("$ARGUMENTS"));
|
|
222
|
-
} else if (!rt.command.args) {
|
|
223
|
-
check(`${id}: the unsubstituted placeholder is explained in the preamble`,
|
|
224
|
-
build.includes("does not substitute placeholders"));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
for (const excluded of rt.exclude_commands || []) {
|
|
228
|
-
check(`${id}: ${excluded} is not installed (it cannot run here)`,
|
|
229
|
-
!existsSync(join(cmdDir, `${excluded}${rt.command.ext}`)));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const agentsDir = fix(p.agents) || join(fix(p.core), "agents");
|
|
233
|
-
const reviewFile = join(agentsDir, `review${rt.agent.ext || ".md"}`);
|
|
234
|
-
check(`${id}: the review agent exists as ${rt.agent.format}`, existsSync(reviewFile));
|
|
235
|
-
if (existsSync(reviewFile)) {
|
|
236
|
-
const review = readFileSync(reviewFile, "utf8");
|
|
237
|
-
// The reviewer must never be able to fix what it reports. Where the runtime can enforce
|
|
238
|
-
// that, the rendered file must carry the restriction; where it cannot, the body must say
|
|
239
|
-
// so — a reviewer that silently gains write access destroys the fix loop's evidence.
|
|
240
|
-
if (rt.agent.readonly_key) {
|
|
241
|
-
check(`${id}: the reviewer is pinned read-only (${rt.agent.readonly_key})`,
|
|
242
|
-
review.includes(rt.agent.readonly_key) && review.includes(rt.agent.readonly_value));
|
|
243
|
-
} else if (rt.capabilities.tool_restriction) {
|
|
244
|
-
// Claude expresses it as the absence of write tools in the `tools:` list.
|
|
245
|
-
check(`${id}: the reviewer's tool list carries no write tool`,
|
|
246
|
-
/^tools:.*$/m.test(review) && !/^tools:.*(Write|Edit|Bash)/m.test(review));
|
|
247
|
-
} else {
|
|
248
|
-
check(`${id}: the reviewer is told read-only is on it`,
|
|
249
|
-
review.includes("read-only **by discipline**") || review.includes("read-only by discipline"));
|
|
250
|
-
}
|
|
251
|
-
// An Anthropic model alias in another vendor's agent file either errors or is ignored.
|
|
252
|
-
check(`${id}: no Anthropic model alias leaked into the agent file`,
|
|
253
|
-
id === "claude" || !/^\s*model\s*[:=]/m.test(review), (review.match(/^.*model.*$/m) || [""])[0]);
|
|
254
|
-
}
|
|
255
|
-
// Every non-Claude runtime shares one `.cohorte` core, so this registry must ACCUMULATE.
|
|
256
|
-
// A single-record file let each install erase the previous runtime's entry.
|
|
257
|
-
const rtJson = join(fix(p.core), "pipeline", "runtimes.json");
|
|
258
|
-
check(`${id}: survives in runtimes.json after the other installs`, existsSync(rtJson)
|
|
259
|
-
&& !!JSON.parse(readFileSync(rtJson, "utf8"))[id]);
|
|
260
|
-
check(`${id}: the gate script ships with the core`, existsSync(join(fix(p.core), "hooks", "gate.py")));
|
|
261
|
-
check(`${id}: workflows ship only where a workflow engine exists`,
|
|
262
|
-
existsSync(join(fix(p.core), "workflows")) === rt.capabilities.workflows);
|
|
263
|
-
|
|
264
|
-
// Templates are resolved in place at install time, so a shared core would let the LAST
|
|
265
|
-
// runtime installed decide what every other one reads. Each core is its own directory
|
|
266
|
-
// precisely to prevent that; assert the resolution actually matches this runtime.
|
|
267
|
-
const step = join(fix(p.core), "templates", "steps", "init-pipeline", "04-write-render.md");
|
|
268
|
-
if (existsSync(step)) {
|
|
269
|
-
const text = readFileSync(step, "utf8");
|
|
270
|
-
check(`${id}: templates carry no unresolved marker`, !/cohorte:(if|else|endif)/.test(text));
|
|
271
|
-
check(`${id}: the settings/hook step matches this runtime`,
|
|
272
|
-
text.includes("Write `.claude/settings.json`") === (rt.capabilities.hooks && id !== "codex"));
|
|
273
|
-
if (id === "codex") {
|
|
274
|
-
check("codex: init generates native project agents and MCP config",
|
|
275
|
-
text.includes('.codex/agents/<agent>.toml') && text.includes('.codex/config.toml')
|
|
276
|
-
&& !text.includes('render `<agents>/<agent>.md`') && !text.includes('`review.md`'));
|
|
277
|
-
const schema = readFileSync(join(p.core, 'pipeline', 'SCHEMA.md'), 'utf8');
|
|
278
|
-
check("codex: reconciliation preserves TOML destinations and Codex MCP context",
|
|
279
|
-
schema.includes('`<agents>/<agent>.toml`') && schema.includes('--context codex')
|
|
280
|
-
&& !schema.includes('claude mcp add'));
|
|
281
|
-
const doctor = readFileSync(join(cmdDir, 'cohorte-doctor', 'SKILL.md'), 'utf8');
|
|
282
|
-
check("codex: doctor understands inherited models and TOML",
|
|
283
|
-
doctor.includes('Missing `model` means inheritance, not an error')
|
|
284
|
-
&& !doctor.includes('sonnet/haiku/haiku'));
|
|
285
|
-
const update = readFileSync(join(cmdDir, 'cohorte-update-pipeline', 'SKILL.md'), 'utf8');
|
|
286
|
-
check("codex: update selects its runtime and does not demand Claude workflows",
|
|
287
|
-
update.includes('update --runtime=codex') && !update.includes('claude mcp add')
|
|
288
|
-
&& !update.includes('`<core>/workflows/` + `agents/profile-reader.md`'));
|
|
289
|
-
const template = readFileSync(join(p.core, 'pipeline', 'PIPELINE.template.md'), 'utf8');
|
|
290
|
-
check("codex: new profiles default to inheritance",
|
|
291
|
-
/model: inherit/.test(template) && !/model: sonnet/.test(template));
|
|
292
|
-
const implementer = readFileSync(join(p.core, 'pipeline', 'implementer.template.md'), 'utf8');
|
|
293
|
-
check('codex: implementers are not accidentally pinned to a read-only sandbox',
|
|
294
|
-
!/^sandbox_mode = "read-only"/m.test(implementer));
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
group("codex — global core, project-local surfaces, native TOML");
|
|
300
|
-
{
|
|
301
|
-
const env = { ...process.env, HOME: home, CLAUDE_CONFIG_DIR: '' };
|
|
302
|
-
const r = spawnSync(process.execPath,
|
|
303
|
-
[join(root, 'bin/cli.js'), 'install', '--global', '--runtime=codex'],
|
|
304
|
-
{ cwd: proj, env, encoding: 'utf8' });
|
|
305
|
-
check('global Codex install succeeds', r.status === 0, r.stderr);
|
|
306
|
-
const init = readFileSync(join(home, '.agents/skills/cohorte-init-pipeline/SKILL.md'), 'utf8');
|
|
307
|
-
check('global skill resolves surfaces relative to whichever project invokes it',
|
|
308
|
-
init.includes('`<agents>` = `.codex/agents`') && !init.includes(proj));
|
|
309
|
-
check('generic agents retain their global destination',
|
|
310
|
-
existsSync(join(home, '.codex/agents/review.toml'))
|
|
311
|
-
&& init.includes('`<fixed-agents>` = `~/.codex/agents`'));
|
|
312
|
-
const registry = JSON.parse(readFileSync(join(home, '.cohorte/codex/pipeline/runtimes.json'), 'utf8'));
|
|
313
|
-
check('global registry does not pin surface agents to the installer cwd',
|
|
314
|
-
registry.codex.paths.surface_agents === '.codex/agents'
|
|
315
|
-
&& registry.codex.paths.agent_ext === '.toml');
|
|
316
|
-
const hook = JSON.parse(readFileSync(join(home, '.codex/hooks.json'), 'utf8')).hooks.PreToolUse[0];
|
|
317
|
-
check('installed Codex hook matches real shell and subagent calls',
|
|
318
|
-
['Bash', 'spawn_agent', 'Agent'].every(n => new RegExp(hook.matcher).test(n)));
|
|
319
|
-
const rt = adapter.loadRuntime('codex');
|
|
320
|
-
const paths = adapter.resolvePaths(rt, 'global', proj);
|
|
321
|
-
const render = model => adapter.renderAgent({
|
|
322
|
-
source: `---\nname: example\ndescription: Test agent\nmodel: ${model}\nmodel_reasoning_effort: high\n---\nHandle literal ''' in instructions.`,
|
|
323
|
-
name: 'example', runtime: rt, paths, projectRoot: proj,
|
|
324
|
-
}).content;
|
|
325
|
-
const explicit = render('gpt-5.6-terra');
|
|
326
|
-
check('explicit Codex model and reasoning choices survive rendering',
|
|
327
|
-
explicit.includes('model = "gpt-5.6-terra"') && explicit.includes('model_reasoning_effort = "high"'));
|
|
328
|
-
check('inherit and legacy Anthropic aliases never become executable pins',
|
|
329
|
-
['inherit', 'sonnet', 'haiku', 'opus'].every(m => !/^model =/m.test(render(m))));
|
|
330
|
-
const python = [process.env.COHORTE_TEST_PYTHON, 'python3', 'python'].filter(Boolean)
|
|
331
|
-
.find(p => spawnSync(p, ['-c', 'import tomllib']).status === 0);
|
|
332
|
-
if (!python) { check('Python 3.11+ available to validate emitted TOML', false); }
|
|
333
|
-
else {
|
|
334
|
-
const parsed = spawnSync(python, ['-c', 'import sys,tomllib; print(tomllib.loads(sys.stdin.read())["developer_instructions"])'],
|
|
335
|
-
{ input: explicit, encoding: 'utf8' });
|
|
336
|
-
check('instructions containing triple apostrophes remain valid TOML',
|
|
337
|
-
parsed.status === 0 && parsed.stdout.includes("literal '''"), parsed.stderr);
|
|
338
|
-
const native = spawnSync(python, ['-c',
|
|
339
|
-
'import pathlib,sys,tomllib; [tomllib.loads(p.read_text()) for p in pathlib.Path(sys.argv[1]).glob("*.toml")]',
|
|
340
|
-
join(home, '.codex/agents')], { encoding: 'utf8' });
|
|
341
|
-
check('all installed generic agents parse as TOML', native.status === 0, native.stderr);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// The project state — gate config, preflight stamp, metrics — describes the repo, not the
|
|
346
|
-
// agent driving it, and must NOT fork per runtime.
|
|
347
|
-
group("state — one project, one gate config");
|
|
348
|
-
const stateDirs = new Set(RUNTIMES.map((id) => adapter.stateDir(adapter.loadRuntime(id))));
|
|
349
|
-
check("every non-Claude runtime shares one state dir", stateDirs.size === 2
|
|
350
|
-
&& stateDirs.has(".claude") && stateDirs.has(".cohorte"));
|
|
351
|
-
check("but each keeps its own rendered core",
|
|
352
|
-
new Set(RUNTIMES.map((id) => adapter.loadRuntime(id).scopes.project.core)).size === RUNTIMES.length);
|
|
353
|
-
|
|
354
|
-
// A config dir with a space in it is not exotic: a desktop host puts CLAUDE_CONFIG_DIR under
|
|
355
|
-
// `~/Library/Application Support/…`. An unquoted path there splits in the shell, python reports
|
|
356
|
-
// `can't open file '/Users/x/Library/Application'`, and EVERY tool call in the session fails —
|
|
357
|
-
// including the ones the human would need to undo it. Assert the registration is quoted, per
|
|
358
|
-
// runtime, and that the installer still recognises its own entry (or a re-install duplicates it).
|
|
359
|
-
group("hook registration — paths with spaces");
|
|
360
|
-
{
|
|
361
|
-
const spacedHome = mkdtempSync(join(tmpdir(), "cohorte home-"));
|
|
362
|
-
const spacedProj = mkdtempSync(join(tmpdir(), "cohorte proj-"));
|
|
363
|
-
tmps.push(spacedHome, spacedProj);
|
|
364
|
-
spawnSync("git", ["init", "-q", "."], { cwd: spacedProj });
|
|
365
|
-
const env = { ...process.env, HOME: spacedHome, CLAUDE_CONFIG_DIR: join(spacedHome, ".claude") };
|
|
366
|
-
const args = [join(root, "bin", "cli.js"), "install", `--runtime=${RUNTIMES.join(",")}`];
|
|
367
|
-
const first = spawnSync(process.execPath, args, { cwd: spacedProj, env, encoding: "utf8" });
|
|
368
|
-
check("installs into a path containing a space", first.status === 0,
|
|
369
|
-
(first.stderr || "").slice(0, 300));
|
|
370
|
-
// Claude registers its hook only on a GLOBAL install — project-scope settings.json is
|
|
371
|
-
// /cohorte-init-pipeline's job — so exercise that scope too.
|
|
372
|
-
const gargs = [join(root, "bin", "cli.js"), "install", "--global", "--runtime=claude"];
|
|
373
|
-
spawnSync(process.execPath, gargs, { cwd: spacedProj, env, encoding: "utf8" });
|
|
374
|
-
// Re-install BOTH: the reconcile must match its own quoted entry, or every run stacks another.
|
|
375
|
-
spawnSync(process.execPath, args, { cwd: spacedProj, env, encoding: "utf8" });
|
|
376
|
-
spawnSync(process.execPath, gargs, { cwd: spacedProj, env, encoding: "utf8" });
|
|
377
|
-
|
|
378
|
-
for (const id of RUNTIMES) {
|
|
379
|
-
const rt = adapter.loadRuntime(id);
|
|
380
|
-
if (!rt.capabilities.hooks) continue;
|
|
381
|
-
const cfgSpec = (id === "claude" ? rt.scopes.global : rt.scopes.project).hooks_config;
|
|
382
|
-
const cfgPath = cfgSpec.startsWith("~/")
|
|
383
|
-
? join(spacedHome, cfgSpec.slice(2)) : join(spacedProj, cfgSpec);
|
|
384
|
-
if (!existsSync(cfgPath)) { check(`${id}: hook config written`, false, cfgPath); continue; }
|
|
385
|
-
const hooks = JSON.parse(readFileSync(cfgPath, "utf8")).hooks || {};
|
|
386
|
-
const entries = hooks[rt.hook.event] || [];
|
|
387
|
-
const cmds = entries.flatMap(e => e.command ? [e.command] : (e.hooks || []).map(h => h.command));
|
|
388
|
-
const ours = cmds.filter(c => /gate\.py/.test(c));
|
|
389
|
-
check(`${id}: the gate path is quoted`, ours.length > 0 && ours.every(c => /"[^"]*gate\.py"/.test(c)),
|
|
390
|
-
ours.join(" | "));
|
|
391
|
-
check(`${id}: re-installing does not stack a second registration`, ours.length === 1,
|
|
392
|
-
`${ours.length} entries`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// CLAUDE_CONFIG_DIR moves Claude Code's whole tree — a desktop host points it at
|
|
397
|
-
// `~/Library/Application Support/…`. The registry declares those paths as `~/.claude`, and
|
|
398
|
-
// resolving them from the homedir instead of the override split the install in half: the core
|
|
399
|
-
// went to the REAL `~/.claude` while the hook was registered in the override. A scratch install
|
|
400
|
-
// therefore wrote into the user's actual global core, silently.
|
|
401
|
-
group("CLAUDE_CONFIG_DIR is honoured, not half-honoured");
|
|
402
|
-
{
|
|
403
|
-
const home = mkdtempSync(join(tmpdir(), "cohorte-home-"));
|
|
404
|
-
const cfg = mkdtempSync(join(tmpdir(), "cohorte-cfg-")); // deliberately NOT under home
|
|
405
|
-
const proj = mkdtempSync(join(tmpdir(), "cohorte-proj-"));
|
|
406
|
-
tmps.push(home, cfg, proj);
|
|
407
|
-
spawnSync("git", ["init", "-q", "."], { cwd: proj });
|
|
408
|
-
const r = spawnSync(process.execPath,
|
|
409
|
-
[join(root, "bin", "cli.js"), "install", "--global", "--runtime=claude"],
|
|
410
|
-
{ cwd: proj, env: { ...process.env, HOME: home, CLAUDE_CONFIG_DIR: cfg }, encoding: "utf8" });
|
|
411
|
-
|
|
412
|
-
check("the global install succeeds under an overridden config dir", r.status === 0,
|
|
413
|
-
(r.stderr || "").slice(0, 300));
|
|
414
|
-
check("the core lands in the override", existsSync(join(cfg, "pipeline", "VERSION")));
|
|
415
|
-
check("the commands land in the override", existsSync(join(cfg, "commands", "cohorte-build.md")));
|
|
416
|
-
check("the hook is registered in the override", existsSync(join(cfg, "settings.json")));
|
|
417
|
-
check("nothing is written to the home default",
|
|
418
|
-
!existsSync(join(home, ".claude", "pipeline", "VERSION")));
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Claude Code must not regress: it is the runtime everyone is already on.
|
|
422
|
-
group("no regression — the Claude install keeps its shape");
|
|
423
|
-
check("commands still in .claude/commands", existsSync(join(proj, ".claude", "commands", "cohorte-build.md")));
|
|
424
|
-
check("agents still in .claude/agents", existsSync(join(proj, ".claude", "agents", "review.md")));
|
|
425
|
-
check("the model pin survives the render",
|
|
426
|
-
/^model: sonnet$/m.test(readFileSync(join(proj, ".claude", "commands", "cohorte-build.md"), "utf8")));
|
|
427
|
-
check("the subagent name survives the render",
|
|
428
|
-
/^name: review$/m.test(readFileSync(join(proj, ".claude", "agents", "review.md"), "utf8")));
|
|
429
|
-
// 2.2.0 retired /cohorte-loop. Copy-over never deletes, so the scrub is the only thing standing
|
|
430
|
-
// between an upgrade and a decoy command the model can still fire — assert it on the layout that
|
|
431
|
-
// actually had one installed.
|
|
432
|
-
check("the retired /cohorte-loop is not installed",
|
|
433
|
-
!existsSync(join(proj, ".claude", "commands", "cohorte-loop.md")));
|
|
434
|
-
|
|
435
|
-
for (const d of tmps) rmSync(d, { recursive: true, force: true });
|
|
436
|
-
console.log(failures ? `\ntest-adapter: ${failures} FAILED` : "\ntest-adapter: OK");
|
|
437
|
-
process.exit(failures ? 1 : 0);
|