nacl-tl-ship

SkillProductivity

Commit, push, create PR, and update YouGile after development. Reads git strategy from config.yaml (direct vs feature-branch). Posts commit summary to YouGile task chat. Use when: ship code, commit and push, create PR, finish development, or the user says "/nacl-tl-ship".

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-ship skill

What this skill tells your AI

The instructions your AI receives, as published by itsalt/nacl in nacl-tl-ship/SKILL.md and read by ahel’s review.

Contract

Inputs this skill consumes:

  • Prior verification status from .tl/status.json or YouGile task chat (six-status vocabulary: PASS / BLOCKED / UNVERIFIED / NO_INFRA / RUNNER_BROKEN / REGRESSION)
  • Local test-suite results (sanity check, NOT a substitute for upstream status)
  • config.yaml (git strategy)

Outputs this skill produces:

  • Headline one of: SHIP COMPLETE / SHIP APPLIED — UNVERIFIED (only when user explicitly overrides on --deploy with non-PASS upstream) / SHIP HALTED — {NO_INFRA | RUNNER_BROKEN | UNVERIFIED | BLOCKED} / SHIP INCOMPLETE — REGRESSION
  • Commit + push to feature branch
  • PR creation (gated on aggregated PASS or explicit override)

Downstream consumers of this output:

  • nacl-tl-deliver
  • nacl-tl-deploy
  • nacl-tl-release

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 Ship — Commit + Push + PR + YouGile

Your Role

You are the shipping specialist. After development is complete (code written, tests pass, review approved), you commit, push, create a PR if needed, and notify YouGile. You bridge the gap between "code works locally" and "code is in the repo."

Key Principle

Read git strategy from config.yaml → commit → push → PR (if feature-branch) → YouGile update
Never push without passing tests first.

Safety Rules

ABSOLUTE RULE: When strategy == "feature-branch", you MUST NOT commit to base_branch. No exceptions. No rationalizations. Not "all recent commits are small fixes." Not "the user gave a bare message." Not "this is a tiny one-liner." If you are on base_branch and strategy is feature-branch, you MUST create a new branch BEFORE committing.

  1. NEVER commit to base_branch when strategy == "feature-branch". This is the highest-priority rule. It overrides ALL edge cases below. If you are about to git commit on base_branch with feature-branch strategy — STOP, you have a bug in your reasoning.
  2. NEVER switch branches. If on branch X (not base_branch), commit to X. Period.
  3. NEVER push to a branch other than the current one. No git checkout main. No autonomous "this is a hotfix" decisions.
  4. If you believe the current branch is wrong — ASK the user. Suggest /nacl-tl-hotfix if critical.
  5. The ONLY skill authorized to target main from another branch is /nacl-tl-hotfix.
  6. MECHANICAL GUARD: Before every git commit, run the base-branch assertion (Step 2.5). If it prints FATAL — do NOT proceed.

Invocation

/nacl-tl-ship UC028                    # ship specific UC
/nacl-tl-ship --feature FR-001         # ship all tasks in a feature
/nacl-tl-ship "custom commit message"  # ship with explicit message
/nacl-tl-ship                          # ship all uncommitted changes (auto-compose message)
/nacl-tl-ship --deploy                 # ship + deploy (method from config.yaml)
/nacl-tl-ship UC028 --deploy           # ship UC + deploy

Configuration Resolution

IMPORTANT: Projects may use different config.yaml structures. Check BOTH formats:

DataSource priority (check in order, use first found)
Git strategygit.strategy > modules.[name].git_strategy > fallback "feature-branch"
Base branchgit.main_branch > modules.[name].git_base_branch > fallback "main"
Branch prefixgit.branch_prefix > fallback "feature/"
Build commandmodules.[name].build_cmd > fallback npm run build
Test commandmodules.[name].test_cmd > fallback npm test
Module pathmodules.[name].path > detect from package.json
YouGile dev_doneyougile.columns.dev_done
Deploy method (staging)deploy.staging.method > deploy.method > fallback "github-actions"
Deploy scriptdeploy.staging.script > no default
Skip CI flagdeploy.staging.skip_ci > fallback false
Deploy env filedeploy.staging.env_file > no default

