# Changelog -- 2026-07-27 -- repackage the openstack-cloud-ops .skill artifact

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.

## Item 1 -- `skills/openstack-cloud-ops/openstack-cloud-ops.skill` REGENERATED

The 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).
- **Two reference files were absent entirely**: `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.
- Consequential content defect: the packaged frontmatter `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:

- Internal layout preserved against the old package: a top-level
  `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.
- Sources verified ASCII + LF before packing: 0 non-ASCII lines and 0 CR lines
  across all 7 files.
- **Round-trip**: `unzip` of the committed artifact `diff -r` against
  `.claude/skills/openstack-cloud-ops/` -> IDENTICAL, byte-for-byte.
- **Determinism**: independent rebuild reproduced
  sha256 `287424fd8ad014db18d9f3609ef4edd2fe434f34fb26c899f67506b68154555d`.
- EOL-normalization checked, since `.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.

## Item 2 -- findings LOGGED, not executed (hard rule 1)

Both surfaced while packaging. Neither is actioned here; both are put to the
operator rather than fixed mid-step.

1. **`.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`.

2. **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.

## Item 3 -- record correction (commit message of `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.

## Item 4 -- ADVISORY (no repo change): devteam TestCluster resize, `openstack-caracal-ipv4`

Recorded 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):

- The string is raised in magnum-ui's `magnum.service.js`, in the `.catch()` of
  `getClusterNodes(id)`, which wraps
  `GET /api/container_infra/clusters/<id>/resize`.
- That URL is Horizon's OWN `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).
- **Therefore the resize call is never made.** This is a READ failure populating
  the dialog, BEFORE any resize is attempted -- so it is not the resize action
  being denied. Authz remains a candidate CAUSE, but of reading nodegroups.
- Launchpad **#2054444** ("Cluster Resize is failing in UI") is **Invalid**
  (2024-03-01, Ubuntu-focal-specific) and carries a DIFFERENT string
  (`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.

## Item 5 -- authoring trap found this session (logged, not executed)

`.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.
