taw-fleetProduction, PRs and deploys

Production, pull requests and deploys

How taw-fleet reads your production sites through the TAW companion, pulls their content into Local, publishes Local content after a field-by-field review, writes through the REST API, merges pull requests and follows deploys, reads BugSmash comments, and sends a notification when something needs you.

taw-fleet reads each production site through the TAW companion, a small plugin that ships with every TAW theme. It also reads GitHub for pull requests and deploys, and BugSmash for the client's comments. Each part turns on with a setting for the site, in [sites.<folder>] of the settings file.

PartNeedsShows as
Production checksproduction_url, taw-fleet's signing key, a pinned site keyLIVE column, live, live.* findings
Content pullThe same, and companion 0.4+C, pull
Content publishThe same, companion 0.5+, taw/core 1.98+, "publish": true in the themep, x, publish, PUBLISHED section
REST writesproduction_url, a bot userwp-remote
Pull requests and deploysA GitHub tokenPR and DEPLOY columns, prs, M, merge
BugSmash commentsbugsmash_project, the BugSmash API keyFB column, F, X, comments
Monthly reportsAny of the above; best with taw-fleet watch runningm, report, REPORTS section
NotificationsAny of the above; taw-fleet watch installA macOS notification, watch status

Check production

taw-fleet live asks each production site's companion how the site is doing: WordPress, PHP, taw/core and companion versions, plugins with updates waiting, known vulnerabilities from the site's scanner (Defender or Wordfence), and recent taw/core log lines. In the dashboard, that's the LIVE column and the detail pane's LIVE section. doctor adds the live.* findings.

How it's secured

Every request is a signed GET to the companion's /wp-json/taw-hub/v1/ routes: health, inventory, vulnerabilities and logs, and content for a pull. Nothing is written on the site.

  • Requests carry taw-fleet's Ed25519 signature. The key lives in your macOS Keychain, never in a file. A site answers only when it trusts that key's public half.
  • Answers carry the site's own signature. taw-fleet checks it against the key you pinned for the site, like SSH's known_hosts. Pinned keys live in ~/.config/taw-fleet/companion-keys.json.
  • An answer from a site with no pinned key is shown but not trusted: ◐ in the LIVE column and a live.untrusted note. A signature that doesn't match the pinned key is a live.signature error.

Set it up

Add the production URL

[sites.awesome-company]
production_url = "https://awesomecompany.com"

Create taw-fleet's signing key

taw-fleet live key new
taw-fleet live key show

show prints the key id (taw-fleet) and the public key, never the secret. To use an existing key instead, pipe it into taw-fleet live key import --key-id <id>. Each Mac needs the key in its own Keychain.

live key new --force replaces the stored key. Every site stops trusting taw-fleet until it gets the new public key.

Give the sites the public key

Put the public key in the theme's composer.json, then deploy. See Install the companion.

Pin each site's key

taw-fleet live trust awesome-company

taw-fleet asks the site for the key it signs with, checks that the answer is signed by that key, shows it, and asks you to confirm (trust on first use). With a key you already have, pin it as given: --key <base64> --key-id <id>.

Check

taw-fleet live awesome-company --logs 20

The site shows ● and “verified”, with its versions, plugin updates, vulnerabilities and the last 20 log lines.

taw-fleet live always asks the sites now. The dashboard, doctor and show reuse an answer for 5 minutes; L in the dashboard asks again. Log lines are never cached.

When a site doesn't answer

MessageMeans
no companion on this site (404)The companion isn't installed or loaded
the companion has no public key configured (501)The site trusts no key yet
the site doesn't trust taw-fleet's signing key yet (401 unknown_key_id)The site has no key with taw-fleet's key id. Give it the public key from live key show
the site doesn't trust taw-fleet's signing key (401 invalid_signature)The site has another public key for that id
this Mac's clock is more than 60 s off (401)Fix the Mac's clock
needs a newer companionOne route (inventory, vulnerabilities, logs) is missing. Update the companion

Pull production content

C in the dashboard, or taw-fleet pull <site>, copies a production site's published content into its Local copy: posts and custom post type entries, TAW fields and options, terms, and the media they use. Drafts, users, comments and settings never come over.

