You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
tools/check_action_inputs.py + CI step: fails when a step passes a with: key the pinned action does not declare. The runner only warns on such a key (Unexpected input(s)) and silently ignores the value - the class behind the syncLabels / sync-labels defect fixed above. Verified as a genuine gap before adding the gate: with the defect reintroduced, actionlint exits 0 and the full pytest suite passes unchanged. Resolves local actions from disk and pinned remote actions from raw.githubusercontent.com; a fetch failure is a tool error, never a pass. Unit tests inject the fetch, so they run offline; the CI step is the only networked consumer.
.github/dependabot.yml: weekly grouped updates for the github-actions ecosystem (updates both the pinned SHA and the # vN comment) and for uv (pyproject.toml + uv.lock). The repo pins everything; with no update bot the pins silently rot, which was the largest structural gap in the workflow layer.
tools/check_pins.py + CI step: fails when UPSTREAM_PIN is missing, duplicated, malformed or out of step across ci.yml / release.yml / publish-plugin.yml / upstream-drift-watch.yml. The lock-step rule was documented in two files and enforced by none; this makes it machine-checked and de-risks the re-pin procedure.
tools/check_action_shell.py + CI step: shellchecks every composite action's run: blocks. actionlint does not lint .github/actions/*/action.yml steps (verified against 1.7.12 by injecting a deliberate shell error), so the shared bash had no static check - a class that had already produced a real bug (see Fixed).
CI also gains: concurrency (superseded PR runs cancel; non-PR runs take a per-run group so every main push keeps its full run - tightened after the review below, see Fixed), timeout-minutes on every job, and 14-day retention on the build artifacts.
.github/actions/drift-issue-upsert/: shared composite action for the idempotent issue-by-title upsert both drift watchers need; the two callers' copies had already drifted apart (different jq expressions for the same intent).
Changed
Repository Actions policy tightened (settings, not files): allowed actions are now an allowlist - GitHub-owned actions plus astral-sh/setup-uv@* and oven-sh/setup-bun@* - with sha_pinning_required still on, and the default GITHUB_TOKEN can no longer create or approve pull requests. Introducing any other action now requires a deliberate settings change. CONTRIBUTING documents the policy and the "add to the allowlist first" rule; its UPSTREAM_PIN lock-step bullet is corrected from three files to four (publish-dist.yml was missing after the publisher rename).
Single-source installs. The package now installs from this one repository: the built tree is published to its dist branch, which is the repository's default branch, so hermes plugins install iap/pstack-hermes works root-level and hermes plugins update pstack updates natively. The separate dist repo is retired; no second repository is ever published. (A main-subdir install of pstack/ remains as a legacy pinned-style alternative.) dist also carries a scheduler stub so the weekly drift watchers keep firing despite the default-branch flip; manual recovery for dispatch-only workflows is gh workflow run <file> --ref main.
Re-pin to upstream c47b1284 (pstack v0.15.5); pstack/ rebuilt. Every converter anchor was re-anchored or pruned against the new upstream text (36 dead anchors at the new pin -> 0; T8-T14 + delegation), F16 retired (upstream now ships the slug-agnostic lane check itself), reflect's 4-column reviewer table adopted with the upstream budget key in the setup-written panel shape, and the two new upstream principles ported (principle-attack-the-premise, principle-test-behavior-not-implementation) - 45 -> 47 skills. Prose model defaults refreshed (grok-4.7-xhigh-fast, claude-opus-5-5-max); the shipped panel keeps its vendor-qualified verified slugs. UPSTREAM_PIN bumped in all four workflows (lock-step enforced by tools/check_pins.py); details and the triage correction (no skills/orchestrate ever existed) in docs/ADAPTATIONS.md section 4.
Docs / agent policy (Phase A, no package rebuild): AGENTS.md and docs/USING.md codify the stock-skills write surface — Hermes self-improvement (skill_manage / learning) targets local skills, skill taps, or a dist fork, not stock plugin skills. Stack forge freeze: Graphite (gt) default, GitHub (gh) fallback, GitLab (glab) halted. README Known limitations aligned (was incorrectly describing Graphite as optional-only).
Fixed
Dev-tree README skill counts corrected: the skills bullet said 45 (24 + 21 principle-*) while the generated package README and .build-provenance.txt already said 47 (24 + 23) after the v0.15.5 re-pin. Docs-only; pstack/ untouched (#80).
Label sync never ran (found while auditing every with: key against each action's declared inputs - the only such mismatch in the repo). labeler.yml passed syncLabels: true, but the input id is sync-labels: the runner logs Unexpected input(s) 'syncLabels' and the action falls back to its declared default (off), so stale labels were never removed on synchronize despite the step comment promising exactly that. Corrected to sync-labels (kebab-case; both v5 and the v7 that #69 landed declare it). Evidence: run 37394726141's log records the ignored input verbatim.
.github/actions/build-package was outside dependabot's scan. The root github-actions entry covers .github/workflows plus a repo-root action.yml, not .github/actions/*/action.yml - observed when #69's setup-uv bump updated every workflow and skipped the shared action, leaving its pin behind and on a path to silent rot, the exact failure mode dependabot.yml exists to prevent (AGENTS.md: pins are "bumped by dependabot, not by hand"). Added a directory-scoped entry (the actions/github-script pattern) and caught the pin up to the SHA the workflows now use.
CI concurrency, from the manual Macroscope review of PR #67: cancel-in-progress: false alone did not make the "main runs stay whole" claim true. By default only one run may be pending per concurrency group and a newer pending run cancels the older one, so two merges in quick succession could cancel a queued main run before it ever started and the intermediate commit would skip verify-determinism. Non-PR runs now take a per-run group (github.run_id); semantics verified against GitHub's concurrency documentation.
Drift watchers (High), from the same review: moving the issue upsert into its own composite step silently dropped its guard. The check step's no-drift exit 0s end only that step, so the upsert still ran with $RUNNER_TEMP/drift-body.md absent and gh issue comment/create --body-file failed - the workflow would fail on the common no-drift path (latent today, because upstream drift keeps the body-writing path active; it would start failing the week after a re-pin). Both watchers now emit drift=true from the check step only on the path that also writes the body, and the upsert step is conditioned on it; pinned for both watchers by tools/tests/test_workflow_review_guards.py.
Drift watchers: the "not creating a possible duplicate" guard could never fire. existing="$(gh ... || lookup_status=$?)" set the variable inside the subshell, so the parent always saw the initial 0; when gh issue list failed, the workflow proceeded with an empty result and CREATED a duplicate tracking issue - the exact failure the guard's message claims to prevent. Reproduced in bash before fixing; shellcheck SC2030/SC2031 flagged it once the shellcheck gate existed. The upsert now lives in the shared drift-issue-upsert action with the status captured outside the substitution, and one jq form for both callers.
Action version pins had drifted between workflows (actions/checkout at v4 in the drift watchers vs v7 elsewhere; astral-sh/setup-uv v5 vs v10). Unified; dependabot keeps them current from here.
Model panel no longer written into the plugin directory (#57). hermes plugins update replaces the installed package tree, so a config/models.json written next to plugin.json was silently discarded on every update - the user's per-role model choices reverted to the shipped defaults with no warning. setup-pstack now writes pstack-models.json in the hermes config directory beside config.yaml, which survives reinstalls, and the consuming skills (why, reflect, arena, interrogate) read that path while still tolerating a legacy in-package panel so existing customizations are migrated rather than ignored. The shipped default panel stays in the package - only the user override moves.
dist_checkout_check: the gate no longer substitutes its own .gitattributes for the tree's, which made it blind to the regression it exists to catch. stage_tree copied tools/assets/dist.gitattributes over whatever the dist tree actually held, so a published tree missing the rule - the publisher cp line deleted, or the rule stale - still validated clean. A staged tree must now carry the rule and match the tested one; the source pstack/ tree (which legitimately has none, since the publisher adds it) is still allowed. Both refusals are counterfactual-checked: dropping either guard fails a test.
dist_checkout_check: the source-tree exemption is an explicit --source-tree flag instead of a directory-name guess. A staged dist checkout is legitimately named pstack, so the name-based rule let a published tree with no attributes file at all validate clean - the same blind spot the previous entry closes, reached by another route.
dist_checkout_check: git output is decoded as UTF-8 with surrogateescape instead of the ambient locale codec. Under cp1252 a non-ASCII path came back mangled, stopped matching its staged key, and was skipped as "absent" - so its CRLF and byte-identity assertions never ran while the gate reported clean.
dist_checkout_check: a package file git never tracks (typically excluded by a packaged .gitignore) is now refused rather than skipped, since it would otherwise never be verified while the gate reported clean.
dist_checkout_check: a git failure now exits 2 as documented instead of propagating a RuntimeError traceback at exit 1; a tracked file with no staged counterpart is reported as unverified rather than silently skipped; dead BINARY_SUFFIXES is removed; and the shipped dist.gitattributes gained its missing trailing newline.
Reworded the *.png binary justification in publish-plugin.yml: it claimed text=auto would corrupt assets/logo.png, but git's own content sniffing already prevents that and only a deliberate *.png text rule corrupts it. The line is kept as intent, not as the thing currently doing the work.
upstream-drift-watch: the job now runs at all. Its gh issue list/comment/create calls carry no --repo, so gh resolves the target from the git checkout - and the job had no actions/checkout, so every run failed with failed to run git: fatal: not a git repository. It has failed this way on every run since 2026-09-14, silently, because the last success was 2026-09-07 when upstream had not yet moved past the pin. The checkout is pinned by SHA with persist-credentials: false, matching model-drift-watch.yml. Also dropped the blobless --filter=blob:none clone (this job needs two commits of pstack/; the promisor repo only made the diff depend on network at diff time), and the drift diff exit status is now checked rather than used only as a condition, so a failed diff is reported as one. This is the guard that should have raised #53 (pstack is at v0.15.5 upstream; the pin is still v0.14.8). Only the blobless attempt was verified as usable, so when the shallow fallback also failed, set -e aborted at the fetch and the run logged two copies of fatal: not a git repository with nothing pointing at the real fault. Both attempts are now checked the same way, and a total failure exits with the clone error itself. This job has failed this way on every run since 2026-09-14 — silently, because the last success was 2026-09-07 when upstream had not yet moved past the pin. It is the guard that exists to report upstream drift, and issue #53 is the drift it should have raised (pstack is now at v0.15.5 upstream; the pin is still v0.14.8).
Added
tools/dist_checkout_check.py: regression check for the property the build gates are structurally unable to see. The build job validates the source pstack/ directory, where files are LF because the converter wrote them that way, so it cannot catch a line-ending or binary-asset regression introduced by the dist publish step. This stages the package the way publish-plugin.yml does, clones it with core.autocrlf=true (the Windows default, and what hermes plugins install does), then asserts every tracked text file checked out LF and every non-text file byte-identical. Wired into the build job and re-run against the staged tree in publish-plugin.yml before it commits, so a bad tree is never published. The attributes rule moved to tools/assets/dist.gitattributes, which the publisher copies verbatim, so the rule and the test that guards it cannot drift apart.
Fixed
Coverage gap: the check classified a file as text only when it had a known extension or a listed name, and as binary only when its bytes contained a NUL. This package ships extension-less text files (LICENSE and the watch-pr launcher), which matched neither test and were therefore silently skipped - the checker reported a clean tree without ever looking at 2 of 134 files. Text is now the default and binary requires a NUL byte, so the two branches are mutually exclusive and exhaustive and every tracked file is checked.
Dist repo line endings: publish-plugin.yml now writes a .gitattributes (* text=auto eol=lf, *.png binary) into the published iap/pstack tree. The dist repo is consumed by hermes plugins install as a git clone, so on a consumer with core.autocrlf=true (the Windows default) every text file - including the user-editable config/models.json - was checked out as CRLF, and hermes plugins update's stash/pull/reapply then rewrote it again, drifting the package away from its own LF-only contract. Measured on a forced checkout with autocrlf=true: 38 CRLF before, 0 after, assets/logo.png byte-identical either way.
test_validate_encoding.py fixture: wrote the LF-only SKILL.md fixture with write_text, which translates \n to os.linesep on Windows, so the assertion failed there for a platform reason rather than a product reason. Now uses write_bytes with explicit \n. check_encoding itself was correct and is unchanged; this only makes the fixture platform-independent. (lint-and-tests runs on ubuntu-latest only, so the windows-latest build matrix never exercised this test.)