Skip to content

Releases: Kntnt/kntnt-wp-skills

v0.10.0

Choose a tag to compare

@TBarregren TBarregren released this 16 Aug 20:04

The client half of a coordinated release with Kntnt Extractor 0.6.0. The verified Extractor API-version ceiling rises from 6 to 7, which is the act of declaring this client correct against that build — so install the Extractor on production first, then this, and do neither while an extraction is in flight.

Three things carry the release. A define whose value the Extractor withholds is no longer ported: null on the wire is a masking value, and writing define('NAME', null) locally makes defined('NAME') true and silently suppresses whatever fallback the owning plugin runs for "not configured". This was latent until the Extractor replaced its secret deny-list with an allow-list, at which point a masked third-party key would have reached the writer. The poll loop moved out of prose and into scripts/poll_extraction.py — one blocking invocation, one terminal verdict — instead of being rewritten by the agent from eight pinned literals on every run. And the main extraction lost its overall wall-clock budget: a 3600 s cap expired on a healthy, visibly-advancing run that then had to be re-polled by hand, and the stall window is the check that actually distinguishes dead from slow.

The run report is also more honest about what it cannot do: it now states the Extractor API version production reported and, when that is below this client's ceiling, names each behaviour degraded on that host — three at the time of writing. The remedy for all of them is upgrading production's Extractor, never a client-side workaround.

A limit worth stating: raising the ceiling to 7 was verified by source inspection from three angles, not by unsealing a version-7 container. No such container existed when the verification was done. The record of exactly what was and was not checked is in plans/004-raise-the-verified-api-version-ceiling-to-7.md, now tracked in the repository rather than living only on the maintainer's machine.

Added

  • The canonical discovery document now carries the Extractor API version the health check observed, as a required top-level api_version sibling of environment — plumbing that previously stopped at the health check itself, discarded the moment the comparison against the floor and ceiling was made. agents/discovery-classify.md passes the number through verbatim rather than re-fetching it, and both skills' run report now states the version production reported and, when it is below this client's ceiling, names each behaviour that is consequently degraded on this host: the identity report's absence below API version 4, the absent progress.chunks_done and widened stall window below API version 6, and strict being silently ignored by any Extractor that predates it. The report was previously silent on all three even though production runs API version 5 today. This plumbing changes no decision — the version pin's floor and ceiling are unchanged — it only makes an existing fact reportable.

Changed

  • Two-phase discovery's "the bootstrap is small" claim is now written down as a premise that holds only where wp_postmeta is small, so a bloated table is a known case rather than a surprise. A skip-wp_postmeta fallback was considered and is not implemented: 493 MB of table versus 279 MB of thumbnails compared InnoDB allocated size to files, while the whole bootstrap container measured 30 MB — a loss on transferred bytes, with only server-side cost remaining (ADR-0017).
  • Both skills now state that a fast preflight is not a promise about the main extraction. The two-table probe proves the loopback and continuation path; a pass in a few seconds says nothing about how a large table or a large file will behave.
  • The poll loop is now scripts/poll_extraction.py: one blocking invocation, one terminal verdict, the seven discipline literals in code so an agent cannot re-derive the cadence, timeouts, backoff, confirmed-vanished check, stall window, or the preflight and bootstrap budgets from prose. The Application Password is KNTNT_EXTRACTOR_APP_PASSWORD in that one process's environment — never argv, never printed. The poll-owning agents and both skills invoke the helper; the consistency suites bind the pinned phrases to the script's constants.
  • The main extraction has no overall wall-clock budget. A 3600 s cap expired on a healthy, visibly-advancing many-file job at less than a quarter of the file phase; sealing cost is per file, not per byte, so a constant cannot be right. The helper now omits the budget argv on that loop and stops only on the stall window — the check that distinguishes dead from slow. Preflight and bootstrap keep their 10- and 15-minute budgets.
  • The main extraction is submitted with strict: false, so a file that vanished between the GET /files walk and the POST is skipped and reported rather than failing the whole job. extract-transfer sends the member, surfaces any skipped_files the create or poll returns, and unseals against the remaining file list — the container only holds what the plugin packaged. A missing table is still a hard 404; the error data now names every missing table and every missing file, so the recovery is no longer a full re-walk. strict defaults to today's hard fail on an older Extractor that ignores the member, and the new fields are additive, so the verified API-version ceiling stays ≤ 6.
  • The verified Extractor API-version ceiling is raised from 6 to 7. The Extractor's GET /environment define-disclosure allow-list is the change that moved api_version, and source inspection of the Extractor's own ADRs, its normative docs/container-format.md, and its artifact-writing commit history since v0.5.1 found no change to the sealed container's byte layout, segment framing, segments-per-resource, sealed index, or reassembly order — the bump is a deliberate compatibility interlock (Extractor ADR-0018), not a shape claim, and scripts/unseal.py needed no change. See ADR-0021.