Two config.yaml formats exist in projects:

Format A (per-module):

modules:
  backend:
    git_strategy: "feature-branch"
    git_base_branch: "main"

Format B (top-level git section):

git:
  strategy: "feature-branch"
  main_branch: "main"
  branch_prefix: "feature/"

Always check Format B first (top-level git: section), then Format A (modules.[name]). Never assume only one format exists.

If config.yaml missing → use all fallback defaults. If YouGile missing → skip task moves.


Workflow: 6 Steps

Step 1: PRE-FLIGHT CHECKS

  1. Read prior verification status (BEFORE running local tests):

    Check .tl/status.json for the task being shipped, or read the sub-skill output from the task chat. Look for the six-status vocabulary:

    Prior statusAction
    PASSProceed normally
    UNVERIFIEDHALT. Post advisory: "Task dev status is UNVERIFIED — no test exercises the change. Local tests passing does NOT substitute for upstream verification. Confirm to ship unverified? [yes/no] Default: no". If the operator answers explicitly "yes" (NOT auto-confirmed by --yes), proceed with SHIP APPLIED — UNVERIFIED headline; PR description is annotated; auto-deploy via --deploy is refused in this state — the operator must run /nacl-tl-deploy separately as an explicit deploy override. If no answer → SHIP HALTED — UNVERIFIED.
    BLOCKEDHALT. Post: "Task dev status is BLOCKED — pre-existing failures. Confirm to ship blocked task? [yes/no] Default: no". If confirmed → proceed under SHIP APPLIED — UNVERIFIED (BLOCKED override); auto-deploy refused. If not → SHIP HALTED — BLOCKED.
    NO_INFRAHALT. Report: SHIP HALTED — NO_INFRA. Recommend fixing infra.
    RUNNER_BROKENHALT. Report: SHIP HALTED — RUNNER_BROKEN. Escalate.
    REGRESSIONHALT. Report: SHIP INCOMPLETE — REGRESSION. Do NOT ship.
    Unknown / not found (no status.json AND no Task node in graph)HALT. Report: SHIP HALTED — UNVERIFIED (upstream status unknown). The previous "warn and proceed" backward-compat path has been removed (P5 + 0.14.0 contract). The operator must populate the graph or .tl/status.json, or invoke an explicit user-initiated path that knowingly accepts the unknown state.

    Local tests passing does NOT override prior UNVERIFIED/BLOCKED/unknown status. Local tests are a sanity check, not a verification substitute.

    Reaffirmed: ship never switches branches autonomously (P5). Ship commits to the current branch only. Hotfix-to-main is a separate user-initiated skill (/nacl-tl-hotfix); a non-PASS upstream status does NOT cause this skill to pivot to a hotfix branch or to main. The operator chooses.

  2. Read config.yaml for git strategy, module config, and deploy config

  3. Run tests for affected modules:

    cd [module_path] && [test_cmd]
    
  4. Run build:

    cd [module_path] && [build_cmd]
    
    • If --deploy flag is set and deploy.staging.env_file exists: load env vars from that file before building the frontend module. This injects staging-specific variables (e.g. NEXT_PUBLIC_VK_CLIENT_ID) into the static frontend build.
  5. If tests or build FAIL → report and stop. Do not push broken code.

  6. Check for uncommitted changes: git status

  7. If no changes → report "nothing to ship" and exit

