Session scope: regenerate the committed .skill upload artifact from the authoritative skill dir, ahead of standing up a troubleshooting clone for the devteam on a SEPARATE cloud (openstack-caracal-ipv4 test env). Packaging + changelog only -- NO cloud mutation, NO script change, NO runbook change, no skill CONTENT edit. CURRENT-STATE deliberately untouched (no status-bearing surface changed: no docs/audit/ file, no **Status: line, no | G<n> | gate row -- L10 not triggered).
Branch base: post-merge main (6495cfb), which was verified to carry the 2026-07-27 skill sweep -- git merge-base --is-ancestor 39e8988 origin/main returned 0, and .claude/ + skills/ are byte-identical between origin/main and dc-dc-stage5-preconditions. This matters because the derived artifact must not be built from a base older than its source.
skills/openstack-cloud-ops/openstack-cloud-ops.skill REGENERATEDThe committed upload artifact was last written 2026-07-05 (35cfe41) and had gone 22 days stale against .claude/skills/openstack-cloud-ops/, which is the single source of truth (changelog-20260703-process-hardening, items at :304 and :349-350: "edit under .claude/skills/, repackage the .skill from it").
Measured staleness of the OLD artifact:
SKILL.md 129 lines packaged vs 325 authoritative -- it predates the GA-R1..R7 record-authority section, the VR1 DC-DC routing rows, the D-128 operating model, the D-135 per-DC artifact-strategy invariant, the DOCFIX-200 "Stage 4 hands off READY nodes" ruling, and the four audit-derived invariants (finding-vs-conclusion, citation-as-existence-claim, ruled-is-not-built, a-checker-that-cannot-fail).platform-traps.md (517 lines -- the per-tool trap + verbatim-error index) and opentofu-provider-docs.md (154 lines).environment.md, operating-discipline.md, script-authoring.md, troubleshooting.md all differed.description names openstack-caracal-ipv4 as "the repository this skill operates against". The 2026-07-05 build predates BOTH the D-110 seed of this repo (2026-07-09) and the repo-agnostic sweep of the same week (changelog-20260709-repo-agnostic-sweep), so the literal was correct WHEN BUILT. It is wrong NOW, and not because of a rename: per D-110, openstack-caracal-ipv4 is the separate, still-live SOURCE project (VR0 v1, IPv4-only) this repo was seeded FROM at 8813efc -- with its own D-series ceiling at D-075 and the D-076..D-099 band reserved to it. The authoritative copy is repo-agnostic ("the repository this skill operates against") per the L9 self-ref guard. So an upload of the stale package points the operating skill at a DIFFERENT PROJECT's repo, which is a live confusion risk right now: that is the same repo backing the devteam's test env.Build (deterministic by construction, so a rebuild from unchanged sources is byte-identical and drift is detectable by hash):
STAGE=<tmp>; cp -r .claude/skills/openstack-cloud-ops "$STAGE/openstack-cloud-ops"
find "$STAGE" -exec touch -h -t 202607270000.00 {} +
cd "$STAGE" && zip -r -X -q out.zip openstack-cloud-ops
Verification captured this session:
openstack-cloud-ops/ prefix dir, NOT a bare ./SKILL.md (a bare layout mis-mounts on claude.ai upload). unzip -l = 9 entries / 7 files (was 5 files), 104171 bytes uncompressed.unzip of the committed artifact diff -r against .claude/skills/openstack-cloud-ops/ -> IDENTICAL, byte-for-byte.287424fd8ad014db18d9f3609ef4edd2fe434f34fb26c899f67506b68154555d..gitattributes pins *.zip binary but the extension here is .skill (see Item 2): working file sha256 == staged blob sha256, and git diff --cached reports Bin 22230 -> 50267 bytes. Git's text=auto content detection classified it binary, so no normalization occurred. The artifact is correct as committed.bash scripts/repo-lint.sh -> PASS (0 fail, 0 warn) on this base.
Revert: git revert this commit, or restore the prior blob directly: git checkout 35cfe41 -- skills/openstack-cloud-ops/openstack-cloud-ops.skill (prior sha256 is recoverable from that blob; prior size 22230 bytes). Note that reverting restores an artifact that mis-names the repo, per Item 1.
Both surfaced while packaging. Neither is actioned here; both are put to the operator rather than fixed mid-step.
.gitattributes covers *.zip binary but not *.skill. The stanza's own comment states the intent -- "Binary artifacts must never be EOL-normalized (DOCFIX-071 ships a committed zip)" -- and this file IS a committed zip that the pattern misses. It is currently benign (measured above: git's content detection catches it), but the protection is incidental rather than declared. The 2026-07-05 artifact used compression method store, which packs markdown RAW -- exactly the case where a text-looking blob is most at risk if content detection ever went the other way. Minimal fix would be one line: *.skill binary.
Nothing gates this artifact's freshness -- which is why it went 22 days stale, spanning three stage closes. The stage-close ritual in SKILL.md names two derived outputs -- keep .claude/skills/ current, then regenerate the dated consolidated .md snapshot -- but does NOT name the .skill. The snapshot was regenerated 2026-07-27 (39e8988); the .skill was not. This is the repo's own "a checker that cannot fail is not a gate" pattern in its weaker form: no checker at all. A scripts/skill-package.sh with its tests/skill-package/run-tests.sh harness, plus a repo-lint rule asserting the artifact's CONTENT matches the source dir (assert on content, never on existence), would make the staleness impossible rather than noticed. Scoped as OPS under GA-R3 -- no Roosevelt delta, fails the A1 grep test.
9ffc904)The commit body of 9ffc904 (this session's packaging commit, already pushed) describes the stale artifact's frontmatter literal as "correct when built (pre-D-110 seed)". That phrasing reads as though openstack-caracal-ipv4 were a FORMER NAME of this repo. It is not, and D-110 says so explicitly: openstack-caracal-ipv4 is the separate, still-live SOURCE project this repo was seeded FROM at 8813efc; its D-series ceils at D-075 and the D-076..D-099 band is reserved to it and NEVER assigned here.
Corrected in Item 1 above, which is the authoritative statement. The commit message is left as pushed rather than amended: git push --force* is a deny rule in .claude/settings.json, and working around a permission rule is barred by hard rule 3. Append-only correction also matches this repo's verbatim-record discipline -- the error and its correction both stay visible, which is the outcome an amend would have destroyed.
Worth keeping, because the misreading is the failure mode the skill already names: a finding is an observation, not a conclusion. "The packaged description names a repo that is not this one" was the reliable OBSERVATION; "therefore it is a stale name for this repo" was an unmeasured CONCLUSION, and one grep of docs/design-decisions.md inverted it.
openstack-caracal-ipv4Recorded here as SESSION NARRATIVE, not as an adopted finding. The cloud in question is openstack-caracal-ipv4 -- per D-110 a SEPARATE, still-live source project with its own D-0NN series -- so nothing here is imported into this repo's decision record. What is captured is the research, because it was expensive to derive and was otherwise transcript-only.
Symptom, operator-reported: signed into Horizon as devteam-cluster, Project > Container Infra > Clusters > TestCluster > Resize Cluster returns Error: Unable to get cluster's working nodes. TestCluster is single-node; the goal is three. That cloud runs Magnum with the magnum-capi-helm CAPI driver.
MEASURED (upstream source, not inferred):
magnum.service.js, in the .catch() of getClusterNodes(id), which wraps GET /api/container_infra/clusters/<id>/resize.ClusterResize.get() view in magnum_ui/api/rest/magnum.py, which only POPULATES the dialog. It has exactly three failure exits, ALL of which surface as this one toast: cluster_show() raising AttributeError -> 404; nodegroup_list() raising AttributeError -> 404; any other exception -> 500 (a 403 from nodegroup_list is NOT caught by that except AttributeError, so authz failures land in the 500 branch).Unable to resize given cluster id). Do not cite it as the cause. Checked per the skill's own rule: a citation is an existence claim, only its content is evidence.magnum-capi-helm DOES support nodegroups, so a driver gap is not the explanation.In-repo prior art that bears on it: D-063's C3 verification resized beta-cluster 1->2 AS THE TENANT to UPDATE_COMPLETE / HEALTHY, which is why the CLI (openstack coe cluster resize) was recommended as the unblock -- with the qualifier that C3 was on THIS project's cloud, not on ipv4. clientdocs/jenkins- kubernetes-guide.md documents both the CLI and the dashboard path; the dashboard line is a DOCFIX candidate IN THE IPV4 REPO if the dialog is broken there.
A self-contained diagnostic prompt for the dev-cloud Code agent was delivered to the operator as a FILE (environment-scoped against VR1 value bleed, four phases, report-before-acting, resize gated as an approved mutation). It is deliberately NOT committed here: it is an ipv4-directed operational document, and importing one is the boundary D-110 draws.
Escalation path if it matters to VR1: should the root cause prove ARCHITECTURAL (a genuine magnum-ui/CAPI-driver incompatibility rather than something local to that cloud), it would also affect the DC-DC deployment, and the correct move is a PROPOSED D-1NN in THIS repo that CITES the ipv4 finding by its original id -- never a merge of ipv4-derived commits.
.claude/hooks/guard-destructive.py inspects the Bash COMMAND STRING, so it blocks a git commit whose MESSAGE TEXT merely contains a force-push string -- the message was being passed via a heredoc, and the hook matched the literal inside it. The block is correct behaviour (fail-closed on a substring match) and should NOT be loosened; the authoring workaround is to write the message to a file and use git commit -F <file>.
Worth folding into references/script-authoring.md at the next stage close, as it will recur for anyone documenting the force-push ban in a commit or changelog. Not done here: editing the authoritative skill is a stage-close activity and this session closed no stage.
Every bash scripts/repo-lint.sh PASS reported earlier in this changelog and in the commit messages of 9ffc904, fb8b328 and 029d6ed was measured from inside a git worktree at .claude/worktrees/skill-repackage-20260727/, and scanned zero files.
Root cause, measured in scripts/repo_lint.py's walk filter (line ~224):
or (".claude" in p.parts and "worktrees" in p.parts):
continue
The exclusion is correct in intent -- DOCFIX-111 added it so a session-local agent worktree nested under a normal checkout is not linted as repo content. But it tests the parts of the ABSOLUTE path. When the lint ROOT is itself the worktree, every descendant path carries .claude and worktrees in its parts, so the filter matches EVERY file and the walk yields nothing. The linter then reports PASS: repo lint (0 fail, 0 warn) over an empty set.
This is the repo's own "a checker that cannot fail is not a gate" failure mode, in its purest form: not a checker that missed a defect, but a checker whose verdict was computed over nothing while printing the word PASS. It was found only because the concurrent Stage-5 session's R15(1) work added an L0 files-scanned floor that REFUSES a verdict below 100 files -- running that newer linter against this worktree failed immediately and correctly.
RE-MEASURED PROPERLY. The branch was exported outside any worktree path (git archive origin/skill-repackage-20260727 | tar -x -C <dir>, 728 files) and linted there:
main's linter (the one used above): 0 fail, 1 warnThe single WARN is the known legacy carve-out (docs/design-decisions.md, 239 non-ASCII bytes in the D-001..018 region) -- the same warn the shared checkout showed at session open, not something this branch introduced.
So the verdict stands but the evidence did not. The correct figure for this branch is 0 fail, 1 warn, not the 0 fail, 0 warn claimed earlier; the earlier zero-warn reading was an artifact of scanning nothing. No FAIL was ever masked -- verified twice above, including against the stricter linter.
SCOPE, measured not assumed: the GAUNTLET is UNAFFECTED. tests/repo-lint/run-tests.sh builds its fixture repos under mktemp -d (/tmp/...), which carries neither path component, so its assertions that each lint class FIRES genuinely fired. GAUNTLET: ALL GREEN (81 harnesses) stands as reported.
CONSEQUENCE BEYOND THIS SESSION (logged, not executed): background jobs on this host are instructed to isolate into .claude/worktrees/ before editing, so every such session's repo-lint gate is currently vacuous on main -- it will print PASS no matter what is wrong. The Stage-5 branch's L0 floor fixes the silent-pass half of this the moment it merges, converting it into a loud FAIL. The remaining half is that a worktree session then has no working lint at all; the fix would be to make the filter relative to the lint ROOT rather than testing absolute path parts. That belongs to whoever owns R15(1), not to this branch -- raised in the handoff rather than patched here.