nacl-tl-release

SkillDocs & knowledge

Full 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.

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_release graph 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.

DataSource priority (check in order, use first found)
Git strategygit.strategy > fallback "feature-branch"
Base branchgit.main_branch > fallback "main"
Merge methodgit.merge_method > fallback "squash"
Production URLdeploy.production.url > no default
Health endpointdeploy.production.health_endpoint > fallback "/api/health"
CI timeoutdeploy.production.ci_timeout > fallback 600 (seconds)
CI platformdeploy.ci_platform > detect from .github/workflows/
YouGile to_release columnyougile.columns.to_release
YouGile done columnyougile.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

#ConditionRefusal headlineWorkflow detail
1Upstream tl-sync verdict is UNVERIFIED (per W2) — wire-evidence missing for any UC with actor != SYSTEMRELEASE HALTED — UNVERIFIED (upstream-sync-unverified)upstream-sync-unverified
2tl-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 UNVERIFIEDRELEASE HALTED — UNVERIFIED (upstream-qa-unverified)upstream-qa-unverified
3Graph 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: CRITICALRELEASE HALTED — UNVERIFIED (sa-validate-critical)sa-validate-critical
5Missing 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
6PR / 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
7Stale 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-downstream for 7a (the remedy is tl-plan), spec-drift-backlog for 7b (the remedy is a review verdict, which tl-plan cannot 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:VERIFIED evidence string (or qa-stage:prod-golden-path:NOT_RUN when 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_PATH mandatory.

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:

  1. config.yaml declares project_kind: prototype, AND
  2. A signed exception exists with affected_gates enumerating 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:

FieldTypeNotes
exception_idstring, format EXC-YYYY-MM-DD-<slug>enforced via regex ^EXC-\d{4}-\d{2}-\d{2}-[a-z0-9][a-z0-9-]*$
ownerstringGitHub handle (no @) or team name
reasonstringconcrete justification; the literals "urgent", "blocked", "needed for demo", and any single-word value are rejected
created_atISO-8601 timestamp (UTC)wall-clock at file creation
expiryISO-8601 timestamp (UTC)wall-clock at which the exception STOPS overriding
affected_gateslist of stringsMUST enumerate specific gate names; ["*"], ["all"], or any catch-all token is rejected
affected_projectslist of stringsproject ids the exception applies to
followup_taskstringtask 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
  1. Expired = blocker. When expiry is 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.

  2. No silent extension. Editing the expiry of an existing exception file is detected as schema tampering (the release skill records exception-file content-hashes in release-status.json on first read; a hash mismatch on the same exception_id triggers refusal with workflow detail exception-id-reused-without-renewal).

  3. 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 (typically EXC-<renewal-date>-<same-slug>-r2). The renewal's reason field references the prior id.

  4. No blanket overrides. affected_gates MUST enumerate specific gate names. ["*"], ["all"], ["any"], or any catch-all token is rejected at load time with workflow detail exception-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:

  1. Release notes — the GitHub release body (Step 8) includes a ## Active exceptions section listing every exception consumed, with exception_id, affected_gates, expiry, and followup_task.

  2. .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"
      }
    ]
    
  3. .tl/conductor-state.json — every active exception that affects a wave-tip commit is appended to the exceptions[] 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.json gets an "emergency" key with the event id and bypassed-gate list.
  • .tl/changelog.md gets a blockquote line under the in-flight version heading naming the bypass.
  • The terminal Status: carries the suffix (emergency-bypass) and is NEVER promoted to VERIFIED.

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

  1. Read config.yaml → resolve all settings (see table above)

  2. If git.strategy == "direct":

    • Skip the merge action of Step 2 (no gh pr merge calls).
    • Enforce the W4 PR/CI carve-out (binding): verify that config.yaml declares project_kind: prototype AND that a signed exception under .tl/exceptions/ lists skipped-pr (and skipped-ci if CI is also skipped) in its affected_gates. If either condition is missing, refuse with Status: BLOCKED and one of the workflow details skipped-pr-without-prototype-exception / skipped-ci-without-prototype-exception. Do NOT proceed to Step 3. (For project_kind: standard, git.strategy == "direct" itself is a configuration error and the prelude refuses with workflow detail direct-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} since git describe --tags --abbrev=0).
    • After the gate, jump to Step 3 (verify production deployment).
  3. Check for existing .tl/release-status.json:

    • If exists → RESUME MODE (skip to incomplete step)
  4. 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 a Fix-level: trailer → FEATURE PR → 1a.
  • fix: — or any PR carrying a Fix-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 resultMerge 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