It needs companion 0.4+ on production with taw/core's Content Interchange, and a Local theme with bin/taw and content:import.

Fetch

taw-fleet starts the Local site if it's stopped (it asks first), then fetches the snapshot from the companion's content route. It only accepts an answer signed by the site's pinned key, that is a content snapshot, and that says it comes from the production_url host. The snapshot is saved in ~/Library/Caches/taw-fleet/content/.

Preview

It runs php bin/taw content:import <file> --json in the theme folder: taw/core's dry run, which writes nothing. You see each record that would be created or changed, with its changed fields. If the Local site already matches, it stops there.

Import, after you confirm

It runs php bin/taw content:import <file> --json --yes --policy=update. Production wins, media is downloaded from production, and taw/core saves a rollback snapshot in wp-content/uploads/taw-private/ first. The summary shows what was created and updated, and the rollback snapshot's path.

To undo the last import, run taw content:import --undo in the theme folder to preview it, then again with --yes.

pull --yes skips both questions. Pull refuses the umbrella's own taw-theme and taw-gutenberg. A site with no pinned key is refused: run taw-fleet live trust <site> first.

After a successful import, taw-fleet keeps the snapshot as the site's base: production's content as Local last had it. Publishing compares with it to know what changed on each side.

Publish content to production

p in the dashboard, or taw-fleet publish <site>, sends what changed in the Local site's content to production: pages, posts and custom post type entries, TAW fields and options, terms, menus, and the media they use. Users, comments and site settings never go. You see every value that would change, field by field, and nothing is written until you press Publish.

Turn it on for a site

Publishing is off until the site's theme asks for it, through a reviewed commit and a deploy.

Update the theme

Production needs taw/core 1.98 or newer (u, then deploy), 1.99.1 or newer when pictures change, 1.100.0 or newer when featured images change, and the TAW Companion 0.5 or newer: "taw/hub-companion": "^0.5" in the theme's composer.json.

Switch publishing on

Add publish next to the fleet key in the theme's composer.json, commit, and deploy:

"extra": { "taw-companion": { "keys": { "taw-fleet": "…" }, "publish": true } }

Pin the key and pull once

Pin the site's key (taw-fleet live trust <site>), then pull (C) so taw-fleet has a base to compare with.

The details' PUBLISHED section says what's still missing. Once publishing is ready, it lists the last publishes, the newest with the path of its review.

How a publish goes

Compare

taw-fleet exports the Local site's content and compares it with the base. Only what changed in Local goes. With no base (no pull yet), every value production already has is marked for you to decide.

Ask production

Production answers what publishing would change, field by field, and which files it lacks. It writes nothing yet. The answer comes with a one-time token, good for an hour.

Review in your browser

A page opens, served by taw-fleet on your Mac only (127.0.0.1, behind a random address). It shows:

  • every record by its title, with a tick to publish it or leave it out;
  • every change by its field (Fieldset › Label), production's value beside Local's, with the words that changed marked; long content such as block markup, line by line;
  • images side by side;
  • pictures' details (alt text, title, caption, description) as records of their own, under Pictures and files. They're unticked: production keeps its own unless you tick one;
  • new files, each with the records that use it. A file goes up only with a ticked record that uses it, and the file count follows what you tick;
  • values changed on both sides first. Production changed them too since the base, so you pick a side for each: keep production's, or use Local's. A value only production changed keeps production's for you.

To decide shows only the values waiting for you. Publish stays off until each has a side. Cancel, or x in the dashboard (“stop waiting”), closes the review with nothing published.

Publish

The files that go are uploaded one by one, each checked against the plan. If production still needs a file taw-fleet didn't tie to a ticked record, taw-fleet sends it and tries once more, and says so in the result. Production plans the approved records again and refuses the whole publish if any changed there since your review: review again. Before writing, it saves a rollback copy and a journal. The page shows the progress, then the result with how to undo it.

After

taw-fleet refreshes the base from production and keeps the review as the record of what went out, in ~/TAW Fleet/sites/<site>/publish/<time>/: review.html (open it any time; it shows the sides you picked), plan.json, decisions.json and report.json.