Remote mode (multi-user shared graph): when config.yaml graph.mode: remote, step 0 above reads the prior verification status from the graph Task node (authoritative), NEVER from the local .tl/status.json (a per-clone cache — another developer may have verified the work, or yours may be stale). After a successful push (end of Step 5/6), release your claim-lock: node nacl-core/scripts/claim-task.mjs release --task <id> --dev "$NACL_DEVELOPER_ID" (run the emitted Cypher via mcp__neo4j__write-cypher). Local mode is unchanged. See nacl-tl-core/references/remote-mode-coordination.md.

Step 2: DETERMINE GIT STRATEGY

current_branch = git rev-parse --abbrev-ref HEAD
base_branch = config.yaml → git.main_branch (default: "main")
strategy = config.yaml → git.strategy (default: "feature-branch")
SituationAction
Already on a feature/topic branch (current_branch != base_branch)STAY here. Commit and push to this branch.
On base_branch AND strategy == directStay on base branch, push directly.
On base_branch AND strategy == feature-branchMUST create a new branch before committing. See naming below.

IMPORTANT: If on a feature branch, ALWAYS commit there — regardless of whether the change "matches" the branch name. If user wants it on main → /nacl-tl-hotfix.

NOTE (conductor-driven invocations): When this skill is invoked under /nacl-tl-conductor, the feature branch is pre-created by the conductor and may host multiple UCs in sequence. Do not require the branch name to match the UC ID — a mismatch is expected and must not halt the ship.

Branch naming (when creating a new branch)
SourceBranch nameExample
UC identifier providedfeature/UC028/nacl-tl-ship UC028
FR identifier providedfeature/FR-001/nacl-tl-ship --feature FR-001
Bare commit messagefeature/[slug]"fix: add lecture breadcrumb" → feature/fix-add-lecture-breadcrumb
Auto-composed messagefeature/[slug]Auto: "fix: cast lectureId" → feature/fix-cast-lectureid

Slugification — branch.sh slug is the single authority (reproduces the historical tr | sed | cut pipeline; equivalence pinned by nacl-core/scripts/branch.test.sh):

slug=$(bash nacl-core/scripts/branch.sh slug "$message")
git checkout -b "feature/${slug}" "$base_branch"

There is NO scenario where strategy == "feature-branch" and you commit directly to base_branch. If you cannot derive a branch name — ask the user. Do NOT fall back to base_branch.

Step 2.5: BASE-BRANCH GUARD (mandatory)

Run this BEFORE every git commit. Non-negotiable. The guard logic is the single authority in nacl-core/scripts/branch.sh (equivalence pinned by nacl-core/scripts/branch.test.sh): exit 1 prints FATAL … and blocks the commit, exit 0 prints GUARD OK ….

# base_branch and strategy were read from config.yaml in Step 1
bash nacl-core/scripts/branch.sh guard "$(git rev-parse --abbrev-ref HEAD)" "$base_branch" "$strategy"

If FATAL → go back to Step 2, create the branch. Do NOT skip or override this check.

Step 3: COMMIT

  1. Pre-commit check: Verify you ran Step 2.5 and are NOT on base_branch with feature-branch strategy. If you are — STOP, go back to Step 2.

  2. Stage relevant files (smart staging — exclude .env, node_modules, dist/, .tl/qa-screenshots/):

    git add [relevant files]
    
  3. Compose commit message. Priority chain:

    IF user provided a message in invocation → use it
    IF .tl/changelog.md has entries since last commit → compose from latest entries
    IF neither → auto-compose from git diff:
    
    • If --deploy flag is set and deploy.staging.skip_ci == true in config.yaml: append [skip ci] to the first line of the commit message. This prevents GitHub Actions (both CI and deploy workflows) from running.
      - Read `git diff --cached --stat` for changed files
      - Read `git diff --cached` for actual changes (first ~100 lines)
      - Compose: "fix/feat/refactor: [summary of changes]"
      - List changed files in body
    

    Example auto-composed message:

    fix: switch to Gemini 2.5 Flash models + add prompt cache
    
    - backend/src/config/env.ts: updated model names
    - backend/src/services/interview-agent.service.ts: added in-memory cache
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    

    Fix-traceability trailer (when this ship authors a fix commit). If the work being committed is a fix produced by /nacl-tl-fix — the message starts fix: and the latest .tl/changelog.md entry (or a handed-off Step 8 report) carries a Level: and Decision: line — append to the commit body, ABOVE Co-Authored-By: (same placement as the Goal-run-id: trailer):

    Fix-level: <L0|L1|L2|L3-spec-gap>
    Fix-decision: <DEC-NNN[, ...]|none>
    

    This is the deterministic link /nacl-tl-release's type-aware pre-merge gate reads to verify a fix PR by its Decision node instead of a Task node. Normally /nacl-tl-fix Phase B already wrote it on the code-fix commit; this covers only the case where ship authors the commit (--auto-ship / uncommitted changes). Omit both lines for a feature / non-fix commit.

  4. Commit:

    git commit -m "$(cat <<'EOF'
    [message]
    EOF
    )"
    

