@cleocode/cleo 2026.5.105 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/cleo",
3
- "version": "2026.5.105",
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/caamp": "2026.5.105",
34
- "@cleocode/animations": "2026.5.105",
35
- "@cleocode/cant": "2026.5.105",
36
- "@cleocode/lafs": "2026.5.105",
37
- "@cleocode/contracts": "2026.5.105",
38
- "@cleocode/nexus": "2026.5.105",
39
- "@cleocode/paths": "2026.5.105",
40
- "@cleocode/playbooks": "2026.5.105",
41
- "@cleocode/runtime": "2026.5.105"
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": "2026.5.105"
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
@@ -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).