Fixed

  • A define whose value the Extractor withheld (null on the wire from GET /environment) is no longer ported. scripts/classify.py now classifies such a define auto-excluded under a new withheld class instead of offering it at the wp_config_defines gate, so scripts/wpconfig_block.py never writes define('NAME', null); into the local wp-config.php — a define php -l accepts and the smoke test never catches, but which makes defined('NAME') report true and suppresses whatever fallback the plugin runs for "not configured". This was latent until now: every name the Extractor currently masks is also routed to an auto-excluded class by name, but the Extractor is replacing its secret deny-list with an allow-list, after which a masked plugin define (a third-party API key, say) would have reached the gate and the writer with a null value. Both clone and pull now name every withheld define in the run report, so the operator learns which values did not come down instead of finding out later that a plugin is silently unconfigured. scripts/wpconfig_block.py also gained its own rejection of a None define value as defence in depth, for a caller outside the normal classifier-to-writer path. See ADR-0020.
  • The resolved exclusion set left cache-plugin trees, backup-tool working directories, and the Extractor's own uploads staging in scope. On a live site that meant LiteSpeed's hashed CSS, BackWPup restore logs, and a previous failed job's .sealed.building were selected for clone — and the last of those can vanish mid-run because POST /extractions reclaims the directory the selection just named. ALWAYS_EXCLUDED now covers the known cache-plugin and backup-tool trees (wp-content/litespeed, wp-content/et-cache, wp-content/w3tc-*, wp-content/uploads/backwpup*) and the plugin's own three uploads directories as a self-reference, not a cache. The names that live inside the uploads directory — the backup tool's scratch and the plugin's three — are re-anchored on the classifications' uploads_prefix, so a site that moved its uploads directory (a non-default WP_CONTENT_DIR, an UPLOADS define) excludes them where they actually are and not only at the standard location, which is the layout classify.py already honours everywhere else. Glob-bearing directory prefixes match in filter_manifest.py and baseline_diff.py the same way, so a suffixed live directory is dropped and a sibling that does not fit the glob is kept; a consistency suite now pins those two matchers as one implementation, since a disagreement between them is exactly the deletion-diff poisoning issue #35 closed at the assembly end. The assembler is still the one source both the selection and the baseline consume.
  • A healthy job against an API-version-5 Extractor could abort as a false stall: below API version 6, progress.chunks_done does not exist, so stall detection fell back to the two coarse counters — but those move only when a whole table or a whole file finishes, and a 186-table site working through one large table stood still for minutes at a time on the unconditional 10-minute stall window. scripts/poll_extraction.py now widens the stall window to 40 minutes from the first poll that observes chunks_done absent, says so in the run's output, and reports the correct give-up minutes for whichever window actually fired. See ADR-0018.
  • Both poll-owning subagents shaped a FAILED verdict specifically so the orchestrator could clean up the still-active job afterwards, and the orchestrator was never told to do it: an unseal failure after a complete download, and an exhausted stall window, both left the production artifact or the active job in place on a ...
Read more

v0.9.0

Choose a tag to compare

@TBarregren TBarregren released this 14 Aug 13:27

The poll discipline now has one place where its wording is decided. It used to be pinned inside a test file — a poor home for a product decision — and the rules stated only in prose drifted apart, as two agents proved by coming to state the same ban in different words.

