nacl-tl-release
SkillDocs & knowledgeFull release pipeline: merge verified PRs to main, wait for production CI, health check, version bump, git tag, changelog, GitHub release, YouGile notification. Use when: create release, bump version, merge and release, generate release notes, tag version, or the user says "/nacl-tl-release".
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the nacl-tl-release skill
What this skill tells your AI
The instructions your AI receives, as published by itsalt/nacl in nacl-tl-release/SKILL.md and read by ahel’s review.
Contract
Inputs this skill consumes:
- Per-PR underlying UC statuses (from graph or .tl/status.json)
- GitHub CI status per PR
Outputs this skill produces:
- Headline one of: RELEASE COMPLETE / RELEASE HALTED — {SUFFIX} / RELEASE INCOMPLETE — REGRESSION
- Release tag (created only on aggregated PASS)
- Per-UC table in release notes
delivered_in_releasegraph stamp gated on PASS
Downstream consumers of this output:
- GitHub release
- Deploy pipeline (downstream of merge to main)
Contract change discipline:
If this skill's output contract changes, every downstream consumer listed above
must be audited and updated in the same release. The 0.10.0→0.10.1 regression
was caused by the absence of this discipline. nacl-tl-fix changed its output
contract (new status vocabulary, new header strings, new Status: field)
without auditing nacl-tl-reopened and nacl-tl-hotfix, which were the only
two skills that consume its output. Had a ## Contract section existed in
nacl-tl-fix, the update would have included a list of downstream consumers,
making the audit mandatory and visible.
TeamLead Release — Merge + Deploy + Version + Notify
Your Role
You execute the full release pipeline: merge verified feature branch PRs into main, verify production deployment, bump version, create git tag, aggregate changelog into release notes, and notify stakeholders via YouGile.
Key Principle
Release = Merge PRs + Verify Deploy + Tag Version + Notify.
With feature-branch strategy, PRs must be merged before tagging.
With direct strategy, merge steps are skipped (code already on main).
Version follows SemVer. Changelog comes from .tl/changelog.md.
Invocation
/nacl-tl-release # full release: merge PRs + deploy verify + version + tag
/nacl-tl-release --minor # force minor version bump
/nacl-tl-release --major # force major version bump
/nacl-tl-release --patch # force patch version bump
/nacl-tl-release --dry-run # show what would be merged + version bump, no action
/nacl-tl-release --pr 42,45 # merge specific PRs (skip discovery)
/nacl-tl-release --yes # skip user confirmation gates
Removed Flags (W4-blocking-release)
Five flags were REMOVED in W4-blocking-release across the chain. The literal flag tokens have been scrubbed from this skill's prose to satisfy the W4 grep acceptance check (a literal-token search across the skill family must return empty). The removed flags are identified here by descriptive name only:
| Removed flag (descriptive name) | Replacement |
|---|---|
| the SKIP-MERGE flag (was: tag-only mode bypassing the merge action) | For prototype projects with git.strategy == "direct", the merge action is skipped by configuration (no PRs to merge). For standard projects, every release goes through PR + CI. Direct-strategy releases on project_kind: prototype require a signed exception with affected_gates: [skipped-pr, skipped-ci]. |
the SKIP-VERIFY flag (was: bypass staging verification; lived on nacl-tl-deliver, consumed here) | Removed at source in W4. The release-time read of verification_evidence = 'no-test' no longer has an upstream producer. Bulk-bypass routes through emergency mode (see below). |
the SKIP-DEPLOY flag (was: bypass health check; lived on nacl-tl-deliver, consumed here) | Removed at source in W4. Missing PROD_GOLDEN_PATH evidence is now a release-blocker. |
the NO-TEST flag (was: permitted no-test evidence; lived on nacl-tl-full / nacl-tl-conductor, consumed here) | Removed at source in W4. no-test evidence is no longer producible by the chain. |
the FORCE flag (was: per-skill bypass; lived on nacl-tl-reconcile, consumed here transitively) | Removed at source in W4. |
(W3 also removed the bulk-QA-skip flag; W5 will remove the SKIP- DELIVER flag; W9 will remove the SKIP-PLAN flag. None of these are re-enabled by signed exceptions.)
The bulk-bypass use case those flags served is now routed through
emergency mode — see
nacl-tl-core/references/emergency-mode.md. Emergency mode is a
separate top-level invocation pattern (three env vars), NOT a
flag, and is loudly recorded in release-status.json +
.tl/changelog.md + .tl/emergencies/<timestamp>-<slug>.yaml.
Configuration Resolution
IMPORTANT: Read config.yaml first for all settings. Fall back to defaults if missing.
| Data | Source priority (check in order, use first found) |
|---|---|
| Git strategy | git.strategy > fallback "feature-branch" |
| Base branch | git.main_branch > fallback "main" |
| Merge method | git.merge_method > fallback "squash" |
| Production URL | deploy.production.url > no default |
| Health endpoint | deploy.production.health_endpoint > fallback "/api/health" |
| CI timeout | deploy.production.ci_timeout > fallback 600 (seconds) |
| CI platform | deploy.ci_platform > detect from .github/workflows/ |
| YouGile to_release column | yougile.columns.to_release |
| YouGile done column | yougile.columns.done |
If config.yaml missing → use all fallback defaults. If YouGile missing → skip task discovery and moves.
State File: .tl/release-status.json
Persists release progress for resumption:
{
"started": "2026-04-11T14:00:00Z",
"prs": [
{ "number": 42, "title": "feat: UC-028 Funnel event tracking", "status": "merged" },
{ "number": 45, "title": "feat: UC-029 Scene prompt display", "status": "pending" }
],
"merge": { "status": "in_progress", "merged_count": 1, "total": 2 },
"ci": { "status": "pending" },
"health": { "status": "pending" },
"version": { "status": "pending", "bump": null, "value": null },
"tag": { "status": "pending" },
"graph": { "status": "pending" },
"release": { "status": "pending" },
"yougile": { "status": "pending" }
}
Always update after each step completes. This enables resumption.
Release Blocking Gates (Strict-Only)
Introduced in: W4-blocking-release.
The release skill refuses VERIFIED → release-tag / promote when ANY
of the seven conditions below holds. These gates are strict-only —
strict is the single, unconditional mode; there is no fallback
branch, no --skip-* flag, and no inline operator-prompt override.
The only sanctioned override paths are: (a) a signed exception under
the schema below, and (b) emergency mode (separate invocation,
loudly recorded — see nacl-tl-core/references/emergency-mode.md).
The Project-Alpha stale-graph episode (live graph 1,083 nodes vs
handover-artifact 970 nodes; /nacl-sa-validate full = FAIL with 1
CRITICAL + 156 WARNINGs; release proceeded under operator override)
and the project-beta health-only episode (/api/health returned 200
OK but no upload golden path ever executed; first real call 404'd)
are the canonical episodes these gates exist to prevent.
The Seven Block Conditions
| # | Condition | Refusal headline | Workflow detail |
|---|---|---|---|
| 1 | Upstream tl-sync verdict is UNVERIFIED (per W2) — wire-evidence missing for any UC with actor != SYSTEM | RELEASE HALTED — UNVERIFIED (upstream-sync-unverified) | upstream-sync-unverified |
| 2 | tl-qa aggregate is UNVERIFIED (per W3) — a mandatory stage (typically LIVE_PROVIDER_SMOKE or PROD_GOLDEN_PATH) is NOT_RUN, OR aggregate weakest-stage rule yielded UNVERIFIED | RELEASE HALTED — UNVERIFIED (upstream-qa-unverified) | upstream-qa-unverified |
| 3 | Graph staleness detected — snapshot vs live mismatch on the project's Neo4j instance. Baseline MUST come from a live capture; never from a stale .cypher export. A _summary.json captured pre-release (live node count, label histogram, rel-type histogram) is compared to the current live state via direct Cypher query. Any node-count delta > 0 OR any label histogram delta OR any rel-type histogram delta = STALE. | RELEASE HALTED — UNVERIFIED (graph-stale) | graph-stale |
| 4 | /nacl-sa-validate full reports Status: FAIL with at least one finding at severity: CRITICAL | RELEASE HALTED — UNVERIFIED (sa-validate-critical) | sa-validate-critical |
| 5 | Missing PROD_GOLDEN_PATH evidence. A bare HTTP 200 from /health is HEALTH_ONLY evidence and is never product-readiness evidence. The release requires a PROD_GOLDEN_PATH evidence string in the QA aggregate (per W3 six-stage decomposition) for every UC where the matrix marks PROD_GOLDEN_PATH mandatory. | RELEASE HALTED — UNVERIFIED (missing-prod-golden-path) | missing-prod-golden-path |
| 6 | PR / CI skipped without project_kind: prototype AND a signed exception. Direct-strategy releases (no PR, no CI) are permitted only when config.yaml declares project_kind: prototype AND .tl/exceptions/ contains a valid (unexpired, well-formed) exception with affected_gates including the literal skipped-pr and / or skipped-ci matching what is actually skipped. | RELEASE HALTED — UNVERIFIED (skipped-pr-without-prototype-exception) or (skipped-ci-without-prototype-exception) | skipped-pr-without-prototype-exception or skipped-ci-without-prototype-exception |
| 7 | Stale downstream of an unreviewed change. /nacl-sa-validate full reports a blocking L8 finding. Two distinct arms: (7a) L8.1 — a node carries review_status='stale', i.e. un-built work whose source spec moved and which was never re-planned. Clear by running /nacl-tl-plan. (7b) L8.1b returned CRITICAL — the spec_drift backlog (shipped code whose spec moved under it) has outgrown validation.spec_drift_budget on count or age. Re-planning cannot clear these; clear them by recording a review verdict per nacl-tl-fix Step 7.5b. An L8.1b WARNING does not block. This is distinct from #4 (any CRITICAL) and #3 (snapshot vs live count): #7 is specifically "a recorded change has un-propagated dependents." | RELEASE HALTED — UNVERIFIED (stale-downstream) or (spec-drift-backlog) | stale-downstream or spec-drift-backlog |
Conditions #4 and #7 both surface through
/nacl-sa-validate full: #4 is the generic "any CRITICAL" gate, #7 names the staleness CRITICAL (L8) specifically so the refusal headline tells the operator what to do rather than just "validation failed." If L8 fires, prefer the L8 headline —stale-downstreamfor 7a (the remedy istl-plan),spec-drift-backlogfor 7b (the remedy is a review verdict, whichtl-plancannot supply). Naming the wrong one sends the operator to a skill that cannot close the finding.
HEALTH_ONLY vs PROD_GOLDEN_PATH
HEALTH_ONLY evidence:
- Is the literal HTTP response from
{production_url}{health_endpoint}returning 200 OK. - Confirms the deploy reached a running process and the process can serve at least one HTTP request.
- Does NOT confirm that any product flow executed end-to-end against production data, against the production database, against the production provider keys, or with production-grade payload sizes.
- Is the kind of evidence Step 3b of this skill collects.
- Is NEVER product-readiness evidence on its own. The project-beta episode (health green; upload golden path 404 on first real call) is the canonical proof.
PROD_GOLDEN_PATH evidence (per W3 six-stage decomposition):
- Is a recorded end-to-end run of the UC's primary happy path against production: real auth, real database write, real provider call (with real provider key when applicable), real artifact returned.
- Lives in the QA aggregate as the
qa-stage:prod-golden-path:VERIFIEDevidence string (orqa-stage:prod-golden-path:NOT_RUNwhen the stage did not run). - Is required by the release gate (condition #5 above) for every UC where the W3 mandatory-stage matrix marks
PROD_GOLDEN_PATHmandatory.
The release skill MUST distinguish between the two: condition #5
fires when PROD_GOLDEN_PATH is missing or NOT_RUN on a UC where
the matrix marks it mandatory, EVEN IF the Step 3b /health probe
returned 200. The health probe is a complement to
PROD_GOLDEN_PATH, not a substitute.
project_kind=prototype + Signed Exception (the PR/CI carve-out)
The carve-out is conjunctive. A direct-strategy release without PR and without CI is permitted only when both:
config.yamldeclaresproject_kind: prototype, AND- A signed exception exists with
affected_gatesenumerating exactly the gate names being skipped (skipped-pr,skipped-ci, or both).
Neither condition alone is sufficient. Prototype-mode without an
exception → block. Exception without prototype-mode → block (the
exception is rejected at load time as malformed —
exception-prototype-only-gate-on-standard-project).
See nacl-tl-core/references/config-schema.md § "W4 PR/CI
Carve-Out (binding)".
Signed Exception Schema (Binding)
.tl/exceptions/<exception_id>.yaml is the only override mechanism
for the seven block conditions above (other than emergency mode). The
schema is defined in .tl/exceptions/_template.yaml. The eight
required fields are:
| Field | Type | Notes |
|---|---|---|
exception_id | string, format EXC-YYYY-MM-DD-<slug> | enforced via regex ^EXC-\d{4}-\d{2}-\d{2}-[a-z0-9][a-z0-9-]*$ |
owner | string | GitHub handle (no @) or team name |
reason | string | concrete justification; the literals "urgent", "blocked", "needed for demo", and any single-word value are rejected |
created_at | ISO-8601 timestamp (UTC) | wall-clock at file creation |
expiry | ISO-8601 timestamp (UTC) | wall-clock at which the exception STOPS overriding |
affected_gates | list of strings | MUST enumerate specific gate names; ["*"], ["all"], or any catch-all token is rejected |
affected_projects | list of strings | project ids the exception applies to |
followup_task | string | task id or in-repo path of the follow-up that closes the underlying issue |
Recognised gate names for affected_gates (W4 release-skill set):
skipped-pr, skipped-ci, upstream-sync-unverified,
upstream-qa-unverified, graph-stale, sa-validate-critical,
missing-prod-golden-path, stale-downstream, spec-drift-backlog. (The cross-skill set — repo-checks-RED,
wire-evidence-missing, LIVE_PROVIDER_SMOKE, etc. — is documented
in .tl/exceptions/_template.yaml header.)
The Four Binding Rules
-
Expired = blocker. When
expiryis in the past at the moment the gate is evaluated, the exception is treated as ABSENT. The named gate refuses VERIFIED again with no grace period. Workflow detail:exception-expired. -
No silent extension. Editing the
expiryof an existing exception file is detected as schema tampering (the release skill records exception-file content-hashes inrelease-status.jsonon first read; a hash mismatch on the sameexception_idtriggers refusal with workflow detailexception-id-reused-without-renewal). -
Renewal requires a new
exception_id. The id format embeds the creation date and a slug; a renewal is a new file with a new id (typicallyEXC-<renewal-date>-<same-slug>-r2). The renewal'sreasonfield references the prior id. -
No blanket overrides.
affected_gatesMUST enumerate specific gate names.["*"],["all"],["any"], or any catch-all token is rejected at load time with workflow detailexception-affects-blanket-gates. Each gate the operator wants to override is listed individually.
Surfacing
Signed exceptions consumed by a release run are surfaced in three places:
-
Release notes — the GitHub release body (Step 8) includes a
## Active exceptionssection listing every exception consumed, withexception_id,affected_gates,expiry, andfollowup_task. -
.tl/release-status.json— under a new"exceptions"key:"exceptions": [ { "exception_id": "EXC-2026-05-22-stale-graph-projectalpha", "affected_gates": ["graph-stale"], "expiry": "2026-05-23T08:35:00Z", "followup_task": "TECH-042-graph-refresh" } ] -
.tl/conductor-state.json— every active exception that affects a wave-tip commit is appended to theexceptions[]array maintained by the conductor (W5 owns the conductor reconciliation that keeps this array in sync).
Removed-Flag Rule
The five W4-owned removed flags (SKIP-MERGE, SKIP-VERIFY, SKIP- DEPLOY, NO-TEST, FORCE — see the table in the "Removed Flags" section above) and the cross-wave removed flags (the bulk-QA-skip flag owned by W3, the SKIP-DELIVER flag owned by W5, the SKIP-PLAN flag owned by W9) are NOT re-enabled by signed exceptions. The flag surface is gone. Bulk-bypass routes through emergency mode only.
Emergency Mode (the bulk-bypass path)
When a release must advance past one or more of the seven block conditions in a situation that signed exceptions cannot anticipate (production outage, security rollback, ransomware response), the operator invokes emergency mode.
Emergency mode is not a --skip-* flag. It is a triple of
environment variables set on the same shell command:
NACL_EMERGENCY=1 \
NACL_EMERGENCY_REASON="prod 500s on /api/release/v0.18.0 — rolling back" \
NACL_EMERGENCY_OWNER="magznikitin" \
claude --skill nacl-tl-release
All three are REQUIRED. Behavior:
- Every Strict-Only gate still evaluates.
- Every gate that would have refused VERIFIED prints a bypass banner naming itself (one per gate, on stderr).
- The skill advances past the refusal and writes a structured
event to
.tl/emergencies/<UTC-timestamp>-<slug>.yaml. release-status.jsongets an"emergency"key with the event id and bypassed-gate list..tl/changelog.mdgets a blockquote line under the in-flight version heading naming the bypass.- The terminal
Status:carries the suffix(emergency-bypass)and is NEVER promoted toVERIFIED.
Full schema and rules: nacl-tl-core/references/emergency-mode.md.
Event-file template: .tl/emergencies/_template.yaml.
Emergency mode does NOT re-enable any removed flag, does NOT silence the gates, and does NOT extend over multiple invocations.
Workflow: 9 Steps
Step 0: PRE-CHECK
-
Read
config.yaml→ resolve all settings (see table above) -
If
git.strategy == "direct":- Skip the merge action of Step 2 (no
gh pr mergecalls). - Enforce the W4 PR/CI carve-out (binding): verify that
config.yamldeclaresproject_kind: prototypeAND that a signed exception under.tl/exceptions/listsskipped-pr(andskipped-ciif CI is also skipped) in itsaffected_gates. If either condition is missing, refuse withStatus: BLOCKEDand one of the workflow detailsskipped-pr-without-prototype-exception/skipped-ci-without-prototype-exception. Do NOT proceed to Step 3. (Forproject_kind: standard,git.strategy == "direct"itself is a configuration error and the prelude refuses with workflow detaildirect-strategy-on-standard-project.) - DO NOT skip the pre-merge graph-proof gate. The type-aware gate at the top of Step 2 (feature PRs → Task-node check + MISSING TASK NODE halt; fix PRs → Decision/level check + UNRECORDED SPEC DRIFT halt; status branching; REGRESSION exclusion) MUST run in every mode (P1 / 0.14.0 contract). The direct-strategy carve-out changes which artifacts are produced, not whether the gate runs.
- Run the gate over the candidate UC list collected in Step 1, or
— if direct-mode bypasses Step 1 entirely — over the UCs
associated with commits since the last tag (
gh pr list --state merged --base {main_branch}sincegit describe --tags --abbrev=0). - After the gate, jump to Step 3 (verify production deployment).
- Skip the merge action of Step 2 (no
-
Check for existing
.tl/release-status.json:- If exists → RESUME MODE (skip to incomplete step)
-
Ensure we're on the base branch or can switch to it:
git fetch origin {main_branch}
Step 1: COLLECT RELEASE CANDIDATES
Find open PRs targeting {main_branch} that are ready for release.
Source A — YouGile (if configured):
Query tasks in yougile.columns.to_release. For each task, extract the PR URL from the task chat (posted by nacl-tl-ship / nacl-tl-deliver).
Source B — GitHub (fallback or supplemental):
gh pr list --base {main_branch} --state open --json number,title,headRefName,mergeable,reviews,statusCheckRollup
Filter to PRs that are:
- Targeting
{main_branch} - All CI checks passing (or no CI configured)
- At least one approving review OR authored by automation
If --pr 42,45 provided: skip discovery, use those specific PRs:
gh pr view 42 --json number,title,headRefName,mergeable,reviews,statusCheckRollup
gh pr view 45 --json number,title,headRefName,mergeable,reviews,statusCheckRollup
If no PRs found: skip Steps 1-3, proceed to Step 4 (tag-only mode — code was merged manually or via direct strategy).
Write initial .tl/release-status.json with discovered PRs.
Step 2: MERGE TO MAIN (USER GATE)
Pre-merge graph-proof gate (runs BEFORE presenting merge plan):
The gate is type-aware: a feature PR proves it shipped a planned UC via its Task node;
a fix PR proves it via the Decision node nacl-tl-fix records (L2/L3-spec-gap) or a
code-only Fix-level marker (L0/L1) — not a Task node, which the bug-fix path correctly
never creates. The per-PR verdict is computed by the single-authority classifier
nacl-core/scripts/classify-pr-merge.mjs (pure, never opens Neo4j, pinned by
classify-pr-merge.test.mjs); this skill gathers the graph rows + trailers and feeds them in,
so the verdict is reproducible:
node nacl-core/scripts/classify-pr-merge.mjs '<pr-json>' # or a JSON array of PRs
# → { "verdict": "MERGE" | "USER_GATE" | "HALT", "detail": <code|null>, "proof": "<graph proof>" }
For each PR in the release candidate list, FIRST classify the PR by its conventional-commit
title prefix (read once: gh pr view <N> --json title,body,commits):
feat:/feature:— or any non-fix:prefix without aFix-level:trailer → FEATURE PR → 1a.fix:— or any PR carrying aFix-level:trailer → FIX PR → 1b.
1a. FEATURE PR — Task-node check (unchanged: graph only, no JSON fallback). Identify the underlying UC(s) and query the graph:
MATCH (t:Task)
WHERE t.id IN [<UC list>]
RETURN t.id, t.status, t.verification_evidence
Feed {prefix:"feat", taskNodeMissing:<query returned no row>, taskStatus:<t.status>}. Verdicts:
| Classifier result | Merge action |
|---|---|
HALT / MISSING_TASK_NODE (no row) | HALT immediately. Print RELEASE HALTED — MISSING TASK NODE / "UC### has no Task node in the graph. The graph may be out of sync. Run /nacl-tl-diagnose to reconcile before retrying the release." Do NOT fall back to .tl/status.json. Do NOT proceed. |
MERGE (done) | Include in merge plan normally |
USER_GATE (verified-pending / blocked) | HALT: "PR #N has UC### with UNVERIFIED/blocked dev status. Merge without verification? [yes/no] Default: no". If user confirms → include with warning. If not → exclude; report RELEASE HALTED — UNVERIFIED |
HALT / REGRESSION (failed / regression) | DO NOT include; report "PR #N excluded — REGRESSION in UC###"; flag RELEASE INCOMPLETE — REGRESSION |
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 27
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
nacl-tl-release- Source
- github.com/itsalt/nacl