publish · diff
git:20260507.173dd5b to git:20260517.c54643b
46 added, 8 removed. Audit A to A.
---
name: publish
description: Bump version, build, test, publish to npm, and install locally
arguments: "<patch|minor|major> <-rc> <--check|-ch>"
---
# /publish - Version Bump, Build, Test & Publish
Automated release pipeline for moflo. Bumps version, commits, builds, tests, runs doctor, publishes to npm, and installs the new version locally.
**Arguments:** $ARGUMENTS
## Usage
```
/publish # patch bump, default mode (skips CI-covered gates)
/publish minor # minor bump (4.8.56 → 4.9.0)
/publish major # major bump (4.8.56 → 5.0.0)
/publish -rc # patch RC bump (4.8.56 → 4.8.57-rc.1)
/publish minor -rc # minor RC bump (4.8.56 → 4.9.0-rc.1)
/publish --check # full pre-flight (lint + test + smoke + everything)
/publish -ch # short form of --check
/publish minor -ch # combine: full pre-flight on a minor bump
```
## --check / -ch flag (presence-only)
- **Default (flag absent):** runs build + doctor + trigger-based manual checks only. Skips lint/test/smoke because those gates are covered by CI on every PR + push to main (`ci.yml`, `consumer-install-smoke.yml`).
+ **Default (flag absent):** runs build + doctor + trigger-based manual checks only. Skips local lint/test/smoke because lint+test are covered by `ci.yml` on every PR + push to main, and **cross-platform smoke is dispatched as part of this skill** (Step 8.5 below) — the `release-smoke.yml` workflow runs the full 3-OS matrix on the exact commit about to be published.
- **With `--check` or `-ch` (any presence):** runs the full pre-flight from `pre-publish-rules.md` — lint, build, test, doctor (strict), clean smoke, populated smoke, and a forced full walk of every manual gate. Use this for publishes that didn't go through a green PR, risky releases, or when you want belt-and-suspenders.
+ **With `--check` or `-ch` (any presence):** runs the full pre-flight from `pre-publish-rules.md` — lint, build, test, doctor (strict), local clean smoke, local populated smoke, and a forced full walk of every manual gate. Use this for publishes that didn't go through a green PR, risky releases, or when you want belt-and-suspenders on the local box in addition to the CI matrix.
The flag is presence-only. `--check` and `-ch` both set it to true. There is no `--check=true` / `--check=false` syntax — absence means false.
---
## Step-by-Step Procedure
### Step 0: Parse Arguments
- Default bump type is `patch` if not specified
- `-rc` flag: produce a release candidate
- `--check` or `-ch` (presence): set `CHECK_MODE=true`. Otherwise `CHECK_MODE=false`.
- Determine the new version string:
- **Without `-rc`:** Use `npm version <patch|minor|major> --no-git-tag-version`
- **With `-rc`:** Calculate manually:
- If current version is already an RC of the same bump level (e.g., `4.8.57-rc.3`), increment the RC number (→ `4.8.57-rc.4`)
- Otherwise, bump the base version and append `-rc.1` (e.g., `4.8.56` → `4.8.57-rc.1`)
- Write with: `npm version <new-version> --no-git-tag-version`
### Step 1: Compute Manual-Check Fingerprint (always)
```bash
bash .claude/skills/publish/fingerprint.sh
```
The script diffs `HEAD` against the most recent `chore: install moflo@*` commit and pattern-matches the file list against the manual-gate triggers from `pre-publish-rules.md`. Output is one block:
```
Triggered manual checks: bin-scriptfiles-sync info-loss-audit
Diff range: <last-publish-sha>..HEAD
Changed files: 7
```
`Triggered manual checks: none` means gates 1/2/6 don't apply to this diff — skip them entirely.
This step costs ~50 tokens and is the cornerstone of token-efficient default mode.
### Step 2: Build (always)
```bash
npm run build
```
Always runs regardless of `CHECK_MODE`. Syncs the version to `src/cli/version.ts` via the `version` lifecycle script, then compiles all TypeScript. Confirms the deliverables on disk are correct.
**Must exit 0.** If it fails, stop and fix the build error.
### Step 3: Lint (only if `CHECK_MODE=true`)
```bash
npm run lint
```
Skipped by default — `ci.yml` runs lint on every PR with `max-warnings 0`. Run when `--check` is set.
### Step 4: Tests (only if `CHECK_MODE=true`)
```bash
npm test
```
Skipped by default — `ci.yml` runs the full test suite on every PR. Run when `--check` is set.
**Must have 0 test file failures.** If any test files fail, retest them individually to distinguish real failures from flaky ones (per broken window theory). Fix all real failures before proceeding.
### Step 5: Doctor (always — strict, no repair)
```bash
npx moflo doctor --strict
```
Doctor is the only check with no CI equivalent — it inspects local state (daemon lock, embeddings hygiene, sandbox tier, vector-stats freshness) that CI cannot validate for you. Always runs in `--strict` mode regardless of `CHECK_MODE`.
**Never `--fix` on the publish path.** A release pipeline must fail fast on broken local state, not silently repair it; a doctor that auto-repairs masks the very signal we want — "something is off, stop and investigate before shipping." If `doctor --strict` fails, stop and run `flo healer --fix` (or `npx moflo doctor --fix`) interactively, verify the repair, then retry the publish.
- ### Step 6: Smoke Tests (only if `CHECK_MODE=true`)
+ ### Step 6: Local Smoke Tests (only if `CHECK_MODE=true`)
```bash
npm run test:smoke
npm run test:smoke:populated
```
- Skipped by default — `consumer-install-smoke.yml` runs both profiles on Ubuntu/macOS/Windows on every PR + push to main. Run when `--check` is set.
+ Skipped by default. Local smoke covers the same harness as CI's Ubuntu leg, so it adds little signal over `release-smoke.yml`'s Ubuntu matrix entry (Step 8.5). Run only when `--check` is set — useful if you want belt-and-suspenders on the local box before dispatching the full CI matrix.
### Step 7: Manual Gate Walk (trigger-based, even in default mode)
Read the fingerprint output from Step 1.
| Trigger | Manual check (gate) | What to walk |
|---------|---------------------|--------------|
| `split-newlines` | Gate 7 | Diff the changed file-reading code; verify any newline split uses `/\r?\n/` not `'\n'` |
| `homedir-tmpdir` | Gate 7 | Verify env-var literals use `os.homedir()` / `os.tmpdir()`, not raw `process.env.HOME` / `/tmp` |
| `bwrap-permissions` | Gate 7 | Spell bash steps that need network declare `permissionLevel: elevated` |
| `posix-only-spell-bash` | Gate 7 | Spell bash steps avoid `mkdir`/`rm`/`cp`; use `node -e` with `fs`/`os` for cross-platform file ops |
| `bin-scriptfiles-sync` | Gate 8 | New `bin/` script appears in all three `scriptFiles` arrays (`session-start-launcher.mjs`, `init/moflo-init.ts`, third sync site) |
| `helper-static-files` | Gate 8 | New helpers ship as static `bin/` files, not generated at runtime |
| `shipped-guidance-prefix` | Gate 8 | New shipped guidance has `moflo-` prefix and lives in `.claude/guidance/shipped/` |
| `files-glob-coverage` | Gate 9 | Run `npm pack --dry-run` and confirm new shipped files appear in the tarball listing |
| `info-loss-audit` | Gate 10 | For each deleted/renamed file, audit destinations bullet-by-bullet — every piece of removed content has a home |
If `CHECK_MODE=true`, walk **all** manual gates regardless of triggers (full audit). If `CHECK_MODE=false`, walk **only triggered** ones.
If the trigger set is `none` AND `CHECK_MODE=false`: skip Step 7 entirely.
### Step 8: Commit & Push
Commit the version bump files:
```bash
git add package.json package-lock.json src/cli/version.ts
git commit -m "chore: bump version to <new-version>"
git push origin main
```
Only commit version-related files. Do not stage unrelated changes.
+ ### Step 8.5: Dispatch release-smoke and block until green
+
+ ```bash
+ # Capture the SHA we just pushed — release-smoke runs against this exact commit.
+ PUBLISH_SHA=$(git rev-parse HEAD)
+
+ # Trigger the full 3-OS × 2-harness matrix. `sha` is a required workflow input
+ # so the dispatched run is unambiguously bound to this commit.
+ gh workflow run release-smoke.yml --ref main -f sha="$PUBLISH_SHA"
+
+ # Give GitHub a moment to register the dispatched run.
+ sleep 10
+
+ # Find the run we just dispatched by matching head_sha. This is robust against
+ # concurrent dispatches (e.g. a manual UI retry from another tab) — never use
+ # `--limit 1` alone, which would race against any unrelated in-flight run.
+ RUN_ID=$(gh run list --workflow=release-smoke.yml --branch main \
+ --json databaseId,headSha \
+ --jq ".[] | select(.headSha == \"$PUBLISH_SHA\") | .databaseId" \
+ | head -1)
+
+ if [ -z "$RUN_ID" ]; then
+ echo "ERROR: no release-smoke run found for SHA $PUBLISH_SHA. Wait a few seconds and retry, or check https://github.com/eric-cielo/moflo/actions/workflows/release-smoke.yml" >&2
+ exit 1
+ fi
+
+ # Block until the run completes. Exits non-zero on failure.
+ gh run watch "$RUN_ID" --exit-status
+ ```
+
+ `release-smoke.yml` runs full consumer + populated smoke on Ubuntu/macOS/Windows, plus file-sync smoke on all three filesystems. Per-PR CI is intentionally Ubuntu-only to keep recurring cost manageable — the full cross-platform matrix is paid once here, at publish time, against the exact commit we're about to publish.
+
+ **Wall time expectations**: typical run is ~15-20 min once caches are warm. **The first dispatch after `release-smoke.yml` lands will have cold caches on every leg** (no prior runs of this workflow exist to populate the `consumer-warm` and `fastembed` caches per-OS). Expect ~25-35 min on that first dispatch — the macOS leg is the long pole. Subsequent dispatches reuse the cache and settle to the typical ~15 min.
+
+ **If `gh run watch` exits non-zero**: stop immediately. Inspect the failed leg with `gh run view "$RUN_ID" --log-failed`, fix the underlying issue, push the fix, then re-run this step (no need to rerun Steps 0–8 — the bump commit is already on main, and the SHA-filtered lookup will pick up the new dispatched run against the same SHA only after `cancel-in-progress: false` lets the previous run drain). Do not proceed to `npm publish` until release-smoke is green for the SHA you intend to publish.
+
### Step 9: Verify npm Authentication
```bash
npm whoami
```
If 401 or "not logged in": auth token in `~/.npmrc` is missing or expired. Ask the user for a valid npm publish token.
**How to create a token** (provide these instructions to the user if needed):
1. Go to https://www.npmjs.com/settings/~/tokens
2. Click "Generate New Token" → select "Granular Access Token"
3. Set permissions: "Read and write" for the `moflo` package
4. Copy the token (starts with `npm_...`)
Once the user provides the token, write it to `~/.npmrc`:
```bash
echo "//registry.npmjs.org/:_authToken=<token>" > ~/.npmrc
```
Verify with `npm whoami` before proceeding.
### Step 10: Publish to npm
```bash
npm publish --tag <tag>
```
- For stable releases: `npm publish` (publishes to `latest` tag by default)
- For RC releases: `npm publish --tag rc` (publishes to `rc` tag so it doesn't become `latest`)
**OTP handling:**
- If npm returns an OTP/one-time-password error, ask the user for their authenticator code
- Then retry with: `npm publish --otp=<code> --tag <tag>`
- Tip: Granular access tokens created on npmjs.com do NOT require OTP — prefer those over legacy tokens
### Step 11: Verify Publication
```bash
npm view moflo version
npm view moflo dist-tags
```
Confirm the published version matches what we just built.
### Step 12: Install Locally
```bash
npm install moflo@<new-version> --save-dev
```
This updates `package.json` and `package-lock.json` to use the newly published version as a devDependency.
### Step 13: Final Commit
If `package.json` or `package-lock.json` changed from the install:
```bash
git add package.json package-lock.json
git commit -m "chore: install moflo@<new-version>"
git push origin main
```
The `chore: install moflo@<version>` commit message is the anchor the fingerprint uses to bound future diffs — keep the prefix exact.
## Output
Print a summary at the end:
```
Publish Summary
───────────────
Mode: default | --check
Version: <old> → <new>
Tag: latest | rc
Build: passed
Tests: skipped (CI-covered) | <N> files passed, <N> tests passed
Lint: skipped (CI-covered) | passed
Doctor: <N> passed, <N> warnings
- Smoke (clean): skipped (CI-covered) | passed
- Smoke (popl): skipped (CI-covered) | passed
+ Smoke (local): skipped (CI-covered) | passed
+ Release-smoke: green (run <id>, 3-OS × 2-harness)
Triggered gates: <list> | none
Published: moflo@<new-version>
Installed: moflo@<new-version> (devDependency)
```
## Important
- All commands run from the project root — never cd into subdirectories
- Never use `--force` on npm publish
- Never install moflo globally — it is always a local devDependency
- If any step that DID run fails, stop and fix the issue before proceeding — do not skip steps
- The `prepublishOnly` script in package.json runs `npm run build` automatically, so npm publish will fail-fast on any TypeScript or asset-bundling error even if Step 2 had been skipped (it isn't — Step 2 always runs)
- Pre-publish rules live in `.claude/guidance/internal/pre-publish-rules.md` — never duplicate or paraphrase them in this skill; reference and follow
## When to use `--check`
Use `--check` / `-ch` when:
- The PR didn't go through CI (e.g., publishing from a local-only branch)
- You're publishing a risky change (broad refactor, infrastructure surface, packaging changes)
- CI was red on something orthogonal you waived, and you want belt-and-suspenders locally
- You explicitly want to re-walk gates 7/8/10 manually regardless of trigger output
For the common case — publishing right after a merged green PR — the default mode is correct and meaningfully cheaper in tokens and time.
## See Also
- `.claude/skills/publish/fingerprint.sh` — Trigger-fingerprint script invoked by Step 1
- `.claude/guidance/internal/pre-publish-rules.md` — Authoritative gate ruleset (gates 1–10) and trigger map
- `docs/BUILD.md` — Step-by-step build/publish process this skill mirrors
- `.claude/guidance/internal/dogfooding.md` — Why we catch consumer-facing regressions first as our own dependency
- `harness/consumer-smoke/README.md` — Smoke harness profiles (clean + populated) that prove a consumer install works
- - `.github/workflows/ci.yml` — Lint/build/test gates this skill skips by default
- - `.github/workflows/consumer-install-smoke.yml` — Smoke gates this skill skips by default
+ - `.github/workflows/ci.yml` — Lint/build/test gates this skill skips by default (ubuntu-only)
+ - `.github/workflows/consumer-install-smoke.yml` — Per-PR ubuntu-only smoke
+ - `.github/workflows/file-sync-smoke.yml` — Per-PR ubuntu-only file-sync smoke
+ - `.github/workflows/release-smoke.yml` — Full 3-OS × 2-harness matrix this skill dispatches at Step 8.5