Undo a publish

x in the site's details, or taw-fleet publish undo <site>, first says what undoing the last publish would do (“put back 4 values and delete 1 record it created”), then asks. A value goes back only where it's still what the publish wrote: an edit made on production since is kept and listed. A record the publish created is deleted unless it was edited since. --journal undoes an older publish; taw-fleet publish history <site> lists them.

Without a browser

taw-fleet publish acme --no-review                 # print the review
taw-fleet publish acme --no-review --json          # the review as JSON, with the record and change keys
taw-fleet publish acme --no-review --yes           # publish as reviewed; refuses while a value changed on both sides is undecided
taw-fleet publish acme --decisions choices.json --yes

choices.json names the records to publish and the side that wins each value changed on both sides (keep = production's, take = Local's):

{"include": ["post:page:about"], "sides": {"post:page:about": {"fields.hero_heading": "take"}}}

A picture's details go only when named, by their key: "include": ["media:2026/09/team.webp"].

Before taw/core 1.99.1, production wrote every picture's details and every new file in a publish, whatever you ticked. taw-fleet 1.31.1 refuses to publish pictures to an older taw/core: update it first (u, then merge and deploy).

Before taw/core 1.100.0, production looked a featured image up by its file name and could pick another picture of the same name. taw-fleet 1.34.1 refuses to publish featured images to an older taw/core.

In the chat, Claude publishes through the publish_content tool: it describes the changes, asks you about each value changed on both sides, then asks before publishing.

How publishing is secured

  • Every request is signed with the fleet key, and every answer is checked against the site's pinned key.
  • The one-time token works once, for one plan. Production writes only the records you approved, as you reviewed them.
  • Production accepts only the files the plan named, matched by their sha256. It never downloads anything from your Mac.
  • Limits: 32 MB per file, 512 MB and 500 files per publish, one publish at a time.
  • Every step is logged on production (content.publish.*; taw-fleet live <site> --logs 20 reads them).
  • No database copy is made or sent. The base is a content snapshot, kept in ~/Library/Application Support/taw-fleet/content/, outside every site, theme and git repository.

Write to production

Site skills that change the live site, like resolve-comments, write through taw-fleet wp-remote. It calls the site's WordPress REST API as a bot user whose Application Password stays in your Keychain, so it never appears in a Claude transcript or a file. Setting up the bot is on Site skills.

taw-fleet wp-remote awesome-company GET '/wp/v2/pages?slug=nosotros&context=edit'
taw-fleet wp-remote awesome-company POST /wp/v2/pages/12 --data @meta.json
taw-fleet wp-remote awesome-company POST /wp/v2/media --file hero.webp
  • Methods: GET, POST, PUT and PATCH. There is no DELETE: taw-fleet never deletes on production.
  • Body: --data takes inline JSON, @file, or - for stdin. --file uploads a file as the body, with its type from the extension or --type. The two can't go together.
  • URL: each request uses the ?rest_route= form, because some hosts' firewalls block /wp-json/wp/v2/users. It uses HTTP/1.1, and retries an empty, unreadable or server-error answer up to twice, a second apart.
  • Output: the JSON answer goes to stdout. A WordPress error exits non-zero with its code and message.
  • Credentials: wp-remote key import <site> reads user:application password from stdin into the Keychain item wp-<folder>. wp-remote key show <site> says which bot is stored and checks it against the site, showing its name and role.

Pull requests and deploys

With a GitHub token, the dashboard shows each theme's open pull requests and its deploy. taw-fleet uses GITHUB_TOKEN or GH_TOKEN, else gh auth token. Without a token, the PR and DEPLOY cells show ?. Run gh auth login.

The repository is the one behind the theme's delivery remote: origin, or the remote the theme's taw.json names in update.remote (taw/core 1.92.0+), for a site whose production deploys from another organization. It's the same remote the update pushes to and opens its pull request on.

  • Pull requests: up to 20 open ones per repository, with their CI result and whether they conflict.
  • Deploy: the repository's active workflow whose file name starts with deploy. Production “has” the commit of its newest successful run. A skipped or cancelled run isn't a failure.