Added

  • docs/poll-discipline.md is the poll discipline's canonical statement, and the two consistency suites now read their pinned phrases from it instead of restating them in Python. The literals had been living inside tests/test_poll_discipline_consistency.py, which made a test file the source of truth for a product decision, and every rule stated only in prose was free to drift — as it did: the two poll-owning agents came to state the same new ban in different words, and the binding meant to catch that had to be loosened to accept both. Changing a rule is now one edit in the document plus the matching edit in each surface, and the suites refuse the change until every surface has followed. The surfaces still restate the discipline in full, because an agent definition loaded standalone must carry the whole rule set; what the document adds is one place where the wording is decided, and a pointer to it from each surface. A guard test pins the expected phrase names, so a phrase vanishing from the document reddens rather than quietly disabling its own enforcement.

Changed

  • agents/extract-transfer.md's two poll hard rules are split apart and carry the canonical wording verbatim, matching agents/discovery-classify.md word for word rather than merely in substance.

Full changelog: https://github.com/Kntnt/kntnt-wp-skills/blob/v0.9.0/CHANGELOG.md

v0.8.0

Choose a tag to compare

@TBarregren TBarregren released this 14 Aug 12:31

Two silent failures, one loud one, and the tripwire that would have caught all three. A form add-on the service registry did not know dropped a mandatory risk warning without a word; the contract between this client and the Extractor's artifact had no test binding it; and a poll agent that could not sit still returned three times without a verdict.

Added

  • The Extractor version pin gains a verified ceiling alongside its floor: the skills are checked against API version ≤ 6, and a GET /status reporting more stops for the operator (aborting under --yes) rather than proceeding on an artifact contract this client has never been tested against. A floor alone cannot catch the hazard that actually occurred: API version 5 changed a table from exactly one sealed segment into one or more, and an un-updated client kept only each table's final slice — every other slice lost, with no error raised on either side and both repositories' suites green throughout. tests/test_api_version_ceiling_consistency.py binds the ceiling across every surface that states the pin, so raising it is a deliberate act of verification rather than an omission.

Changed

  • scripts/classify.py no longer stays silent about a form plugin's add-on it does not recognise. A slug carrying a recognised form-plugin prefix now earns one of three outcomes: a finding naming the service, silence when the remainder is a known non-service add-on (a paid tier, a PDF or signature feature), or an unidentified-service finding naming the add-on for the operator to check. safeteam.se runs ws-form-fluentcrm, which matched the ws-form prefix and no service suffix, so the mandatory form-to-service bullet in the risk warning — the one check standing between a local form submission and a live CRM — was dropped without a word (ADR-0009). The two registries now lag in opposite directions on purpose: extending the service registry improves the wording, extending the non-service registry removes noise, and an add-on in neither is reported rather than assumed harmless. The service registry also gained FluentCRM and eighteen other engines, and matching is on whole hyphen-delimited tokens, so ws-form-mailchimp-pro names Mailchimp and a remainder merely ending in a service's letters does not.
  • Both clone and pull list an unidentified-service finding exactly like a named one — an unrecognised service is one the operator has to check, not one to leave out.
  • The poll discipline's stall rule counts progress.chunks_done as an advance, alongside the state change and the two coarse counters it already watched (ADR-0018). Those two move only when a whole table or a whole file finishes, so a job slicing one large table reported 3/186 unchanged for minutes while perfectly healthy — indistinguishable from a wedged job, and worked around by widening the 10-minute stall window to 2400 s, which is how a genuinely dead job comes to take forty minutes to notice. Kntnt Extractor 0.5.0 adds chunks_done, which moves on every packaging chunk; the stall window therefore stays at 10 minutes. Against an Extractor below API version 6 the field is absent and the rule degrades to its previous form, with the fallback stated in the run's output rather than an absent field read as a stall.
  • agents/discovery-classify.md and agents/extract-transfer.md state that a poll loop is one blocking, self-terminating shell invocation, never one tool call per poll, and that the agent returns exactly once, with a verdict. discovery-classify returned three times mid-bootstrap saying it was still waiting — ~55k tokens each, no evidence block, every piece of real work already done — because its contract gave it no way to wait out a job taking minutes and no way to resume polling after a return. An exhausted budget is now a FAILED carrying the job id and last counters, so the orchestrator can consume or cancel the still-active job instead of leaving one wedged against the plugin's one-active-job rule.
  • discovery-classify creates its own working directory under the run's scratchpad and may name only artifacts it wrote there in its evidence block. In the same run it reported artifacts the orchestrator had produced, with matching SHA256s, while its own consume returned 404 because the orchestrator had already consumed the job — an evidence block is only evidence when nothing else could have written the files it describes.

