@cleocode/cleo 2026.5.106 → 2026.5.107
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/dist/cli/index.js +471 -2138
- package/dist/cli/index.js.map +3 -3
- package/package.json +11 -11
- package/templates/HANDOFF-REDIRECT-STUB.md +0 -37
- package/templates/hooks/commit-msg +0 -213
- package/templates/hooks/pre-push +0 -87
- package/templates/hooks/pre-push.t1595-extension.sh +0 -140
- package/templates/workflows/README.md +0 -157
- package/templates/workflows/release-fanout.yml.tmpl +0 -214
- package/templates/workflows/release-prepare.yml.tmpl +0 -296
- package/templates/workflows/release-publish.yml.tmpl +0 -388
- package/templates/workflows/release-rollback.yml.tmpl +0 -322
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/cleo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2026.5.107",
|
|
4
4
|
"description": "CLEO CLI — the assembled product consuming @cleocode/core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli/index.js",
|
|
@@ -30,18 +30,18 @@
|
|
|
30
30
|
"tree-sitter-rust": "0.23.1",
|
|
31
31
|
"tree-sitter-typescript": "^0.23.2",
|
|
32
32
|
"yaml": "^2.8.3",
|
|
33
|
-
"@cleocode/animations": "
|
|
34
|
-
"@cleocode/
|
|
35
|
-
"@cleocode/
|
|
36
|
-
"@cleocode/contracts": "
|
|
37
|
-
"@cleocode/
|
|
38
|
-
"@cleocode/
|
|
39
|
-
"@cleocode/runtime": "
|
|
40
|
-
"@cleocode/playbooks": "
|
|
41
|
-
"@cleocode/
|
|
33
|
+
"@cleocode/animations": "2026.5.107",
|
|
34
|
+
"@cleocode/caamp": "2026.5.107",
|
|
35
|
+
"@cleocode/cant": "2026.5.107",
|
|
36
|
+
"@cleocode/contracts": "2026.5.107",
|
|
37
|
+
"@cleocode/lafs": "2026.5.107",
|
|
38
|
+
"@cleocode/paths": "2026.5.107",
|
|
39
|
+
"@cleocode/runtime": "2026.5.107",
|
|
40
|
+
"@cleocode/playbooks": "2026.5.107",
|
|
41
|
+
"@cleocode/nexus": "2026.5.107"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@cleocode/core": "
|
|
44
|
+
"@cleocode/core": "2026.5.107"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"@cleocode/core": {
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# STALE — DO NOT READ THIS FILE FOR STATE
|
|
2
|
-
|
|
3
|
-
**This file is deprecated as canonical state per T1593 (shipped in v2026.4.157).**
|
|
4
|
-
|
|
5
|
-
The current state of the project lives in **TASKS + BRAIN** — never in markdown.
|
|
6
|
-
|
|
7
|
-
## What you must do instead
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
cleo briefing
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
That command returns:
|
|
14
|
-
- The structured `lastSession.handoff.note` — guaranteed current, written at session-end
|
|
15
|
-
- `nextTasks` ranked by score — the system's recommendation for what to work on
|
|
16
|
-
- `blockedTasks` showing dependency chains — what cannot start yet
|
|
17
|
-
- `memoryContext` with relevant BRAIN observations
|
|
18
|
-
- `activeEpics` with completion percentages
|
|
19
|
-
|
|
20
|
-
## If you are seeing this and you ALREADY started reading instead of running `cleo briefing`
|
|
21
|
-
|
|
22
|
-
Stop. Run `cleo briefing`. Then `cleo memory find "IRONCLAD-ROADMAP"` if needed.
|
|
23
|
-
The system has explicit instructions for the next orchestrator that do NOT live in this file.
|
|
24
|
-
|
|
25
|
-
## Verification
|
|
26
|
-
|
|
27
|
-
Run any of these to verify state from the canonical source:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
cleo briefing # next-session handoff (canonical)
|
|
31
|
-
cleo dash # task counts
|
|
32
|
-
cleo memory find "roadmap" # roadmap observations in BRAIN
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
*This file deliberately contains no state. Reading it cannot mislead you. Run `cleo briefing`.*
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# CLEO_MANAGED_HOOK v1
|
|
3
|
-
# T1588 — project-agnostic T-ID enforcement for every commit subject.
|
|
4
|
-
# T1608 — diff-scope validation: warn when staged files drift from task scope.
|
|
5
|
-
#
|
|
6
|
-
# Rule: subject MUST contain `T<digits>` somewhere, OR be a merge/revert
|
|
7
|
-
# (which preserves the git merge --no-ff path established in T1587 and the
|
|
8
|
-
# stock `git revert` flow).
|
|
9
|
-
#
|
|
10
|
-
# Diff-scope check (T1608): if `cleo` is on the PATH and the referenced task
|
|
11
|
-
# has a non-empty files[] array, compare the staged diff against that scope.
|
|
12
|
-
# If >50 % of staged files fall outside the task's declared scope, emit a
|
|
13
|
-
# WARNING on stderr — but still exit 0 (hard-block is intentionally omitted
|
|
14
|
-
# to avoid rejecting valid refactors; the warning feeds audit tooling).
|
|
15
|
-
#
|
|
16
|
-
# Override: `git commit --no-verify` bypasses (standard git behaviour).
|
|
17
|
-
# A best-effort audit of `--no-verify` lives in the git shim (see T1591) —
|
|
18
|
-
# hooks themselves cannot observe `--no-verify`.
|
|
19
|
-
#
|
|
20
|
-
# This script is POSIX `/bin/sh` only (no bash/zsh-isms). It MUST work in
|
|
21
|
-
# any environment cleo init runs in: node-less projects, Rust, Python,
|
|
22
|
-
# bare repos, etc. Do not introduce node/pnpm dependencies here.
|
|
23
|
-
# The diff-scope check degrades gracefully when cleo or python3 is absent.
|
|
24
|
-
set -e
|
|
25
|
-
|
|
26
|
-
MSG_FILE="$1"
|
|
27
|
-
if [ -z "$MSG_FILE" ] || [ ! -f "$MSG_FILE" ]; then
|
|
28
|
-
echo "cleo commit-msg hook: missing message file argument" >&2
|
|
29
|
-
exit 1
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
# First non-empty, non-comment line = the subject.
|
|
33
|
-
SUBJECT=""
|
|
34
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
35
|
-
case "$line" in
|
|
36
|
-
'#'*) continue ;;
|
|
37
|
-
esac
|
|
38
|
-
trimmed=$(printf '%s' "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
39
|
-
if [ -n "$trimmed" ]; then
|
|
40
|
-
SUBJECT="$trimmed"
|
|
41
|
-
break
|
|
42
|
-
fi
|
|
43
|
-
done < "$MSG_FILE"
|
|
44
|
-
|
|
45
|
-
if [ -z "$SUBJECT" ]; then
|
|
46
|
-
echo "cleo commit-msg hook: empty commit subject — refusing." >&2
|
|
47
|
-
exit 1
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Bypass merge / revert / fixup / squash / amend-only metadata commits.
|
|
51
|
-
case "$SUBJECT" in
|
|
52
|
-
'Merge '*) exit 0 ;;
|
|
53
|
-
'Revert '*) exit 0 ;;
|
|
54
|
-
'fixup! '*) exit 0 ;;
|
|
55
|
-
'squash! '*) exit 0 ;;
|
|
56
|
-
'amend! '*) exit 0 ;;
|
|
57
|
-
esac
|
|
58
|
-
|
|
59
|
-
# Match `T` followed by 1+ digits anywhere in the subject.
|
|
60
|
-
# POSIX BRE — `[0-9][0-9]*` is `\d+` equivalent.
|
|
61
|
-
if ! printf '%s' "$SUBJECT" | grep -Eq 'T[0-9]+'; then
|
|
62
|
-
cat >&2 <<EOF
|
|
63
|
-
cleo commit-msg hook: commit subject is missing a task ID.
|
|
64
|
-
|
|
65
|
-
subject: $SUBJECT
|
|
66
|
-
|
|
67
|
-
Every commit MUST reference at least one CLEO task. Examples:
|
|
68
|
-
|
|
69
|
-
feat(T1588): ship POSIX commit-msg hook
|
|
70
|
-
T1588 — wire hooks-install into cleo init
|
|
71
|
-
fix: T1588 typo
|
|
72
|
-
|
|
73
|
-
Override (audited via git shim — see T1591):
|
|
74
|
-
|
|
75
|
-
git commit --no-verify
|
|
76
|
-
|
|
77
|
-
EOF
|
|
78
|
-
exit 1
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
# -----------------------------------------------------------------------
|
|
82
|
-
# T1608: diff-scope validation (warning-only — exits 0 on all paths below)
|
|
83
|
-
# -----------------------------------------------------------------------
|
|
84
|
-
# Extract the first T-ID from the subject (e.g. "feat(T1608): ..." → T1608).
|
|
85
|
-
TASK_ID=$(printf '%s' "$SUBJECT" | grep -Eo 'T[0-9]+' | head -n 1)
|
|
86
|
-
|
|
87
|
-
# Resolve cleo binary: prefer explicit CLEO_BIN env, then PATH lookup.
|
|
88
|
-
CLEO_BIN="${CLEO_BIN:-}"
|
|
89
|
-
if [ -z "$CLEO_BIN" ]; then
|
|
90
|
-
CLEO_BIN=$(command -v cleo 2>/dev/null || true)
|
|
91
|
-
fi
|
|
92
|
-
|
|
93
|
-
if [ -z "$CLEO_BIN" ] || [ ! -x "$CLEO_BIN" ] || [ -z "$TASK_ID" ]; then
|
|
94
|
-
# cleo not available or no T-ID extracted — skip diff-scope check.
|
|
95
|
-
exit 0
|
|
96
|
-
fi
|
|
97
|
-
|
|
98
|
-
# Resolve python3 (used for JSON parsing — lightweight, no npm required).
|
|
99
|
-
PYTHON3_BIN=$(command -v python3 2>/dev/null || true)
|
|
100
|
-
if [ -z "$PYTHON3_BIN" ]; then
|
|
101
|
-
# python3 unavailable — skip diff-scope check gracefully.
|
|
102
|
-
exit 0
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
# Fetch task.files[] via cleo show. Suppress errors (task may not exist
|
|
106
|
-
# in cleo DB for the target project; non-zero cleo exit → skip silently).
|
|
107
|
-
TASK_JSON=$("$CLEO_BIN" show "$TASK_ID" 2>/dev/null) || true
|
|
108
|
-
if [ -z "$TASK_JSON" ]; then
|
|
109
|
-
exit 0
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
# Extract the files[] array as one path per line via python3 -c.
|
|
113
|
-
# Using -c avoids the stdin-conflict that arises with heredoc + pipe.
|
|
114
|
-
TASK_FILES=$("$PYTHON3_BIN" -c "
|
|
115
|
-
import json, sys
|
|
116
|
-
try:
|
|
117
|
-
d = json.loads(sys.argv[1])
|
|
118
|
-
files = d.get('data', {}).get('task', {}).get('files', [])
|
|
119
|
-
print('\n'.join(f for f in files if f))
|
|
120
|
-
except Exception:
|
|
121
|
-
pass
|
|
122
|
-
" "$TASK_JSON" 2>/dev/null) || true
|
|
123
|
-
|
|
124
|
-
if [ -z "$TASK_FILES" ]; then
|
|
125
|
-
# Task has no files[] scope declared — nothing to validate against.
|
|
126
|
-
exit 0
|
|
127
|
-
fi
|
|
128
|
-
|
|
129
|
-
# Get staged file list (diff-scope is only meaningful for staged changes).
|
|
130
|
-
# git diff --cached exits 0 even when empty, so this is always safe.
|
|
131
|
-
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null) || true
|
|
132
|
-
|
|
133
|
-
if [ -z "$STAGED_FILES" ]; then
|
|
134
|
-
# No staged files (e.g. amend of message only) — skip check.
|
|
135
|
-
exit 0
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
# Count staged files and how many are in-scope (path prefix or exact match).
|
|
139
|
-
# A staged file is "in-scope" when it matches any task file path OR any
|
|
140
|
-
# task file path is a path-prefix of the staged file (directory scope).
|
|
141
|
-
# We delegate the maths to python3 to avoid shell integer-division quirks.
|
|
142
|
-
# Both TASK_FILES and STAGED_FILES are passed as argv to avoid stdin conflict.
|
|
143
|
-
DRIFT_RESULT=$("$PYTHON3_BIN" -c "
|
|
144
|
-
import sys
|
|
145
|
-
|
|
146
|
-
# argv[1] = newline-separated task files; argv[2] = newline-separated staged files
|
|
147
|
-
task_files = [f.strip() for f in sys.argv[1].splitlines() if f.strip()]
|
|
148
|
-
staged = [f.strip() for f in sys.argv[2].splitlines() if f.strip()]
|
|
149
|
-
|
|
150
|
-
def in_scope(staged_path, task_files):
|
|
151
|
-
for tf in task_files:
|
|
152
|
-
# Exact match.
|
|
153
|
-
if staged_path == tf:
|
|
154
|
-
return True
|
|
155
|
-
# task file is a directory prefix of the staged file.
|
|
156
|
-
if staged_path.startswith(tf.rstrip('/') + '/'):
|
|
157
|
-
return True
|
|
158
|
-
# staged file is a directory prefix of a task file (task file
|
|
159
|
-
# is deeper; e.g. task scope is src/foo.ts, staged is src/).
|
|
160
|
-
if tf.startswith(staged_path.rstrip('/') + '/'):
|
|
161
|
-
return True
|
|
162
|
-
return False
|
|
163
|
-
|
|
164
|
-
if not staged or not task_files:
|
|
165
|
-
print('SKIP')
|
|
166
|
-
sys.exit(0)
|
|
167
|
-
|
|
168
|
-
out_of_scope = [f for f in staged if not in_scope(f, task_files)]
|
|
169
|
-
total = len(staged)
|
|
170
|
-
drift_count = len(out_of_scope)
|
|
171
|
-
drift_pct = (drift_count * 100) // total if total > 0 else 0
|
|
172
|
-
|
|
173
|
-
print('TOTAL=' + str(total))
|
|
174
|
-
print('DRIFT_COUNT=' + str(drift_count))
|
|
175
|
-
print('DRIFT_PCT=' + str(drift_pct))
|
|
176
|
-
for f in out_of_scope:
|
|
177
|
-
print('OUT=' + f)
|
|
178
|
-
" "$TASK_FILES" "$STAGED_FILES" 2>/dev/null) || true
|
|
179
|
-
|
|
180
|
-
# Parse drift result.
|
|
181
|
-
if [ -z "$DRIFT_RESULT" ] || printf '%s' "$DRIFT_RESULT" | grep -q '^SKIP$'; then
|
|
182
|
-
exit 0
|
|
183
|
-
fi
|
|
184
|
-
|
|
185
|
-
DRIFT_PCT=$(printf '%s' "$DRIFT_RESULT" | grep '^DRIFT_PCT=' | sed 's/^DRIFT_PCT=//')
|
|
186
|
-
TOTAL=$(printf '%s' "$DRIFT_RESULT" | grep '^TOTAL=' | sed 's/^TOTAL=//')
|
|
187
|
-
DRIFT_COUNT=$(printf '%s' "$DRIFT_RESULT" | grep '^DRIFT_COUNT=' | sed 's/^DRIFT_COUNT=//')
|
|
188
|
-
OUT_FILES=$(printf '%s' "$DRIFT_RESULT" | grep '^OUT=' | sed 's/^OUT=//')
|
|
189
|
-
|
|
190
|
-
# Threshold: warn when drift exceeds 50 % (i.e. drift_pct > 50).
|
|
191
|
-
THRESHOLD=50
|
|
192
|
-
if [ -n "$DRIFT_PCT" ] && [ "$DRIFT_PCT" -gt "$THRESHOLD" ] 2>/dev/null; then
|
|
193
|
-
cat >&2 <<EOF
|
|
194
|
-
cleo commit-msg hook [T1608]: diff-scope drift WARNING
|
|
195
|
-
|
|
196
|
-
Task : $TASK_ID
|
|
197
|
-
Staged : $TOTAL file(s) — $DRIFT_COUNT ($DRIFT_PCT%) are outside $TASK_ID scope
|
|
198
|
-
|
|
199
|
-
Out-of-scope staged files:
|
|
200
|
-
$(printf '%s' "$OUT_FILES" | sed 's/^/ /')
|
|
201
|
-
|
|
202
|
-
Task scope (files[]):
|
|
203
|
-
$(printf '%s' "$TASK_FILES" | sed 's/^/ /')
|
|
204
|
-
|
|
205
|
-
This is a WARNING, not a hard block. The commit will proceed.
|
|
206
|
-
To silence: ensure staged files align with the task scope,
|
|
207
|
-
or add the files to the task via \`cleo update $TASK_ID --files ...\`.
|
|
208
|
-
|
|
209
|
-
EOF
|
|
210
|
-
# Exit 0 — warning only.
|
|
211
|
-
fi
|
|
212
|
-
|
|
213
|
-
exit 0
|
package/templates/hooks/pre-push
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# CLEO_MANAGED_HOOK v1
|
|
3
|
-
# T1588 — project-agnostic T-ID enforcement for every commit being pushed.
|
|
4
|
-
#
|
|
5
|
-
# git invokes this with `<remote> <url>` as args and feeds ref updates on
|
|
6
|
-
# stdin: `<local-ref> <local-sha> <remote-ref> <remote-sha>` (one per line).
|
|
7
|
-
#
|
|
8
|
-
# For each new commit being pushed (commits in `local-sha` that are NOT
|
|
9
|
-
# already in any remote ref), require a T-ID in the subject. Same allow-
|
|
10
|
-
# list as commit-msg: merge / revert / fixup / squash / amend.
|
|
11
|
-
#
|
|
12
|
-
# Override: `git push --no-verify` (standard git override).
|
|
13
|
-
#
|
|
14
|
-
# T1595:reconcile-extension-point
|
|
15
|
-
# Pre-push reconcile gate hooks here (see T1595 worker).
|
|
16
|
-
# Reserved range below — DO NOT remove these markers; T1595 extends here.
|
|
17
|
-
# T1595:reconcile-extension-point-end
|
|
18
|
-
#
|
|
19
|
-
# POSIX `/bin/sh` only. No node/pnpm dependency.
|
|
20
|
-
set -e
|
|
21
|
-
|
|
22
|
-
ZERO_SHA="0000000000000000000000000000000000000000"
|
|
23
|
-
EMPTY_TREE_SHA=""
|
|
24
|
-
FAIL=0
|
|
25
|
-
FAIL_LOG=""
|
|
26
|
-
|
|
27
|
-
check_subject() {
|
|
28
|
-
subject="$1"
|
|
29
|
-
case "$subject" in
|
|
30
|
-
'Merge '*) return 0 ;;
|
|
31
|
-
'Revert '*) return 0 ;;
|
|
32
|
-
'fixup! '*) return 0 ;;
|
|
33
|
-
'squash! '*) return 0 ;;
|
|
34
|
-
'amend! '*) return 0 ;;
|
|
35
|
-
esac
|
|
36
|
-
if printf '%s' "$subject" | grep -Eq 'T[0-9]+'; then
|
|
37
|
-
return 0
|
|
38
|
-
fi
|
|
39
|
-
return 1
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
# Read each ref-update line from stdin.
|
|
43
|
-
while read -r local_ref local_sha remote_ref remote_sha; do
|
|
44
|
-
# Branch deletion (push :branch) — nothing to validate.
|
|
45
|
-
if [ "$local_sha" = "$ZERO_SHA" ]; then
|
|
46
|
-
continue
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
if [ "$remote_sha" = "$ZERO_SHA" ]; then
|
|
50
|
-
# New branch: validate every commit reachable from local_sha that is
|
|
51
|
-
# NOT reachable from any other remote ref.
|
|
52
|
-
range="$local_sha --not --remotes"
|
|
53
|
-
else
|
|
54
|
-
range="$remote_sha..$local_sha"
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
# Subject extraction: %s = subject. NUL-delimit for safety.
|
|
58
|
-
# shellcheck disable=SC2086
|
|
59
|
-
commits=$(git rev-list $range 2>/dev/null || true)
|
|
60
|
-
for sha in $commits; do
|
|
61
|
-
subject=$(git log -1 --pretty=%s "$sha" 2>/dev/null || true)
|
|
62
|
-
if [ -z "$subject" ]; then
|
|
63
|
-
continue
|
|
64
|
-
fi
|
|
65
|
-
if ! check_subject "$subject"; then
|
|
66
|
-
FAIL=1
|
|
67
|
-
FAIL_LOG="${FAIL_LOG} ${sha} ${subject}
|
|
68
|
-
"
|
|
69
|
-
fi
|
|
70
|
-
done
|
|
71
|
-
done
|
|
72
|
-
|
|
73
|
-
if [ "$FAIL" -ne 0 ]; then
|
|
74
|
-
cat >&2 <<EOF
|
|
75
|
-
cleo pre-push hook: refusing push — commits are missing task IDs.
|
|
76
|
-
|
|
77
|
-
${FAIL_LOG}
|
|
78
|
-
Every commit MUST reference at least one CLEO task (e.g. \`T1588\`).
|
|
79
|
-
Fix the subjects (\`git rebase -i\` + reword), or override with:
|
|
80
|
-
|
|
81
|
-
git push --no-verify
|
|
82
|
-
|
|
83
|
-
EOF
|
|
84
|
-
exit 1
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
exit 0
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# T1595 — pre-push reconcile gate (extension)
|
|
3
|
-
#
|
|
4
|
-
# This file is the reconcile-gate extension for the project's pre-push
|
|
5
|
-
# hook. T1588 will produce a unified pre-push hook that contains a
|
|
6
|
-
# sentinel block:
|
|
7
|
-
#
|
|
8
|
-
# # T1595:reconcile-extension-point
|
|
9
|
-
# # Pre-push reconcile gate hooks here (see T1595 worker)
|
|
10
|
-
#
|
|
11
|
-
# When T1588 lands, the contents of `reconcile_gate()` below MUST be
|
|
12
|
-
# inlined at that sentinel. Until then, this file is sourced as-is by
|
|
13
|
-
# any pre-push hook that wants the reconcile gate (see installer in
|
|
14
|
-
# `packages/core/src/hooks/install-pre-push.ts`, future).
|
|
15
|
-
#
|
|
16
|
-
# CONTRACT
|
|
17
|
-
# - POSIX shell (`/bin/sh`); no bashisms.
|
|
18
|
-
# - Reads pending tag from `git tag --sort=-v:refname | head -1`
|
|
19
|
-
# (tag-shape agnostic — works for CalVer or SemVer).
|
|
20
|
-
# - Calls `cleo reconcile release --tag <pending> --dry-run --json`
|
|
21
|
-
# and parses the aggregate `reconciled` count.
|
|
22
|
-
# - Drift > 0 → exit 1 with task-ID list.
|
|
23
|
-
# - Drift == 0 → return 0.
|
|
24
|
-
# - Override: env `CLEO_ALLOW_DRIFT_PUSH=1` bypasses the gate AND
|
|
25
|
-
# appends an audit entry to
|
|
26
|
-
# `${XDG_DATA_HOME:-$HOME/.local/share}/cleo/audit/drift-push-bypass.jsonl`.
|
|
27
|
-
# - Project-agnostic: no hardcoded branch name; default branch is
|
|
28
|
-
# resolved via `git symbolic-ref refs/remotes/origin/HEAD` when
|
|
29
|
-
# needed.
|
|
30
|
-
#
|
|
31
|
-
# EXIT CODES
|
|
32
|
-
# 0 — no drift, push allowed
|
|
33
|
-
# 1 — drift detected, push refused (or cleo CLI unavailable in
|
|
34
|
-
# strict mode; see CLEO_RECONCILE_STRICT below)
|
|
35
|
-
#
|
|
36
|
-
# CONFIGURATION (env)
|
|
37
|
-
# CLEO_ALLOW_DRIFT_PUSH=1 bypass the gate (audited)
|
|
38
|
-
# CLEO_RECONCILE_STRICT=1 if `cleo` CLI is missing, refuse push
|
|
39
|
-
# (default: warn-and-allow so first-time
|
|
40
|
-
# clones without cleo installed still work)
|
|
41
|
-
# CLEO_RECONCILE_BIN=<path> override the cleo binary path (testing)
|
|
42
|
-
|
|
43
|
-
set -eu
|
|
44
|
-
|
|
45
|
-
reconcile_gate() {
|
|
46
|
-
# Locate cleo CLI ------------------------------------------------------
|
|
47
|
-
cleo_bin="${CLEO_RECONCILE_BIN:-cleo}"
|
|
48
|
-
if ! command -v "$cleo_bin" >/dev/null 2>&1; then
|
|
49
|
-
if [ "${CLEO_RECONCILE_STRICT:-0}" = "1" ]; then
|
|
50
|
-
echo "ERROR: cleo CLI not found on PATH (strict mode)" >&2
|
|
51
|
-
echo " install cleo or unset CLEO_RECONCILE_STRICT" >&2
|
|
52
|
-
return 1
|
|
53
|
-
fi
|
|
54
|
-
# Soft-fail: warn but allow push. Avoids blocking fresh clones.
|
|
55
|
-
echo "warn: cleo CLI not found; skipping reconcile gate" >&2
|
|
56
|
-
return 0
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
# Resolve the pending release tag -------------------------------------
|
|
60
|
-
# We use the most recent tag as the "pending" anchor. Reconcile is
|
|
61
|
-
# project-agnostic — it walks tasks released_in this tag's range
|
|
62
|
-
# regardless of CalVer vs SemVer shape.
|
|
63
|
-
pending_tag="$(git tag --sort=-v:refname 2>/dev/null | head -n 1 || true)"
|
|
64
|
-
if [ -z "$pending_tag" ]; then
|
|
65
|
-
# No tags yet → no release to reconcile.
|
|
66
|
-
return 0
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
# Override path -------------------------------------------------------
|
|
70
|
-
if [ "${CLEO_ALLOW_DRIFT_PUSH:-0}" = "1" ]; then
|
|
71
|
-
audit_dir="${XDG_DATA_HOME:-$HOME/.local/share}/cleo/audit"
|
|
72
|
-
mkdir -p "$audit_dir"
|
|
73
|
-
audit_log="$audit_dir/drift-push-bypass.jsonl"
|
|
74
|
-
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
75
|
-
user="${USER:-${LOGNAME:-unknown}}"
|
|
76
|
-
repo="$(git rev-parse --show-toplevel 2>/dev/null || echo unknown)"
|
|
77
|
-
head_sha="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
78
|
-
# Write a JSONL line. Stay project-agnostic: no jq dependency.
|
|
79
|
-
printf '{"ts":"%s","user":"%s","repo":"%s","head":"%s","tag":"%s","reason":"CLEO_ALLOW_DRIFT_PUSH=1"}\n' \
|
|
80
|
-
"$ts" "$user" "$repo" "$head_sha" "$pending_tag" >> "$audit_log"
|
|
81
|
-
echo "warn: pre-push reconcile gate bypassed (CLEO_ALLOW_DRIFT_PUSH=1)" >&2
|
|
82
|
-
echo " audit: $audit_log" >&2
|
|
83
|
-
return 0
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
# Run reconcile in dry-run JSON mode ----------------------------------
|
|
87
|
-
# We swallow non-zero exit codes from the CLI here because reconcile
|
|
88
|
-
# exits 2 when drift exists; we want to read the JSON regardless.
|
|
89
|
-
json_out="$("$cleo_bin" reconcile release --tag "$pending_tag" --dry-run --json 2>/dev/null || true)"
|
|
90
|
-
if [ -z "$json_out" ]; then
|
|
91
|
-
if [ "${CLEO_RECONCILE_STRICT:-0}" = "1" ]; then
|
|
92
|
-
echo "ERROR: cleo reconcile release returned empty output (strict mode)" >&2
|
|
93
|
-
return 1
|
|
94
|
-
fi
|
|
95
|
-
echo "warn: cleo reconcile release returned empty output; skipping gate" >&2
|
|
96
|
-
return 0
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
# Parse the aggregate `reconciled` count without jq.
|
|
100
|
-
# The InvariantReport JSON has a top-level `"reconciled": N` integer.
|
|
101
|
-
# We grep the first such key (top-level always emitted before per-result
|
|
102
|
-
# entries). Per-result entries are nested under `results[*].details`,
|
|
103
|
-
# so the first match is the aggregate.
|
|
104
|
-
drift_count="$(printf '%s' "$json_out" \
|
|
105
|
-
| grep -o '"reconciled"[[:space:]]*:[[:space:]]*[0-9]\+' \
|
|
106
|
-
| head -n 1 \
|
|
107
|
-
| grep -o '[0-9]\+' \
|
|
108
|
-
|| true)"
|
|
109
|
-
drift_count="${drift_count:-0}"
|
|
110
|
-
|
|
111
|
-
if [ "$drift_count" -gt 0 ] 2>/dev/null; then
|
|
112
|
-
echo "ERROR: pre-push reconcile gate detected drift" >&2
|
|
113
|
-
echo " tag: $pending_tag" >&2
|
|
114
|
-
echo " drift count: $drift_count shipped-but-pending task(s)" >&2
|
|
115
|
-
# Best-effort: extract reconciled task IDs from results[*].details.reconciled
|
|
116
|
-
# arrays. Strings look like "T1411" (project-agnostic prefix).
|
|
117
|
-
drifted_ids="$(printf '%s' "$json_out" \
|
|
118
|
-
| tr -d '\n' \
|
|
119
|
-
| grep -o '"reconciled"[[:space:]]*:[[:space:]]*\[[^]]*\]' \
|
|
120
|
-
| grep -o '"[A-Za-z][A-Za-z0-9_-]*"' \
|
|
121
|
-
| sort -u \
|
|
122
|
-
| tr '\n' ' ' \
|
|
123
|
-
|| true)"
|
|
124
|
-
if [ -n "$drifted_ids" ]; then
|
|
125
|
-
echo " drifted tasks: $drifted_ids" >&2
|
|
126
|
-
fi
|
|
127
|
-
echo "" >&2
|
|
128
|
-
echo "Refuse push: run 'cleo reconcile release --tag $pending_tag' to" >&2
|
|
129
|
-
echo "reconcile, or set CLEO_ALLOW_DRIFT_PUSH=1 (audited bypass)." >&2
|
|
130
|
-
return 1
|
|
131
|
-
fi
|
|
132
|
-
|
|
133
|
-
return 0
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
# When sourced from the unified pre-push hook the function is invoked
|
|
137
|
-
# at the sentinel point. When run standalone (testing), invoke directly.
|
|
138
|
-
if [ "${T1595_SOURCED:-0}" != "1" ]; then
|
|
139
|
-
reconcile_gate
|
|
140
|
-
fi
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# CLEO GitHub Actions workflow templates
|
|
2
|
-
|
|
3
|
-
This directory contains CLEO's templated GitHub Actions workflows for the
|
|
4
|
-
release pipeline defined in
|
|
5
|
-
`.cleo/rcasd/T9345/research/SPEC-T9345-release-pipeline-v2.md`. Templates
|
|
6
|
-
are project-agnostic: they ship with `{{PLACEHOLDER}}` markers that are
|
|
7
|
-
resolved at scaffold time by `cleo init --workflows` (T9531) against the
|
|
8
|
-
local `.cleo/project-context.json` and the ADR-061 tool resolver.
|
|
9
|
-
|
|
10
|
-
## Template files
|
|
11
|
-
|
|
12
|
-
| Template | SPEC section | Purpose |
|
|
13
|
-
|-----------------------------------|--------------|--------------------------------------------------------|
|
|
14
|
-
| `release-prepare.yml.tmpl` | §5.1 | Cut release branch, bump version, open bump-PR. |
|
|
15
|
-
| `release-publish.yml.tmpl` | §5.2 | Publish + tag once the bump-PR is merged. |
|
|
16
|
-
| `release-fanout.yml.tmpl` | §5.3 | Best-effort post-publish fanout (docs, docker, etc.). |
|
|
17
|
-
| `release-rollback.yml.tmpl` | §5.4 | Rollback workflow (revert PR + npm deprecate + reconcile). |
|
|
18
|
-
|
|
19
|
-
## Template contract
|
|
20
|
-
|
|
21
|
-
- Templates MUST use `{{PLACEHOLDER}}` markers compatible with simple regex
|
|
22
|
-
substitution (`s/{{NAME}}/value/g`). They MUST NOT use Mustache, Handlebars,
|
|
23
|
-
or any nested-syntax templating engine — the scaffold step relies on
|
|
24
|
-
deterministic, single-pass regex replacement so a stale template can be
|
|
25
|
-
diffed cleanly against a re-render.
|
|
26
|
-
- Placeholder names MUST be `UPPER_SNAKE_CASE` wrapped in double braces.
|
|
27
|
-
- Templates MUST be ASCII-only outside of strings.
|
|
28
|
-
- Templates MUST pass `actionlint` after substitution. The
|
|
29
|
-
`release-prepare-render.test.ts` snapshot test renders with sample values
|
|
30
|
-
and (if `actionlint` is on `PATH`) pipes the output through `actionlint -`
|
|
31
|
-
via `child_process.execSync`.
|
|
32
|
-
|
|
33
|
-
## Placeholders
|
|
34
|
-
|
|
35
|
-
All four templates draw from the same placeholder vocabulary. Unused
|
|
36
|
-
placeholders for a given template are silently ignored by the scaffolder.
|
|
37
|
-
|
|
38
|
-
| Placeholder | Source | Default | Example |
|
|
39
|
-
|-----------------------|--------------------------------------------------------|--------------------------------------|--------------------------------------|
|
|
40
|
-
| `{{NODE_VERSION}}` | `.cleo/project-context.json` `node.version` | `22.x` | `"22.x"` |
|
|
41
|
-
| `{{INSTALL_CMD}}` | ADR-061 archetype defaults (`primaryType`-specific) | `pnpm install --frozen-lockfile` | `pnpm install --frozen-lockfile` |
|
|
42
|
-
| `{{LINT_CMD}}` | ADR-061 `tool:lint` resolution | `pnpm run lint` | `pnpm biome ci .` |
|
|
43
|
-
| `{{TYPECHECK_CMD}}` | ADR-061 `tool:typecheck` resolution | `pnpm run typecheck` | `pnpm run typecheck` |
|
|
44
|
-
| `{{TEST_CMD}}` | ADR-061 `tool:test` resolution | `pnpm run test` | `pnpm run test` |
|
|
45
|
-
| `{{BUILD_CMD}}` | ADR-061 `tool:build` resolution | `pnpm run build` | `pnpm run build` |
|
|
46
|
-
| `{{BRANCH_PREFIX}}` | `release.branchPrefix` in `.cleo/config.json` | `release` | `release` |
|
|
47
|
-
| `{{PR_LABEL}}` | `release.prLabel` in `.cleo/config.json` | `release` | `release` |
|
|
48
|
-
| `{{NPM_PUBLISH_CMD}}` | `release.npmPublishCmd` in `.cleo/config.json` | `pnpm publish --access public --tag latest` | `pnpm publish -r --access public --tag latest` |
|
|
49
|
-
| `{{PUBLISHERS}}` | `release.publishers` in `.cleo/config.json` | `npm` | `npm cargo` |
|
|
50
|
-
| `{{DOCS_BUILD_CMD}}` | `release.fanout.docsBuildCmd` in `.cleo/config.json` | `pnpm run docs:build` | `pnpm --filter @cleocode/docs run build` |
|
|
51
|
-
| `{{ENABLE_DOCS_DEPLOY}}` | `release.fanout.docsDeploy` in `.cleo/config.json` | `false` | `true` |
|
|
52
|
-
| `{{ENABLE_DOCKER_RETAG}}` | `release.fanout.dockerRetag` in `.cleo/config.json` | `false` | `true` |
|
|
53
|
-
| `{{ENABLE_SENTINEL_NOTIFY}}` | `release.fanout.sentinelNotify` in `.cleo/config.json` | `false` | `true` |
|
|
54
|
-
| `{{ENABLE_STUDIO_DEPLOY}}` | `release.fanout.studioDeploy` in `.cleo/config.json` | `false` | `true` |
|
|
55
|
-
| `{{ENABLE_NIGHTLY_TRIGGER}}` | `release.fanout.nightlyTrigger` in `.cleo/config.json` | `false` | `true` |
|
|
56
|
-
| `{{DOCKER_IMAGE}}` | `release.fanout.dockerImage` in `.cleo/config.json` | *(none — required if `dockerRetag=true`)* | `cleocode/cleo` |
|
|
57
|
-
| `{{DOCKER_HUB_USER}}` | `release.fanout.dockerHubUser` in `.cleo/config.json` | *(none — required if `dockerRetag=true`)* | `cleocode` |
|
|
58
|
-
| `{{SENTINEL_WEBHOOK_URL}}` | `release.fanout.sentinelWebhookUrl` in `.cleo/config.json` | *(none — required if `sentinelNotify=true`)* | `https://sentinel.example.com/hooks/release` |
|
|
59
|
-
| `{{STUDIO_DEPLOY_HOOK}}` | `release.fanout.studioDeployHook` in `.cleo/config.json` | *(none — required if `studioDeploy=true`)* | `https://studio.example.com/deploy` |
|
|
60
|
-
| `{{NPM_PACKAGES}}` | `release.rollback.npmPackages` in `.cleo/config.json` | *(none — required if `PUBLISHERS` contains `npm`)* | `@cleocode/cleo @cleocode/core` |
|
|
61
|
-
| `{{CARGO_CRATES}}` | `release.rollback.cargoCrates` in `.cleo/config.json` | *(none — required if `PUBLISHERS` contains `cargo`)* | `cleo-core cleo-cli` |
|
|
62
|
-
|
|
63
|
-
Source precedence (highest first):
|
|
64
|
-
|
|
65
|
-
1. Explicit override in `.cleo/project-context.json` (ADR-061 §1).
|
|
66
|
-
2. Project archetype default keyed on `primaryType` (e.g. `node` → `pnpm`,
|
|
67
|
-
`rust` → `cargo`, `python` → `uv`).
|
|
68
|
-
3. Hard-coded fallback in the scaffolder.
|
|
69
|
-
|
|
70
|
-
## GitHub permissions required per template
|
|
71
|
-
|
|
72
|
-
| Template | `contents` | `pull-requests` | `id-token` | `packages` | Other |
|
|
73
|
-
|------------------------------|-----------------|------------------|----------------------|--------------------|------------------|
|
|
74
|
-
| `release-prepare.yml.tmpl` | `write` | `write` | `write` (signed tags) | (MUST NOT request) | — |
|
|
75
|
-
| `release-publish.yml.tmpl` | `write` (tag) | `read` | `write` (OIDC) | `write` (publish job only) | — |
|
|
76
|
-
| `release-fanout.yml.tmpl` | `read` | — | `write` (Pages, docs job only)* | — | `pages: write`* |
|
|
77
|
-
| `release-rollback.yml.tmpl` | `write` (revert + tag delete) | `write` | — | `write` (npm deprecate) | — |
|
|
78
|
-
|
|
79
|
-
*Per-job — only granted to the job that needs it.
|
|
80
|
-
|
|
81
|
-
## Required secrets
|
|
82
|
-
|
|
83
|
-
| Template | Required secrets | Optional secrets |
|
|
84
|
-
|------------------------------|-------------------------|---------------------------------------------------|
|
|
85
|
-
| `release-prepare.yml.tmpl` | `GITHUB_TOKEN` (auto) | *(none)* — MUST NOT require `NPM_TOKEN` (R-210) |
|
|
86
|
-
| `release-publish.yml.tmpl` | `GITHUB_TOKEN`, `NPM_TOKEN`, `ANTHROPIC_API_KEY` | `CARGO_TOKEN`, `PYPI_TOKEN`, `DOCKER_HUB_TOKEN` |
|
|
87
|
-
| `release-fanout.yml.tmpl` | `GITHUB_TOKEN` (auto) | `DOCKER_HUB_TOKEN` (if `dockerRetag=true`), `SENTINEL_TOKEN` (if `sentinelNotify=true`), `STUDIO_DEPLOY_TOKEN` (if `studioDeploy=true`) |
|
|
88
|
-
| `release-rollback.yml.tmpl` | `GITHUB_TOKEN` (auto), `NPM_TOKEN` (if `PUBLISHERS` contains `npm`) | `CARGO_TOKEN` (if `PUBLISHERS` contains `cargo`) |
|
|
89
|
-
|
|
90
|
-
## Scaffolding workflow
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
# Render the templates against the local project, writing to .github/workflows/.
|
|
94
|
-
cleo init --workflows
|
|
95
|
-
|
|
96
|
-
# Re-render after editing project-context.json or release config.
|
|
97
|
-
cleo init --workflows --force
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
The scaffolder reads each `*.yml.tmpl` file in this directory, performs
|
|
101
|
-
regex substitution against the placeholder vocabulary above, validates the
|
|
102
|
-
result with `actionlint`, and writes the rendered YAML to
|
|
103
|
-
`<project>/.github/workflows/<basename>.yml`. Existing files are NOT
|
|
104
|
-
overwritten without `--force`.
|
|
105
|
-
|
|
106
|
-
## Extending without forking
|
|
107
|
-
|
|
108
|
-
Per SPEC R-260, downstream projects MAY layer customizations onto the
|
|
109
|
-
rendered workflows via a sibling `.github/workflows/<basename>.overrides.yml`
|
|
110
|
-
file (planned: `.workflow-overrides.yml` at repo root for cross-template
|
|
111
|
-
overrides). The scaffolder merges overrides as a final pass; conflicts at
|
|
112
|
-
the same YAML key path resolve to the override.
|
|
113
|
-
|
|
114
|
-
Override examples (illustrative — full schema lands with T9531):
|
|
115
|
-
|
|
116
|
-
```yaml
|
|
117
|
-
# .github/workflows/release-prepare.overrides.yml
|
|
118
|
-
jobs:
|
|
119
|
-
preflight:
|
|
120
|
-
steps:
|
|
121
|
-
- name: Project-specific cache warm
|
|
122
|
-
run: ./scripts/warm-cache.sh
|
|
123
|
-
timeout-minutes: 3
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Overrides MUST NOT remove or relax any RFC2119 invariant from
|
|
127
|
-
`SPEC-T9345-release-pipeline-v2.md`. The scaffolder rejects override files
|
|
128
|
-
that drop required `timeout-minutes`, modify `concurrency.group`, or strip
|
|
129
|
-
declared permissions.
|
|
130
|
-
|
|
131
|
-
## Cross-references
|
|
132
|
-
|
|
133
|
-
- SPEC: `.cleo/rcasd/T9345/research/SPEC-T9345-release-pipeline-v2.md`
|
|
134
|
-
- §5.1 → `release-prepare.yml.tmpl` *(T9532, landed)*
|
|
135
|
-
- §5.2 → `release-publish.yml.tmpl` *(T9533, landed)*
|
|
136
|
-
- §5.3 → `release-fanout.yml.tmpl` *(T9534, landed)*
|
|
137
|
-
- §5.4 → `release-rollback.yml.tmpl` *(T9535, current)*
|
|
138
|
-
- ADR-061 (tool resolver): governs `tool:*` placeholder resolution.
|
|
139
|
-
- ADR-073 (release pipeline v2): umbrella architectural decision.
|
|
140
|
-
- T9531: `cleo init --workflows` scaffold command (consumes these templates).
|
|
141
|
-
- T9532: `release-prepare.yml.tmpl` + README skeleton + snapshot test.
|
|
142
|
-
- T9533: `release-publish.yml.tmpl` + README placeholders + snapshot test
|
|
143
|
-
(eliminates F6 tag-on-pre-merge-SHA race by construction).
|
|
144
|
-
- T9534: `release-fanout.yml.tmpl` + 11 fanout placeholders + snapshot test
|
|
145
|
-
(five independent best-effort jobs gated on env toggles, every job
|
|
146
|
-
carries `continue-on-error: true` so fanout failures cannot mark the
|
|
147
|
-
release as failed; fanout jobs MUST NOT be required status checks per
|
|
148
|
-
R-244).
|
|
149
|
-
- T9535: `release-rollback.yml.tmpl` + rollback placeholders (`NPM_PACKAGES`,
|
|
150
|
-
`CARGO_CRATES`) + snapshot test (current task — `workflow_dispatch`-only
|
|
151
|
-
rollback with 4 jobs `validate` → `revert` → `deprecate` →
|
|
152
|
-
`reconcile-rollback`. The `revert` job opens a revert PR against `main`
|
|
153
|
-
via `gh pr create --label rollback` — NEVER pushes directly to `main`
|
|
154
|
-
per ADR-065. The `deprecate` job is best-effort
|
|
155
|
-
(`continue-on-error: true`); reconcile runs whenever validate succeeded
|
|
156
|
-
so the provenance graph records the operator's intent even if a
|
|
157
|
-
downstream side-effect failed).
|