ColumnSymbolMeans
PR2✓ 2… 2✗ 2! 2◇Two open: CI passed (or none), CI running, CI failed, a conflict, drafts
DEPLOY✓ ↑2 ⟳ ✗Production is current, two commits not deployed, a deploy or CI runs, the deploy or CI failed

The columns refresh every 5 minutes, every 15 seconds while a deploy runs, and right after an update opens a pull request. L refreshes them now. taw-fleet prs shows the same, with the commits not deployed yet. doctor adds pr.ready, pr.failing, pr.conflict, deploy.failed, deploy.blocked and deploy.pending.

Merge a pull request

M in the dashboard, or taw-fleet merge <site>, merges a theme's pull request. In the dashboard, several sites can merge at once, also while an update runs: each merge runs on its own, and a site that's still merging refuses a second M.

  1. Pick. With several open, the dashboard asks which, by number. On the command line, use --pr <number>.

  2. Refusals. A draft, a conflict, CI that failed, and CI that still runs are refused, each with the reason.

  3. Ask. The question names what the merge does: “Merge #12 “Hero copy” into main? CI passed. This deploys https://awesomecompany.com (production).” When taw.json delivers to another remote, it adds: “Then origin (the copy) gets main too, when it can follow.”

  4. Merge. taw-fleet uses the repository's default merge method for you, and only if the branch is still at the commit taw-fleet read. If someone pushed since, it stops: refresh with L and look again.

  5. Clean up. It deletes the branch on GitHub, when the branch is in the same repository. If the local theme has no changes to tracked files and is on that branch or the default one, it switches to the default branch, pulls it (from the delivery remote, when taw.json names one), and deletes the local branch. Untracked files, such as the update's .taw/ report, don't stop it. Otherwise it leaves the theme alone and says why.

    After that pull, when the delivery remote isn't origin, it runs git push origin <default>:<default>, a fast-forward only, so origin stays a copy of the merge. If origin's default branch has commits of its own, it's left alone, and the summary says to realign it once (git push --force-with-lease origin <default>, after checking it has nothing the other repository lacks).

  6. Follow the deploy. The dashboard checks GitHub every 15 seconds for up to 30 minutes. It says when production has the merge and how long it took, or that the deploy or CI failed, with the run's link. Then it checks the live site again.

  7. Remind you about the page cache. While the deploy runs, the MERGE section says to clear the page cache once production has the merge, not before. Once it does, it says to clear it now (Hummingbird, WP Rocket, the host's cache). A cache cleared during the deploy is rebuilt from the old code, and the site keeps the old pages until it's cleared again. taw-fleet doesn't clear it for you.

While a site merges, its card says so on the theme line: a spinner with “merging #8…”, then “deploying…”. Afterwards the card keeps ✓ or ✗ for 15 minutes. The detail pane opens with a MERGE section that lists each step: merged (commit and time), this Mac back on the site's branch, deploying to the production host (elapsed time), then on production, with the time after the merge, or what failed. The bottom line lists every merge in progress.

BugSmash comments

Clients review their live site in BugSmash and pin comments to the page. taw-fleet shows them per site.

Store the API key

Copy the key from BugSmash → Settings → API Key, then:

pbpaste | taw-fleet comments key import
taw-fleet comments key show

show says where the key comes from and whether BugSmash accepts it. BUGSMASH_API_KEY is used when the Keychain has none.

Link each site to its project

taw-fleet comments projects

Put the site's project id in the settings:

[sites.awesome-company]
bugsmash_project = "00000000-0000-4000-8000-000000000000"
  • FB column: how many comments are open, in the warning color once the oldest has waited more than two days. The second line shows the oldest's age.
  • Detail pane: the FEEDBACK section quotes the three latest comments with their number, page, author and age.
  • doctor: comments.open, comments.unreachable and comments.no-key.
  • F: opens the site's review page in BugSmash.
  • X: opens Claude Code beside the dashboard, in the theme folder, with the theme's resolve-comments skill. Its first message has the open comments, the project, the review page and the production URL, and asks it to show its plan first, ask before anything reaches production, and resolve a comment only once its change is live. X needs open comments and a theme with the skill (taw/core 1.89+). When Claude Code exits, the dashboard checks BugSmash again.
  • taw-fleet comments: the open comments of every site, or one (--json for scripts).

taw-fleet only reads BugSmash, and caches each answer for 5 minutes (L asks again). Replying and resolving happen in the skill: see Site skills.

Monthly reports

m in the dashboard, or taw-fleet report <site>, makes a site's monthly report for its client. It's for you to read and send: taw-fleet never sends it.

SectionWhere it comes from
AvailabilityNotifications' checks every 5 minutes: the share that found the site answering
Updates and changesThe theme's pull requests merged that month. Framework and maintenance work is counted in one line, such as “12 updates; the site now runs TAW core 1.93.1”. The site's own changes are listed, with how often the live site was published
Review commentsBugSmash: comments received that month, how many are resolved, how many are still open
SpeedGoogle PageSpeed Insights, mobile and desktop, measured when the report is made, plus real visitors' Core Web Vitals when Google has enough traffic
Security and maintenanceThe live site through its companion: WordPress, PHP and taw/core versions, plugin updates waiting, known vulnerabilities

A section taw-fleet couldn't measure says “Not available for this month” in the report. The terminal and --json say why.

Set the name and the language

report_from = "Marco Lizardo"   # "Prepared by …"; leave it out for no name
report_language = "en"          # or "es"

[sites.awesome-company]
report_language = "es"          # this client's reports in Spanish

Store a PageSpeed key

Without a key, PageSpeed Insights shares a small quota with everyone and often refuses. Create a free one in the Google Cloud console: enable the PageSpeed Insights API, then Credentials → Create credentials → API key. Then:

pbpaste | taw-fleet report key import

It's kept in the Keychain. $PAGESPEED_API_KEY works too.

Make one

Press m on a site and pick this month so far or last month. The report opens in your browser and is saved as Markdown (to paste into an email) and HTML (one page to attach or print) in ~/TAW Fleet/reports/<site>/<month>. The site's details list them in a REPORTS section.

On the 1st of each month (up to the 3rd, in case your Mac was asleep), taw-fleet watch makes last month's report for every production site it was watching, and a notification tells you it's ready.

taw-fleet report awesome-company --month 2026-09 --open
taw-fleet report --all --month 2026-09   # every production site
taw-fleet report awesome-company --print        # the Markdown only, nothing saved

Notifications

taw-fleet watch tells you when something needs you, even with the dashboard closed. It checks the fleet every 5 minutes and sends a macOS notification for each change, once.

Turn it on

taw-fleet watch install

A launchd agent runs the check in the background, also after a restart. The first check only notes the fleet as it is, so nothing already failing is told.

Check that notifications arrive

taw-fleet watch test awesome-company

Clicking the notification opens the dashboard on that site, its details showing. The first time, macOS asks whether terminal-notifier may show notifications: allow it, with the Banners style.

NotificationWhen
Deploy failedThe theme's deploy workflow failed. Production keeps the last good deploy
CI failed on mainThe default branch's newest commit didn't pass its checks
Production is downThe site's companion didn't answer two checks in a row (one miss is often a blip)
Production is backIt answers again, and says how long it was down
Production not verifiedIt answered without its pinned key's signature
New commentsNew client comments in the site's BugSmash project, quoting the newest
Update ready to mergeAn update's pull request passed its checks: M merges it
Report readyOn the 1st, last month's report for a production site was made
  • Told once: each failed run, failing commit, new comment or ready pull request is told the first time it's seen. A check that can't reach GitHub, a site or BugSmash keeps what the last one saw, so old news doesn't come back after an outage.
  • Clicking: opens the dashboard on the site through terminal-notifier, which the Homebrew cask installs. Without it, or with its notifications turned off, the notification still comes, through Script Editor, and clicking it doesn't open the dashboard.
  • Uptime: each production site's checks are kept per day, for its monthly report.
CommandDoes
taw-fleet watch statusOn or off, the last check, what it can't check, and what it told lately
taw-fleet watch --onceOne check here (--json, --no-notify)
taw-fleet watchA check every 5 minutes in this terminal, until ctrl+c
taw-fleet watch uninstallStop checking in the background