Step 4: PUSH

git push [-u origin feature/UC028]  # feature-branch
# or
git push                            # direct

If push fails (reject, conflict):

  • Pull and rebase: git pull --rebase
  • If merge conflict → report to user, do not force-push
  • Retry push after rebase

Post-push check: If strategy == "feature-branch" and you just pushed to base_branch — STOP. Something went wrong. Report the error to the user immediately.

Step 5: CREATE MERGE REQUEST / PR (feature-branch only)

If git_strategy is feature-branch:

Verification gate before PR creation:

  • If prior task status (from Step 1.0) was PASS → create PR normally
  • If prior task status was UNVERIFIED and user confirmed override in Step 1.0: → create PR with **Verification status:** UNVERIFIED note in body
  • If prior task status was BLOCKED and user confirmed override in Step 1.0: → create PR with **Verification status:** BLOCKED (user override) in body
  • If prior task status was REGRESSION → DO NOT create PR; report SHIP INCOMPLETE — REGRESSION
  • If no status found (backward-compat) → create PR normally

Create Pull Request on GitHub:

gh pr create \
  --title "feat: UC-028 Funnel event tracking" \
  --body "$(cat <<'EOF'
## Summary
- POST /api/analytics/event with idempotent dedup
- FunnelEvent entity + migration
- Integration hooks across 7 existing pages

## Test Plan
- [x] 34 unit tests passing
- [ ] E2E verification via /nacl-tl-verify

**Verification status:** PASS

Generated with Claude Code
EOF
)" \
  --base [git_base_branch]

Step 5.5: DEPLOY (config-driven, only with --deploy flag)

This step only runs when --deploy flag is provided.

Verification gate before deploy:

  • If prior task status (from Step 1.0) was PASS → proceed with deploy.
  • If prior task status was UNVERIFIED (operator-confirmed ship in Step 1.0) → auto-deploy is refused. This skill emits an advisory: Auto-deploy disabled — upstream status is UNVERIFIED. Run /nacl-tl-deploy --staging separately as an explicit deploy override. The PR is created and the report ends with the SHIP APPLIED — UNVERIFIED headline. --deploy does NOT chain into deploy under unverified upstream.
  • If prior task status was BLOCKED (operator-confirmed ship) → same as UNVERIFIED: no auto-deploy, separate explicit operator action required.
  • If prior task status was REGRESSION → SHIP INCOMPLETE — REGRESSION; no PR; no deploy.
  • If upstream status was unknown → never reaches Step 5.5 (Step 1.0 already halted).
  • Deploy never bypasses verification. Local tests passing does not substitute.

Read deploy strategy from config.yaml:

current_branch = git rev-parse --abbrev-ref HEAD
main_branch = config.yaml → git.main_branch (default: "main")

IF current_branch == main_branch:
  SKIP — production deploys always go through CI pipeline.
  Report: "Production deploy happens via GitHub Actions after push to main."

