release: v1.3.2 — scope-aware install, upgrade and version reporting - #110
Merged
Conversation
Remove .claude/, CLAUDE.md, .github/copilot-instructions.md and docs/ from version control (kept locally, gitignored) so they are not published to GitHub.
chore: remove AI-assistant config and internal docs from the repo
Previously the ASCII banner only appeared on 'hkm version'. It now headers the default help output too.
feat(cli): show Sentinel banner on bare hkm and hkm help
hkm upgrade now detects the OS, downloads the matching release artifact, and installs it (Linux apt / macOS install.sh / Windows install.bat), instead of only printing manual instructions.
feat(upgrade): auto-download and install updates per OS
…g.env - run/registry now resolve the kernel relative to the launcher (installed /opt/hkm-kernel or dev repo), fixing 'Kernel registry not found' on packaged installs and stopping use of a dev kernel found via PWD. - hkm-config checks the kernel + writes/repairs HKM_KERNEL_HOME. - launcher loads ~/.config/hkm/config.env at startup (real env wins). No version change.
fix(cli): self-locate installed kernel + real hkm-config
Scaffolding templates moved tools/src/templates -> top-level templates/. tools/ is not bundled, so hkm new / hkm ui init could not find templates on a packaged install. bundle.sh now ships templates/ (exempt from the docs/ tools strip); services resolves <kernel>/templates via self-location. No version change.
fix(templates): ship templates in the kernel (move out of tools/)
Kernel self-location, real hkm-config, config.env loading, and templates shipped inside the kernel.
chore(release): v1.0.3
…help note
- projects/projects.json is committed empty ({}) so developer-local
registrations (and machine paths) never ship in the repo or bundles.
- .githooks/pre-commit forces projects.json to {} in every commit; enable
with: git config core.hooksPath .githooks
- hkm help notes the env vars are auto-detected (override only if needed).
…help chore: empty committed project registry + help note
projects.json + platform.json are user data. HKM_USERDATA_DIR relocates them outside the kernel install (honoured by the hkm CLI registry and the PHP DomainResolver), and the .deb marks them as conffiles so an in-place upgrade preserves the user's registrations. Falls back to <kernel>/projects when unset.
feat(userdata): HKM_USERDATA_DIR so updates don't clobber the registry
…ata) hkm-config now resolves/pins HKM_KERNEL_HOME AND provisions the persistent userdata dir: creates XDG_DATA_HOME/hkm (or ~/.local/share/hkm), migrates any existing registry into it, and pins HKM_USERDATA_DIR. One command configures everything the launcher and runtime need.
feat(config): hkm-config provisions the full environment
- .env (holds generated APP_KEY) written chmod 600; config.env too - debug output force-disabled when APP_ENV=production regardless of APP_DEBUG - new projects ship app/public/.htaccess (deny dotfiles, no listing, drop X-Powered-By, baseline security headers, front-controller rewrite) - env.example documents the production/secret-handling expectations
security: harden scaffolding defaults
Security hardening (scaffolding perms, prod debug gate, Apache+nginx web config), HKM_USERDATA_DIR for persistent registry across updates, and hkm-config full-environment setup.
chore(release): v1.0.4
New projects scaffold app/apache.conf.example (DocumentRoot=app/public, deny dotfiles, only index.php executable, security headers).
feat(scaffold): Apache vhost sample
Adds the Apache virtual-host sample to project scaffolding (alongside nginx).
- README now documents native install (.deb/.tar.gz/.zip), the hkm command set, HKM_* env vars, requirements, dev/build flow, and security defaults. - Remove links to the removed docs/ai-context files from the Auth plugin README.
docs: refresh README + fix broken links
Route policy — the third route verb (add/override/DISABLE):
- Kernel::withRoutePolicy() + proj.json "routePolicy": {"disable": []} let a
project veto plugin routes without forking the plugin. Specs are either
"METHOD /path" (one route) or a module domain (all of a plugin's routes).
- CompileRouteManifestStage applies the policy to plugin routes AFTER they
compile and BEFORE project routes, so a disabled key can be re-declared by
the project. An unmatched spec fails the boot (anti-typo guard).
- EntryHelpers::projectRoutePolicy() reads the proj.json block.
hkm dev environment for contributors:
- `hkm <command> --dev` pins one invocation to the development kernel
(HKM_DEV_HOME from config.env, or walk-up self-location from a repo-built
launcher). Exports HKM_KERNEL_HOME + HKM_CLI_PATH for the child only;
fails loudly when no dev kernel is found.
- hkm-config set-dev-home <path> (validated) + help/README documentation.
Templates: scaffolded proj.json ships the routePolicy stub, bootstrap wires
withRoutePolicy(), project README documents the three route verbs.
Project routePolicy.disable (veto plugin routes without forking) and the hkm --dev contributor environment (stable install + dev checkout side by side).
…s plugins
Class "Plugins\Logger\Provider" does not exist
The scaffolded bootstrap enables nineteen providers (Logger, Crypto, Database,
…) and the scaffolded composer.json maps Plugins\ to the project's plugins/
directory — but nothing ever put anything there. The kernel stopped shipping
plugins when they moved to their own repositories, and `hkm new` was never
taught to fetch them, so every project created since the decoupling died on its
first request.
`hkm new` now installs them over git, through the same path `hkm plugins
install` uses: resolved to a tag, kernel-compatibility checked, and recorded in
plugins.lock.json so the project is reproducible from the moment it is created.
The set is read from the scaffolded app/bootstrap/app.php rather than hard-coded
here. A list in this file would drift from what the template actually wires, and
the drift would reproduce exactly the missing-class failure this fixes.
Failures are reported per plugin and summarised, rather than left to surface as
a class-not-found at runtime: the message says how many are missing and that the
project will not boot until they are installed.
Verified end to end: scaffolding a project installs 19 plugins, publishes 52
assets, and the bootstrap now returns a Kernel instead of throwing. Confirmed
plugins/Logger/Provider.php — the exact class from the report — is present.
…ache
The routing layer, end to end.
DECLARATION
groups[] in module.json / proj.json states a prefix, filters, requires, a name
prefix and a domain ONCE for every route inside; groups nest. Module-wide
routePrefix / routeFilters / routeRequires / routeName / routeDomain do the
same for a whole file. Everything expands at BOOT into ordinary flat routes,
so grouping costs nothing at request time.
DOMAINS
A route may declare the host it answers on — "africavoting.local",
"*.example.com", or a bare "subdomain": "api". The domain is part of the route
KEY, not a post-match filter, so one project can answer GET / differently per
host. Ungrouped routes stay global; a bare subdomain answers on that label of
every domain. A declared host is checked against proj.json "domains".
SECURITY
Captured parameters are percent-decoded and RE-VALIDATED against their type,
so /files/..%2F..%2Fetc%2Fpasswd no longer satisfies {name} — and /users/José
reaches the controller decoded instead of as Jos%C3%A9. Patterns are anchored
with the D modifier (a trailing newline no longer satisfies $), literal path
text is preg_quote'd, and signed-URL verification compares the query
byte-for-byte instead of round-tripping it through parse_str().
New {file:path} type: a catch-all that refuses ".." — use it instead of {any}
for anything reaching a filesystem. New {s:enum(a|b)} and optional {page?}.
COST
Kernel::build() was not idempotent, so under PHP-FPM the whole boot pipeline
ran on EVERY request — ~2 ms and ~150 KB of writes for ~130 routes, producing
byte-identical output. BOOT_CACHE=1 skips it when nothing it read changed:
~0.02 ms, 86x cheaper. Off by default; clear var/cache/manifests on deploy.
The matcher now reads a precompiled index (route-index.php) instead of
deriving regexes per worker, and buckets dynamic routes by first literal
segment. Filter stages resolve once per worker rather than per request.
ANTI-TYPO
These now fail the BOOT instead of compiling into a route that silently never
matched: a path not starting with '/', a duplicate or PCRE-invalid capture
name, a handler without exactly one '@', an unregistered filter alias, and a
route domain absent from proj.json "domains".
The 1.2.0 release build could not check out: the submodule pointer referenced
cd3f71c, which exists only on a developer machine and was never pushed to
AlfaCode-Team/Let-Migrate.
fatal: remote error: upload-pack: not our ref cd3f71c…
Fetched in submodule path 'modules/let-migrate', but it did not contain it.
Restores the pointer to 68f72db, the newest commit the remote actually has. The
local checkout is left where it is, so the unpushed work is not lost — push it
to Let-Migrate and bump the pointer deliberately when it is ready to ship.
- Introduced `plugin_domains.zig` to manage domain resolution for plugins, allowing for better dependency handling based on declared capabilities rather than repository names. - Implemented `plugin_store.zig` to create a global plugin cache, ensuring that plugins are shared across projects and reducing redundant downloads. - Added migration template `migration_alter.php` for altering existing database tables in plugins. - Created a simple application bootstrap file `app.php` to initialize the project with a focus on minimalism and explicit dependency management. - Established a test aggregator `tests.zig` to ensure all relevant tests are executed, improving test coverage and reliability.
`composer validate` is a required check and it failed on 1.2.0: the lock file's content-hash covers the version field, so stamping a literal version dirtied the lock, and Composer warns about the field on a Packagist-published package. The field is absent from the repo BY DESIGN — build.zig says why: the native distribution ships without a .git directory, so a .deb or zip has no tags to derive a version from and needs the marker; but in a checkout "a literal version OVERRIDES the tags, and the two silently drift apart". tools/bundle.sh stamps it at release time, which is how the published v1.2.0 artifacts got their version.
The previous fix set the gitlink in the index only, leaving the submodule's working tree on the unpushed cd3f71c — and the next commit silently picked it back up, so CI failed again with "not our ref cd3f71c". This moves the working tree too, so index, tree and HEAD agree and it cannot drift back. The unpushed work is preserved as the branch `wip/unpushed-cd3f71c` inside modules/let-migrate. Push it to AlfaCode-Team/Let-Migrate and bump the pointer deliberately when it is ready to ship.
…rupts JSON Three fixes from the review on #109. DUPLICATE CHECKS php-analysis.yml listed master under BOTH push and pull_request, so every master->main PR ran it twice — the push event for refs/heads/master and the pull_request event for refs/pull/N/merge. The concurrency group is keyed on the ref, so the two never collided, and each PR showed "composer audit", "PHPStan" and "Semgrep" twice. Limited push to main, which is the shape ci.yml already uses (and why PHPUnit and Zig build appeared only once). Master is still analysed — through the PR. COMPOSER.JSON CORRUPTION composerValid() discarded everything after '+' without looking at it, so `1.1.0+"` validated; stamp() then writes the version RAW between JSON quotes and produced an unparseable composer.json: "version": "1.1.0+"", Build metadata is now validated as semver defines it — dot-separated [0-9A-Za-z-] identifiers — and stamp() refuses outright any version carrying a quote, backslash or control character. Regression tests cover both. SILENT DIAGNOSTICS - A version longer than the 256-byte message buffer made bufPrint fail, and the error path returned WITHOUT printing anything, so the marker was skipped with no diagnostic at all. Falls back to a fixed message. - `hkm new` swallowed recordInLock failures while still counting the plugin as installed, so the command reported success with a lock that did not list it. It now names the plugin and how to repair the lock.
`domain` / `subdomain` previously took a single host, at the route and group level, while the module-wide `routeDomain` took one too. A project serving several hosts therefore could not pin a group to "these three and not that one" — the only way to express it was to duplicate the whole group per host, which is how a route ends up on a host nobody meant to serve it on. All three levels — module-wide, group, route — now take either a string or a list, and behave identically. One of them quietly refusing a list is the kind of inconsistency only ever discovered by it not working. The domain stays part of the route KEY, so one project still answers GET / differently per host, and a route grouped under a host the project does not serve is still rejected at boot rather than silently unreachable.
Every plugin lists the environment it reads in module.json `config[]`, and the
kernel FAILS THE BOOT when a required one is absent (ValidateConfigStage).
Until now enabling a plugin left the operator to discover that list from a
stack trace, one variable per boot attempt.
Enabling now writes the whole set into .env at once, in three shapes, and the
difference between them is load-bearing:
default present KEY=value written ACTIVE — the documented default
required, no default KEY= written ACTIVE but empty — boot still fails,
but it fails pointing at a line you can see
optional, no default # KEY= written COMMENTED — documents the knob
without pinning a value
The .deb was the only install path, so trying the kernel meant apt, sudo and a system-wide PHP — a high price for "does this work on my machine", and impossible on a box you do not own. Linux release builds now produce TWO artifacts and the tarball is the primary one: tools/install.sh unpacks the kernel and launcher entirely inside $HOME, writes nothing outside it, and needs no privileges. install.sh is published alongside the assets so `curl … | sh` works without a checkout. The .deb stays for multi-user machines and CI images, where a system-wide install and apt-managed PHP are the point. `hkm doctor` grew the diagnostics this makes necessary: which install is actually being used, and whether a stale HKM_KERNEL_HOME pin in ~/.config/hkm/config.env is overriding it — the failure that otherwise presents as "my changes do nothing".
Pageflow v1.1.0 ships @pageflow/admin — an admin shell that deliberately owns
no state of its own. Three things have to exist on the PROJECT side for that to
work, and the scaffold now provides all three:
- **A three-state theme.** `@providers/theme` exposes { theme, resolvedTheme,
setTheme, toggle } and a "system" default that keeps following the OS if the
OS setting changes mid-session. The shell's <ThemeToggle> is only a control
over this context, so two plugins can never fight over the app's appearance.
- **Sidebar CSS variables.** A plugin cannot ship the variables its own
components depend on and still be overridable per project, so they live in
the project's theme.css. Restyle freely; keep the NAMES.
- **A globbed nav registry.** Each plugin contributing to the sidebar ships
ui/admin/nav.ts and registers at import time. Globbing them beats a
hard-coded list in the registry, which every new plugin would have to edit.
Both surfaces also wrap their tree in AppErrorBoundary: without one, a throw in
any page component unmounts the whole app rather than the page that failed.
655829006 merges two parallel implementations of unknown-option handling into src/AbstractCommand.php and keeps BOTH: `private array $unknownOptions` is declared twice (lines 50 and 78), with two incompatible row shapes (`spelling`/`key` populated at 482 and resolved by resolveUnknownOptions(), `token`/`name` populated at 158/520 and rejected by rejectUnknownOptions()). A duplicated property is a fatal at CLASS LOAD, so this is not one failing test — at that pointer every command built on AbstractCommand dies with "Cannot redeclare AlfacodeTeam\PhpIoCli\AbstractCommand::$unknownOptions". The kernel suite surfaces it as UnknownOptionTest ending the PHP process. Pinned back to 53620ec, the last commit where the class loads: 312 tests, 585 assertions green. Which of the two implementations is canonical is php-io-cli's call, so this reverts the POINTER only and changes nothing in that repo.
A machine can hold both a system (.deb) and a user (tarball) install. The CLI did not model that: a shared config.env HKM_KERNEL_HOME pin let either install redirect the other's kernel, and hkm upgrade could only ever update the system scope. Both made installing or upgrading appear to do nothing. Release 1.3.2.
hakeemRash
requested review from
Alshatri and
craftdevscommunity
as code owners
August 17, 2026 18:37
Alshatri
approved these changes
Aug 17, 2026
craftdevscommunity
approved these changes
Aug 17, 2026
hakeemRash
added a commit
that referenced
this pull request
Aug 18, 2026
main is the SQUASH of #110, so it is a new commit that master's history does not contain, and git reports the branches as diverged. Their trees are not: origin/main and master@299a271 are the same tree (9abe29c), and master has only moved forward since. -s ours therefore records the merge without touching a single file — the content already includes everything main has. The merge commit itself disappears when this PR is squash-merged, which is what branch protection on main requires.
hakeemRash
added a commit
that referenced
this pull request
Aug 18, 2026
main is the SQUASH of #110, so it is a new commit that master's history does not contain, and git reports the branches as diverged. Their trees are not: origin/main and master@299a271 are the same tree (9abe29c), and master has only moved forward since. -s ours therefore records the merge without touching a single file — the content already includes everything main has. The merge commit itself disappears when this PR is squash-merged, which is what branch protection on main requires.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Releases v1.3.2.
auto-release.ymlreads the topCHANGELOG.mdheading, somerging this tags
v1.3.2and publishes the build.Same content as #109, on a fresh branch so the 17 stale review threads from the
1.2.0 cycle do not block the merge. #109 can be closed once this lands.
v1.3.2 — two install scopes that stop hijacking each other
A machine can hold both a system install (
.deb→/opt/hkm-kernel+/usr/bin)and a user install (tarball →
~/.local). The CLI did not model that, and everysymptom below came from the same gap:
~/.config/hkm/config.envisread by every
hkmon the machine, andHKM_KERNEL_HOMEwas checked beforeself-location — so whichever installer wrote that pin last redirected the other.
A
.deblauncher reported its own version while running a kernel out of theuser's home, and upgrading either scope could not move the number on screen.
hkm upgradecould not update a user install on Linux. It only ever fetchedthe
.deband shelled out tosudo apt-get, despite the user-local tarballbeing the documented default since 1.3.1. Because
PATHusually resolves~/.local/binfirst, the command reported success and the next invocation ranthe old launcher unchanged.
kernel somewhere else — two numbers that differ exactly when the launcher on
PATHbelongs to the other scope.What changed
Resolution ranks sources by how specific they are to the invocation: exported env
var → self-location → config pin →
/opt. A launcher in/usr/binclaims/opt/hkm-kernelat the self-location step, since no relative probe reaches itfrom there.
hkm upgradepicks its scope from privilege — root → system (.debvia apt),otherwise → user (tarball + its
install.sh), with--user/--systemtoforce it. No
sudoanywhere in the user path.New
hkm versionreports every install: the kernel version in each scope, thelauncher serving it and the version it was built as, and which one
PATHruns.New
hkm-config unset <KEY>clears a stale pin;install.shnow removes aredundant or superseded pin instead of repointing it.
Full detail in
CHANGELOG.mdunder[1.3.2].Verification
version round-tripping).
past the shared pin; scope defaults behave under root and non-root; a full
--local --userinstall stamps a version Composer validates.Not included
The uncommitted
modules/php-io-clibump (53620ec→04147c7) is deliberatelyleft out — it breaks 4 tests in
UnknownOptionTest(AbstractCommand.php:175,undefined array key
name), so including it would fail the PHPUnit gate.