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.
| Part | Needs | Shows as |
|---|---|---|
| Production checks | production_url, taw-fleet's signing key, a pinned site key | LIVE column, live, live.* findings |
| Content pull | The same, and companion 0.4+ | C, pull |
| Content publish | The same, companion 0.5+, taw/core 1.98+, "publish": true in the theme | p, x, publish, PUBLISHED section |
| REST writes | production_url, a bot user | wp-remote |
| Pull requests and deploys | A GitHub token | PR and DEPLOY columns, prs, M, merge |
| BugSmash comments | bugsmash_project, the BugSmash API key | FB column, F, X, comments |
| Monthly reports | Any of the above; best with taw-fleet watch running | m, report, REPORTS section |
| Notifications | Any of the above; taw-fleet watch install | A 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 alive.untrustednote. A signature that doesn't match the pinned key is alive.signatureerror.
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
| Message | Means |
|---|---|
| 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 companion | One 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 20reads 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,PUTandPATCH. There is noDELETE: taw-fleet never deletes on production. - Body:
--datatakes inline JSON,@file, or-for stdin.--fileuploads 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>readsuser:application passwordfrom stdin into the Keychain itemwp-<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.
| Column | Symbol | Means |
|---|---|---|
| PR | 2✓ 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.
-
Pick. With several open, the dashboard asks which, by number. On the command line, use
--pr <number>. -
Refusals. A draft, a conflict, CI that failed, and CI that still runs are refused, each with the reason.
-
Ask. The question names what the merge does: “Merge #12 “Hero copy” into main? CI passed. This deploys https://awesomecompany.com (production).” When
taw.jsondelivers to another remote, it adds: “Then origin (the copy) gets main too, when it can follow.” -
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
Land look again. -
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.jsonnames 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 runsgit push origin <default>:<default>, a fast-forward only, sooriginstays a copy of the merge. Iforigin'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). -
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.
-
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.unreachableandcomments.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'sresolve-commentsskill. 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.Xneeds 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 (--jsonfor 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.
| Section | Where it comes from |
|---|---|
| Availability | Notifications' checks every 5 minutes: the share that found the site answering |
| Updates and changes | The 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 comments | BugSmash: comments received that month, how many are resolved, how many are still open |
| Speed | Google PageSpeed Insights, mobile and desktop, measured when the report is made, plus real visitors' Core Web Vitals when Google has enough traffic |
| Security and maintenance | The 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.
| Notification | When |
|---|---|
| Deploy failed | The theme's deploy workflow failed. Production keeps the last good deploy |
| CI failed on main | The default branch's newest commit didn't pass its checks |
| Production is down | The site's companion didn't answer two checks in a row (one miss is often a blip) |
| Production is back | It answers again, and says how long it was down |
| Production not verified | It answered without its pinned key's signature |
| New comments | New client comments in the site's BugSmash project, quoting the newest |
| Update ready to merge | An update's pull request passed its checks: M merges it |
| Report ready | On 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.
| Command | Does |
|---|---|
taw-fleet watch status | On or off, the last check, what it can't check, and what it told lately |
taw-fleet watch --once | One check here (--json, --no-notify) |
taw-fleet watch | A check every 5 minutes in this terminal, until ctrl+c |
taw-fleet watch uninstall | Stop checking in the background |