Fixed

  • The container-format contract in docs/implementation-notes.md stated that slices concatenate to the whole-table dump byte for byte because the plugin cuts them on extended-INSERT boundaries. From Extractor API version 6 a slice is bounded by bytes as well as rows and a byte-bounded cut lands on the row that fills the budget, so the statement grouping differs from an unsliced dump's. The contract now states the property that actually holds and that reassembly relies on — every slice ends on a complete statement — and says explicitly that byte-identity is not among them. tests/test_unseal.py pins the uneven-statement shape that a byte budget produces.

Full changelog: https://github.com/Kntnt/kntnt-wp-skills/blob/v0.8.0/CHANGELOG.md

v0.7.0

Choose a tag to compare

@TBarregren TBarregren released this 13 Aug 17:34

Added

  • scripts/unseal.py reads a container in which a table arrives as one or more consecutive segments sharing its name, concatenating them in index order into the reassembled dump. Kntnt Extractor 0.4.0 packages a large table in bounded row slices across as many ticks as it takes — one sealed segment per slice — because a table exceeding the host's memory_limit or max_execution_time was previously killed mid-tick and retried forever, making the site unclonable; the reader's positional check assumed exactly one segment per table and refused such a container outright. The newline terminator is applied once per table rather than once per segment, so a sliced dump is byte-identical to an unsliced one. The old one-segment shape is the one-slice case of the same rule and still reads, so the Extractor API floor stays at ≥ 2 and a client works against an un-upgraded production install. Validation stays strict: a structure-only table is still required to be exactly one segment, and a container that does not match the requested selection is still refused rather than reassembled into the wrong thing.

Fixed

  • The control channel no longer mistakes a failed authentication, or a cached error response, for a missing capability. Three changes, one root cause: an authentication attempt with a wrong username is treated as anonymous by WordPress — wp_authenticate_application_password() short-circuits silently on an unknown login and returns no error of its own — so the caller sees only rest_not_logged_in, and on a site behind a page cache that anonymous-looking refusal was cached and replayed to every later call, correct credentials included. (1) Every Extractor request now carries a unique _cb cache-buster (&-appended in the paged GET /files cursor loop, which already has a query string), so no intermediary can answer a call from a stored response. (2) The health check's step 2 establishes who is authenticated — from a credentialed GET /status's authenticated_as and capabilities on Extractor API version ≥ 4, and otherwise from a same-username/wrong-password probe whose incorrect_password proves the Authorization header arrives — before it interprets any refusal; the old inference from a 403 on /environment beside a 200 on /audit-log was unsound, since a cached refusal and a wrong username both mimic it exactly. (3) An authenticated response carrying x-litespeed-cache: hit, cf-cache-status: HIT, x-cache: HIT, x-proxy-cache: HIT, or a non-zero age: now raises a loud, specific diagnostic and aborts, instead of being read as a genuine answer. docs/implementation-notes.md gains a symptom-to-cause troubleshooting table for the three indistinguishable causes.
  • The Keychain account convention <wp-user>@<host> is now documented as splitting on the last @, never the first. The WordPress user_login is frequently an email address in its own right — thomas@kntnt.com@safeteam.se is a real, valid account name — so a first-@ split produces the login thomas, which does not exist, and authenticates as nobody without reporting any error. The documentation now states explicitly that the first component is the user_login (often an email), where an operator finds theirs (Users → Profile → Username), and that a credentialed GET /status reports it back as authenticated_as.

v0.6.0

Choose a tag to compare

@TBarregren TBarregren released this 24 Jul 07:09

Hardening from the second live smoke test: a heavy-blob heuristic that now sees stray directories anywhere in the install, a default-empty privacy gate for CRM/mass-mailer subscriber tables, and a deterministic wp-config.php marked-block writer — plus a batch of contract and documentation fixes across the transfer engine.