ELSE (feature/staging branch):
  method = config.yaml → deploy.staging.method (default: "github-actions")

  IF method == "direct":
    script = config.yaml → deploy.staging.script
    IF script exists:
      Run: bash [script]
      Report result (success/failure, duration)
    ELSE:
      Report error: "deploy.staging.script not found"

  ELSE IF method == "github-actions":
    Report: "Deploy will happen via GitHub Actions pipeline."
    (Optionally monitor with /nacl-tl-deploy --staging)

  ELSE:
    Report error: "Unknown deploy method: [method]"

The skill does NOT know deployment specifics (SSH hosts, rsync paths, PM2 commands). All of that lives in the project's deploy script referenced by deploy.staging.script. The skill only resolves the config and executes the script.

Step 6: YOUGILE UPDATE

If config.yaml → yougile is configured:

  1. Move task to DevDone column:

    update_task(taskId, columnId: config.yougile.columns.dev_done)
    
  2. Post commit summary to task chat:

    send_task_message(taskId, message: "
    🚀 Shipped to repository
    
    Branch: feature/UC028 (PR #42)
    Commit: abc1234
    Files: 12 changed, 450 insertions, 30 deletions
    
    Tests: 34 passing
    Build: OK
    
    Next: /nacl-tl-verify UC028   (bare fix, no UC id → /nacl-tl-release --pr 42 to merge)
    ")
    

If YouGile not configured → skip, just report locally.


Output

Per-task status table is the first block of every report — the headline summarizes the aggregated status, then a Verification status: line surfaces the per-task value that was consumed (PASS / UNVERIFIED / BLOCKED / NO_INFRA / RUNNER_BROKEN / REGRESSION) so the reader sees the per-task status at a glance. Present to user (in their language):

Next-step invariant: the Next step: block lists only concrete /nacl-... commands (with current, real flags) — never prose. The block always names the skill that performs the next action. When no UC/FR id is available (e.g. a bare bug-fix), omit the id argument; do not substitute a prose description like "review and merge the PR". See the Next-step resolution table below for how to fill it.

Without --deploy (PASS case):

═══════════════════════════════════════════════
  SHIP COMPLETE
═══════════════════════════════════════════════

UC-028: Funnel Event Tracking
Verification status: PASS

Git:
  Branch: feature/UC028
  Commit: abc1234
  PR: #42 (https://github.com/org/repo/pull/42)
  Push: origin (GitHub) — OK

Tests: 34 passing
Build: OK

YouGile: task moved to DevDone, summary posted

Next step:
  /nacl-tl-verify UC028     — verify implementation (E2E/code)
  /nacl-tl-release --pr 42  — merge PR #42 into main (after verify)
═══════════════════════════════════════════════

UNVERIFIED case (user confirmed override):

═══════════════════════════════════════════════
  SHIP APPLIED — UNVERIFIED
═══════════════════════════════════════════════

UC-028: Funnel Event Tracking
Verification status: UNVERIFIED (user override — shipped without test coverage)

WARNING: No test exercises the change. Ship confirmed by user.
...
═══════════════════════════════════════════════

With --deploy (PASS — auto-deploy chained):

═══════════════════════════════════════════════
  SHIP COMPLETE — DEPLOYED (direct)
═══════════════════════════════════════════════

UC-028: Funnel Event Tracking
Verification status: PASS

Git:
  Branch: feature/UC028
  Commit: abc1234 [skip ci]
  PR: #42

Deploy (staging):
  Method: direct (via deploy/staging-direct.sh)
  Result: OK
  Duration: 47s

Tests: 34 passing
Build: OK

YouGile: task moved to DevDone, summary posted

Next step:
  /nacl-tl-verify UC028     — verify implementation (E2E/code)
  /nacl-tl-release --pr 42  — merge PR #42 into main (after verify)
═══════════════════════════════════════════════

With --deploy (UNVERIFIED — auto-deploy refused):

═══════════════════════════════════════════════
  SHIP APPLIED — UNVERIFIED (auto-deploy refused)
═══════════════════════════════════════════════

UC-028: Funnel Event Tracking
Verification status: UNVERIFIED (operator override at Step 1.0)

Git:
  Branch: feature/UC028
  Commit: abc1234
  PR: #42 (annotated: SHIP APPLIED — UNVERIFIED)

Deploy (staging):
  Method: SKIPPED — upstream UNVERIFIED. --deploy does not chain under
          non-PASS status. Run /nacl-tl-deploy --staging as a separate
          explicit operator action if you accept the risk.

Tests: 34 passing
Build: OK

YouGile: task moved to DevDone with UNVERIFIED note

Next step:
  /nacl-tl-deploy --staging   — explicit operator deploy (separate skill)
  /nacl-tl-verify UC028       — restore verified status before re-shipping
═══════════════════════════════════════════════

Bare bug-fix (PASS, no UC id) — feature-branch: When ship is invoked with a bare commit message (no UC/FR id) and upstream status is already PASS (e.g. shipped after /nacl-tl-fix, which ran a RED→GREEN regression test), the only remaining action is the merge. Name the merge skill — do NOT emit prose like "Review and merge the PR":

═══════════════════════════════════════════════
  SHIP COMPLETE
═══════════════════════════════════════════════

fix: <commit message>
Verification status: PASS

Git:
  Branch: fix/<slug>
  Commit: abc1234
  PR: #5 (https://github.com/org/repo/pull/5)
  Base: main-v2
  Push: origin (GitHub) — OK

Tests: 28 passing

YouGile: not configured

Next step:
  /nacl-tl-release --pr 5   — merge PR #5 into main-v2
  (/nacl-tl-verify          — optional: E2E-verify the fix before merge)
═══════════════════════════════════════════════

Headline selection (the only authoritative classifier is the consumed Status: line — see Step 1.0):

SHIP COMPLETE — upstream PASS, no skip flag, no health/CI failure. SHIP COMPLETE — DEPLOYED (direct) — PASS + --deploy succeeded. SHIP APPLIED — UNVERIFIED — upstream UNVERIFIED or BLOCKED with operator override; PR annotated; auto-deploy refused regardless of --deploy. SHIP HALTED — UNVERIFIED (upstream status unknown) — no .tl/status.json AND no Task node in graph (P1 / P5). SHIP HALTED — UNVERIFIED — operator declined the unverified-ship prompt at Step 1.0. SHIP HALTED — BLOCKED — operator declined the BLOCKED-ship prompt at Step 1.0. SHIP HALTED — NO_INFRA — declared workspace command missing (P2). SHIP HALTED — RUNNER_BROKEN — runner cannot be exercised. SHIP INCOMPLETE — REGRESSION — upstream REGRESSION; no PR, no deploy.

Next-step resolution

Fill the Next step: block from the data this skill already has — strategy, pr_number + resolved base_branch (git.main_branch), the consumed verification status, and whether a UC/FR id was provided. <base> = resolved base_branch; <N> = the PR number just created/found. The block always names a skill.

StrategyVerification statusNext step: block (named skills, in order)
feature-branchPASS, UC/FR id present/nacl-tl-verify <id> — verify implementation; then /nacl-tl-release --pr <N> — merge PR # into <base>
feature-branchPASS, no id (bare fix)/nacl-tl-release --pr <N> — merge PR # into <base>; (/nacl-tl-verify — optional E2E before merge)
feature-branchUNVERIFIED / BLOCKED (operator override)/nacl-tl-deploy --staging — explicit operator deploy; /nacl-tl-verify <id> — restore verified status before re-shipping
directPASS/nacl-tl-deploy — monitor CI/deploy (no PR to merge)

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-ship
Source
github.com/itsalt/nacl