Added

  • A new deterministic helper, scripts/wpconfig_block.py, now owns the wp-config.php marked-block write that clone/pull §9.4 previously prescribed as hand surgery. It takes the current config text, the resolved portable defines, the table prefix, and the cron decision, and returns the new full text with the skills' marked block written (delimited by // BEGIN kntnt-wp-skills / // END kntnt-wp-skills, replaced in place when present or inserted above the /* That's all, stop editing! line when absent) and every scaffold collision it supersedes removed. The collision set is computed — the portable defines plus DISABLE_WP_CRON intersected with whatever the scaffold actually shipped — never a hard-coded name list: the second live smoke test's scaffold carried five colliding defines (DISABLE_WP_CRON, EMPTY_TRASH_DAYS, WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY) where the SKILL prose named two, and a repeated define() on the same constant fatals. Both clone/pull §9.4 now invoke the helper and lint the result with ddev exec php -l wp-config.php instead of describing the edit by hand. The helper fails loud at the boundary on the malformed-input classes that would emit a duplicate define() on one constant — a runtime fatal php -l cannot catch: a name repeated within the input defines, DISABLE_WP_CRON smuggled into the defines (its write is solely the cron field's job), a define name outside the PHP constant-identifier shape (closing a PHP-injection path through a quote-bearing name from production's config), and a non-finite float value (NaN/Infinity, which json.loads accepts and would render as a bare undefined-constant fetch). (#42).
  • A recognised CRM/mass-mailer's subscriber tables now get their own carry/empty privacy gate, default empty — a sibling to the user_submissions gate and a sixth classification family. scripts/classify.py tags FluentCRM, MailPoet, The Newsletter Plugin, and Mailster subscriber/address stores (fc_subscribers and its family, fc_campaign_emails, fc_campaign_url_metrics, fc_funnel_subscribers, fc_funnel_metrics, mailpoet_subscriber*, mailster_subscriber*, mailster_queue, The Newsletter Plugin's exact-matched newsletter table plus newsletter_sent*/newsletter_stats*/newsletter_user*) as crm_subscribers, while the engine's definitions (campaigns, funnels, lists, tags, terms, meta, url stores, and Newsletter's newsletter_emails campaign bodies) carry in full as site config. Every engine in bootstrap_parse.py's MAILER_ENGINES recognition registry (FluentCRM / MailPoet / The Newsletter Plugin) now has a gated subscriber store — pinned by a drift test — so a recognised mailer's addresses can never carry in full; Mailster is gated in addition, making the gated set a superset of the recognised engines. scripts/resolve_plan.py adds the crm_subscribers decision and folds each privacy gate's resolved carry independently into db_table_content, so one gate's carry never disturbs the other's tables. Closes the gap the second live smoke test exposed, where 584 real FluentCRM subscribers transferred in full and standing funnel automations could mail them from a dev copy under the mail=live + cron-runs defaults — a risk the mass-send valve does not catch (ADR-0019, #39).

Changed

  • The Application Password's home is now a documented convention instead of living only in the operator's head: macOS Keychain, service kntnt-extractor-app-password, account <wp-user>@<host>, retrieved with security find-generic-password -s ... -a ... -w inside a subshell, with the KNTNT_EXTRACTOR_APP_PASSWORD environment variable as the portable non-macOS fallback. The health check now resolves and verifies the credential itself (§1) and fails with the exact remediation ("create a Keychain item: service ..., account ...") when it is absent, instead of presuming "the configured" password exists. The three subagent task envelopes (discovery-classify, extract-transfer, manifest-baseline-diff) now take a credential reference — a Keychain service+account or an env-var name — resolved by the calling code inside each authenticated call's own subshell, so the secret's value never transits the orchestrating agent's own context (#44).

Fixed

  • The heavy-blob heuristic is no longer uploads-scoped: a heavy stray directory anywhere else — the 7.6 GB install-root 2026/ directory the second live smoke test found, which the heavy_blobs=exclude default could not reach and would transfer silently — is now flagged too. scripts/discovery.py adds a root and a content breakdown beside the existing uploads one, each summing its top-level subdirectories (loose files are not directories and do not count), and scripts/classify.py runs a deliberately simpler floor-only rule over them: any non-standard directory at or above the 1 GiB floor is flagged, no median-outlier test, since at these levels the peer population is small and dominated by standard directories. The standard sets are derived from the document's own content and uploads locations, so a non-default content dir or uploads dir is honoured and its payload never flagged; when the content path is absent the default wp-content segment is assumed rather than exposing the payload tree. All flags merge into the single blobs.flagged list, so the existing heavy_blobs gate and scripts/build_exclusions.py keep the newly flagged directory out of the transfer by default and give the operator a lever over it, with no gate or exclusion-assembler change. A pre-existing document without the new sections yields exactly today's uploads flags (#38).

  • scripts/unseal.py's seal mode (the development/test round-trip aid) still reached into each segment with bare segment["name"] / segment["data"] indexing, so a malformed segment raised an uncaught KeyError traceback instead of the clean unseal.py: missing required config key: 'name' diagnostic the rest of the helper emits — a residue of #47, which deliberately narrowed main()'s except to stop masking stray KeyErrors but left these two accesses unguarded. Both per-segment reads now go through the same _required accessor, so a missing key fails loud and uniform like every other required-key check. seal is off the clone/pull hot path — the plugin seals in production — so the impact was confined to the test aid (#47).

  • Clone §3 accepted the plan at the end of its gate walk but wrote it out immediately, before §4 scaffolds the site directory <directory_name>/ that plan belongs in — so a literal top-to-bottom implementation wrote the bare .kntnt-wp-skills.json one level up, in the operator's invocation cwd. That mismatch guaranteed two false smoke FAILs (check_saved_plan_present, check_baseline_present, both anchored at the site directory) and left a future pull unable to find its plan or baseline. §3 now defers the write; §4's final bullet, after the restart onto the corrected engine, persists the accepted plan into <directory_name>/.kntnt-wp-skills.json — pre-creating the directory in §3 was rejected, since mkwp (≥ 1.8.1) refuses to scaffold into a path that already exists. "The local project root" — the site directory <directory_name>/ both persistence artifacts live in — is now defined once, in CONTEXT.md, and referenced consistently from docs/spec.md and both skills (#40).

  • The poll discipline no longer treats a single spurious 404 on GET /extractions/{id} as terminal. The second live smoke test of the Extractor cutover proved the bare 404-is-terminal rule wrong in the field: the status endpoint returned 404 twice mid-job while the job was alive and progressing, a server-side non-atomic job.json rewrite race (Kntnt/kntnt-extractor#20). A 404 is now terminal only when confirmed vanished: the first 404 is treated as a transport-class fault, retried under the existing 30 s / 60 s backoff, and cross-checked against GET /extractions; the job counts as vanished only when the re-poll also 404s and the id is absent from the listing — otherwise the blip is logged and retried, and polling continues within budget. ADR-0018, both clone/pull SKILLs, agents/extract-transfer.md, and agents/discovery-classify.md's compact reference now state the rule identically (#41, refines ADR-0018 / #34).

  • Every documented keygen invocation was the bare uv run scripts/unseal.py keygen, but the helper always parses a JSON envelope from stdin and keygen requires a caller-chosen private_key_path (the private key must land outside the docroot), so following the docs verbatim crashed at the health-check preflight with a generic JSON-parse error. All eight call sites — three each in the clone and pull SKILLs, plus agents/discovery-classify.md and agents/extract-transfer.md — now pipe the real envelope echo '{"private_key_path": "..."}' | uv run scripts/unseal.py keygen, and keygen fed empty or malformed stdin now exits non-zero with a self-documenting...

Read more

v0.5.0

Choose a tag to compare

@TBarregren TBarregren released this 23 Jul 14:23

Added

  • A single deterministic assembler, scripts/build_exclusions.py, now builds the resolved exclusion set — the union of a canonical always-excluded constant (ALWAYS_EXCLUDED: the configuration file, the WordPress drop-ins, the debug log, the cache dir, and the upgrade dirs) with the decision-gated DB-known thumbnails, flagged heavy blobs, and — under --exclude-media — the whole uploads tree. Both consumers that previously hand-assembled the set from the same ingredients now obtain it from this one helper: the extraction file selection (clone §5) and the baseline manifest (clone §9.12, pull's diff), so the two are byte-identical and a scope change can never poison the pull deletion diff. The always-excluded paths exist exactly once, in the constant — every prose reference points at it rather than re-enumerating — and scripts/classify.py now emits uploads_prefix so a media exclusion can be anchored at the WordPress root. Structured so the two blocked child issues can extend the constant: credential-bearing backup patterns (#36) and the WordPress core tree (#37) (#35).

Fixed

  • ALWAYS_EXCLUDED now covers the credential-bearing pattern family that previously sailed through unfiltered: every backup, editor-swap, and variant sibling of wp-config.php (wp-config.php.*, wp-config.php~, .wp-config.php.sw?, wp-config-*.php — with wp-config-sample.php, WordPress' own placeholder-only template, explicitly carved back out), .env/.env.* anywhere in the tree, and root-level database dumps (*.sql, *.sql.gz, *.sql.zip) and key material (*.pem, *.key, id_rsa*). scripts/filter_manifest.py and scripts/baseline_diff.py's is_excluded now match a glob-bearing exclusion entry case-insensitively, anchored either at the install root or anywhere in the tree, alongside the existing exact-or-descendant prefix matching. Found on the second live smoke test of the Extractor cutover, where two live wp-config.php.bak-* files carrying the complete secret family in clear text sailed through the exclusion set and landed in local staging (#36).
  • ALWAYS_EXCLUDED now also covers the whole WordPress core tree — wp-admin/, wp-includes/, and the root-level core PHP files (index.php, wp-login.php, wp-settings.php, xmlrpc.php, and the rest of the install-root PHP core ships) — so it is never selected for extraction: production's copy is always byte-identical to what clone's scaffold just installed, so transferring it was pure waste. scripts/filter_manifest.py and scripts/baseline_diff.py's is_excluded now also match a bare top-level directory prefix (no / of its own, like wp-admin) against itself and its descendants, not only against a root-anchored file pattern. The specification, the clone/pull skills, and the implementation notes no longer claim GET /files returns a content-scoped tree — it is install-root-wide and carries core too — and now state explicitly that core is kept out by this client-side exclusion set, never by the server. Found on the second live smoke test of the Extractor cutover, where 3,485 core files were extracted and merged over the byte-identical core the scaffold had just installed (#37).

Full changelog: https://github.com/Kntnt/kntnt-wp-skills/blob/v0.5.0/CHANGELOG.md

v0.4.1

Choose a tag to compare

@TBarregren TBarregren released this 23 Jul 09:33

Fixed

  • CONTEXT.md's Exposure window glossary entry named the wrong endpoint for the happy-path close: consuming a finished job is POST /extractions/{id}/consume, not DELETE /extractions/{id} — the latter is only for cancelling a stranded or aborted job. A leftover from the 0.4.0 cutover, where the same mix-up was fixed in the spec and the implementation notes but the glossary was missed.
  • The extraction poll no longer fails on a single stalled status request, and the health-check download preflight now exercises the continuation path a one-chunk job never touches. All three poll loops — preflight, bootstrap, and main extraction — follow one pinned discipline: a steady 15 s cadence with a 120 s per-request timeout, retry with backoff on a transport timeout, connection error, or 5xx, progress reported between polls, and failure only on a failed state, a vanished job, a 10-minute no-progress stall, or the loop's overall wall-clock budget (10/15/60 minutes respectively). The preflight submits exactly two structure-only tables ({table_prefix}options, {table_prefix}users) and times create-to-ready: ≤ 90 s passes silently, a slower-but-completing probe warns loudly that the host advances jobs at backstop cadence and gates on the operator, and a probe that misses the budget aborts with that warning as the remediation. Both faults surfaced on the first live smoke of the Extractor cutover, where two zero-byte poll responses were read as failure while the job completed fine, and a one-chunk preflight passed on a host whose continuation loopback was dead (#34, ADR-0018).

Full changelog: https://github.com/Kntnt/kntnt-wp-skills/blob/v0.4.1/CHANGELOG.md

v0.4.0

Choose a tag to compare

@TBarregren TBarregren released this 22 Jul 20:12

This release cuts the control channel over from the Novamira MCP to the Kntnt Extractor REST API (ADR-0016, ADR-0017). Breaking: production now requires the Kntnt Extractor plugin (REST API version ≥ 2) and an Application Password in place of the Novamira MCP server.

Changed

  • The control channel is now the Kntnt Extractor plugin's REST API instead of the Novamira MCP execute-php channel. clone and pull reach production over HTTPS as a real WordPress user authenticated with an Application Password (HTTP basic auth), and every data call is gated on both the kntnt_extractor_operate and manage_options capabilities. Production now requires Kntnt Extractor (REST API version ≥ 2) installed and enabled in place of the Novamira MCP server; there is no arbitrary PHP or WP-CLI execution and no SSH path (#24, ADR-0016, ADR-0017).
  • Discovery is now a two-phase reconstruction over that surface: GET /environment, GET /tables, and the paged GET /files supply the runtime, table, and file facts, and a cheap bootstrap extraction — parsed client-side — supplies the attachment metadata, entity counts, and mass-send poised-campaign scan that shape the main extraction, replacing the single server-side execute-php discovery scan.
  • The extraction, per-segment sealing, one-time download link, and cleanup are now owned by the plugin's own background job; the engine only builds the selection, submits it, polls the job to completion, downloads, and unseals the sealed container to the run's ephemeral X25519 key pair. Data comes down sealed under authenticated encryption rather than a passphrase-encrypted archive, so no passphrase is ever generated or web-served, and the database password and the auth keys/salts/nonces are masked server-side and never enter model context.
  • The mkwp skill now recommends installing Kntnt Extractor by default on a freshly scaffolded site — resolving kntnt-extractor-*.zip from the plugin's latest GitHub release, with a graceful drop on failure — so the new site is already reachable by a later clone/pull.
  • The required CLI tool floor is now uv, jq, and curl. shasum/sha256sum and openssl are no longer needed: integrity is authenticated encryption verified when the container unseals, and the unseal is a uv-run helper with pynacl as an inline dependency.

Removed

  • The Novamira MCP control channel and its execute-php/run-wp-cli abilities, the SSH path, and the entire client-side pack machinery — the generated pack.sh, the server-side passphrase, the openssl encryption, the outside-docroot working directory, and the docroot download directory — all superseded by the plugin-owned background extraction (#24, ADR-0016, ADR-0017).

Full changelog: https://github.com/Kntnt/kntnt-wp-skills/blob/v0.4.0/CHANGELOG.md

v0.3.2

Choose a tag to compare

@TBarregren TBarregren released this 20 Jul 20:42

A documentation-only patch: the docs, manual pages, and helper docstrings are reconciled with the code. No plugin behaviour changed.

Fixed

  • Documentation reconciled with the code across the specification, the manual pages, and templates/README.md, with no change to behaviour: templates/README.md now documents kntnt-wp-skills-mailpit.php as the local capture mu-plugin the engine drops into the copy — not an execute-php control-channel payload — and scopes the "no declare(strict_types=1), no namespace" convention to the payload table so it no longer contradicts that file; docs/spec.md lists the fifth build-ollie-site helper (instantiate_patterns.py) and the local manifest-filtering helper (filter_manifest.py), and no longer claims the saved plan records the empty-table classification patterns (they are re-derived classify.py constants, never persisted); the clone/pull manual pages describe the --include-media/--exclude-media default as the built-in constant it is rather than a discovery-derived one; README.md notes the production-side pack script is generated at runtime by scripts/pack_script.py rather than shipped as a static shell template; and docs/implementation-notes.md corrects the Mailpit host to 127.0.0.1:1025 and adds @@collation_database to the discovery queries. The build-ollie-site reference and helper docstrings are likewise corrected (M<n> module tags label sections rather than detect components; the check_contrast.py and dump_ground_truth.py usage strings).

Full changelog: https://github.com/Kntnt/kntnt-wp-skills/blob/v0.3.2/CHANGELOG.md

v0.3.1

Choose a tag to compare

@TBarregren TBarregren released this 20 Jul 20:00

Fixed

  • The entity-count smoke-test check no longer produces a false-positive FAIL on a site whose active plugin filters the main query. scripts/smoke_test.py's check_entity_counts now derives its live counts with raw SQL over ddev wp db query — the same unfiltered COUNT(*) templates/discovery.php builds the expectation from, mirrored clause-for-clause per entity (published posts, published pages, attachments with trash/auto-draft excluded, users) against the site's real table prefix — rather than through wp post list / wp user list, which go via WP_Query and are silently narrowed by any active main-query-filtering plugin (Bogo, and the broader membership/geo-restriction/post-visibility class); a complete clone of such a site tripped the check on every run (#33).
  • Bogo (bogo/bogo.php) is now recognised as a multilingual plugin by scripts/discovery.py's detect_multilingual, so the localised-subpage rewrite-flush smoke-test canary is armed on Bogo sites instead of being silently skipped (#33).

Full changelog: https://github.com/Kntnt/kntnt-wp-skills/blob/v0.3.1/CHANGELOG.md