# α Close-Out — Cycle #262 (PR #265)

## Cycle summary

| | |
|---|---|
| Issue | #262 — cn pack: derive packlist from content-class model instead of walking entire package root |
| PR | #265 — squash-merged to main as `9a1c7df` |
| Ships in | **unreleased** — next tagged release inherits this change |
| Scope shipped | **Partial**. AC2 (shared constant), AC3-subdir-filtering, AC4 (sorted deterministic packlist), AC6 (visibility does not gate packaging), AC7 (tests) landed. AC1 (cn build → cn pack rename), AC5 (`--list`/`--dry-run`), AC3-root-allowlist tightening + `.gitignore` honoring deferred with explicit reasons; #262 stays open. |
| Review rounds | **3** (Round 1 REQUEST CHANGES — D1 + C1 + C2 + B1 + A1 + A2 → Round 2 REQUEST CHANGES — D1' base re-drift → Round 3 APPROVED) |
| Findings total | **7 filed · 5 fixed in-cycle · 2 deferred with reason** |
| Mechanical ratio (fixed) | **60% (3/5)** — D1, D1', A2 mechanical; C1, C2 judgment |
| Diff shape | **L4/L5** — contained refactor of `createTarGz` from walk-based to packlist-based, plus a named accessor (`pkg.ClassSkills`) to close AC2. No API surface change externally. |
| Cycle process level | **L6** — implementation itself L5-clean, but three review rounds required: two were mechanical base-drift (D1, D1'), one was a scope-correction round (C1 AC2 gap, C2 PR body wording). |

## Findings

### Finding 1 — D1: branch not rebased on current main at initial push (D, mechanical)

**Caught:** Round 1 β review.

**Cause:** First push used the branch as-provided by γ dispatch, based on `8265fe0` (parent of `b4ca7e4`/`73de30b` which had landed on main between the dispatch and my first push). α pre-review gate #1 ("branch rebased onto current main") was not exercised mechanically before the first push. β's merge-commit CI then ran I3 against a tree whose `dist/packages/` was stale relative to main's SKILL.md edits.

**Fix:** `git rebase origin/main && cn build && git add dist/packages && git commit --amend && git push --force-with-lease`. Landed in `7248001`.

### Finding 2 — C1: AC2 incomplete — activation still hardcoded `"skills"` (C, judgment)

**Caught:** Round 1 β review.

**Cause:** DerivePacklist reused `pkgtypes.ContentClasses` as intended, but `src/go/internal/activation/index.go:discoverPackageSkills` still had `skillsRoot := filepath.Join(pkgDir, "skills")` as a raw string literal. AC2 explicitly requires the shared constant be used by *both* `cn pack` and skill activation; without the activation-side wiring the single-source-of-truth promise held for one consumer only.

**Fix:** Added `pkg.ClassSkills = "skills"` as a named accessor alongside `ContentClasses` in `src/go/internal/pkg/pkg.go`, then referenced it from activation. Landed in `eb9a6fe`. Alternative considered: index into `ContentClasses` by position. Rejected — fragile across future additions to the slice. Named accessor keeps the semantic "this call-site addresses exactly one class" visible, and the accompanying docstring makes "adding a new class with a bespoke consumer" a mechanical edit (slice entry + optional named constant).

### Finding 3 — C2: PR body wording — #262 status unclear (C, judgment)

**Caught:** Round 1 β review.

**Cause:** Initial PR body said "Refs #262" but did not enumerate which ACs were deferred vs closed. β's γ-side dispatch described the full #262 scope, so absent a scope table the audit trail for the remaining slices was ambiguous.

**Fix:** Rewrote PR body with (a) explicit #262-stays-open AC table, (b) CDD Trace through step 7a (also closes A2), (c) deviation section detailing why AC3's root-allowlist tightening is deferred (load-bearing root files in cnos.core/cnos.kata), (d) explicit `Refs #262` not `Closes`. No code change.

### Finding 4 — D1' (Round 2): branch re-drifted during fix round (D, mechanical)

**Caught:** Round 2 β review.

**Cause:** While I was fixing Round 1 findings, main moved from `07f021f` (my Round-1 rebase base) to `22c0be0` (one commit touching `cnos.cdd/skills/cdd/alpha/SKILL.md`). Round 1's "fresh CI run against rebased base" was green only because GitHub doesn't re-run CI on the merge commit when main lands non-conflicting commits. Post-merge, the committed dist would no longer match source.

**Fix:** `git rebase origin/main` again (now onto `22c0be0`), `cn build`, amend, force-push. Landed in `da92e44`. Only `cnos.cdd-3.57.0.tar.gz` bytes changed; hash matched β's local round-2 rebuild exactly.

### Finding 5 — B1: root-file filter is wider than AC3 intent (B, judgment, deferred)

**Caught:** Round 1 β review.

**Cause:** AC3 specifies `README*` / `LICENSE*` as the only non-manifest root files that should ship. Implementation ships any non-dotfile root entry, because tightening drops `cnos.core/AGENTS.md` + `HEARTBEAT.md` + `TOOLS.md` (load-bearing doc content) and `cnos.kata/lib.sh` (sourced by every kata `run.sh`). Narrower rule would break katas and silently demote cnos.core docs.

**Fix:** Deferred with explicit rationale in PR body. Follow-up under #262: first migrate the load-bearing root files into content-class dirs, then narrow the rule, then add `.gitignore` honoring. No pre-merge block per β.

### Finding 6 — A1: branch name doesn't match CDD §4.2 (A, mechanical, acknowledged)

**Caught:** Round 1 β review.

**Cause:** Dispatch-provided branch name `claude/add-content-class-constant-Oo4a8` doesn't follow `{agent}/{issue}-{scope}`. Can't rename a pushed branch without opening a new PR — γ-side coordination drift. β explicitly marked as acknowledge-only, not a fix target.

**Fix:** None. Noted for γ close-out. This is the same γ-dispatch-drift class as C2.

### Finding 7 — A2: PR body missing CDD Trace (A, mechanical)

**Caught:** Round 1 β review.

**Fix:** Added six-row CDD Trace (Gap / Mode / Artifacts / Self-coherence / Pre-review / Review-loop) to PR body during the C2 rewrite. Same patch.

### Additional context (caught during Round 2) — cross-Go-toolchain non-determinism

**Caught:** Round 2, while investigating why I3 was still red after Round 1's rebase + dist refresh.

**Surfaced:** `compress/gzip`'s flate output is **not byte-identical across minor Go toolchain versions.** CI was pinned to Go 1.22 (`go.mod` + three `setup-go@v5` calls); local dev was on Go 1.24.7. Same source tree produced different SHA-256s depending on which toolchain rebuilt. #264's `TestCreateTarGzDeterministic` only proved *within-version* determinism — the cross-version axis was an unnamed assumption.

**Compounding defect on main:** I3 was already red on main at `07f021f` because `b4ca7e4`/`73de30b` landed SKILL.md edits (which live under `src/packages/cnos.cdd/`) without rebuilding dist. Even a rebuild by a matching toolchain would not have been authored in those commits.

**Fix:** Bumped `go.mod` `go 1.22` → `go 1.24` and every `setup-go@v5` in `ci.yml`/`coherence.yml`/`release.yml` to `'1.24'`. Dist refreshed against matching toolchain. Landed in `24f12d4` (later subsumed by `da92e44`). Operator concurred on scope expansion before pushing.

## Cycle Iteration

§9.1 triggers — two fired:

- [x] review rounds > 2 (actual: 3 — one was base-drift re-round, one was scope-correction round)
- [x] mechanical ratio > 20% (actual: 60% of fixed findings — D1, D1', A2)
- [x] avoidable tooling/environmental failure — D1 could have been caught by a mechanical `git fetch origin && git merge-base --is-ancestor origin/main HEAD` pre-push check. D1' could have been narrowed by the same check before the round-1 fix push, though some race is inherent.
- [ ] loaded skill failed to prevent a finding — α pre-review gate #1 *does* say "branch rebased onto current main". The skill didn't fail; I didn't mechanically run the check before either push. The gate is ritual-shaped, not enforcement-shaped.

**Avoidable failure analysis.** D1 and D1' are the same class: "committed dist/ matches committed source, but merge-commit dist does not match merged source because main moved." Pre-review gate #1 catches this at the moment of pushing, but only if the check is mechanical. A five-second `git fetch origin && git rebase origin/main` ahead of every α push would have eliminated both rounds of re-work. The gate is currently a checklist item in prose; worth promoting to a pre-push hook or a CI pre-check on the PR branch itself.

## What worked

1. **Reuse of `pkgtypes.ContentClasses` — no duplicate constant.** The γ dispatch explicitly called this out ("The content-class constant already exists … Reuse it — do not duplicate"). `DerivePacklist` and `FindContentClasses` both iterate the same slice; the only new surface added was `pkg.ClassSkills`, introduced because C1 surfaced a single-class call-site that couldn't cleanly use the slice. Single source of truth now holds end-to-end (packlist, check, status, activation).

2. **Integration test against real tarball bytes.** `TestBuildOneExcludesStrayFilesFromTarball` + `TestBuildOneIncludesInternalVisibilitySkill` use `readTarEntries` to walk the actually-produced `.tar.gz`, not just `DerivePacklist`'s return value. This caught the negative-space claim ("stray files never ship") at the right evidence depth (artifact, not return-value).

3. **Cross-surface canary: `TestDerivePacklistReusesContentClassConstant`.** If a future PR adds a content class to the slice but a consumer forgets to pick it up, this test fails with a clear message. Cheap regression guard around AC2.

4. **Round-2 diagnosis — separated "base drift" from "cross-version drift".** When the Round-1 rebase didn't fix I3, the instinct to re-rebase would have been wrong. Actually checking main's I3 state at HEAD + comparing my local hash with β's reported local hash surfaced the real cross-toolchain-version issue. Proportionate scope expansion (Go 1.24 bump) followed.

5. **Scope discipline under a permissive dispatch.** γ dispatch described the full #262 surface; the branch name + active tier-3 skills (`eng/go`, `eng/test`, notably not `eng/ux-cli`) signalled a narrower slice. PR body made the deferral table explicit up-front rather than letting β discover the gap. C2 was a wording fix, not a scope fix.

## What didn't — pre-review and review friction

Three rounds between first push and merge:

- **Round 1** (`567e195` initial): scope was correct but six findings (D + C×2 + B + A×2). D1 was preventable; C1 was a real AC-coverage gap.
- **Round 2** (`24f12d4`): D1 + C1 + C2 + A2 closed, then I3 still red. Diagnosed as cross-version drift + main-side I3 defect. Go toolchain bumped. D1' filed because main had moved during the round.
- **Round 3** (`da92e44`): D1' closed via another rebase. Approved and merged.

**Friction sources (attributable to α):**
- α pre-review gate #1 not mechanical — D1 and half of D1'.
- C1 was a genuine AC-coverage miss at authoring time: DerivePacklist used the constant, but I didn't grep for other string-literal "skills" in the tree. Single-source-of-truth audits must enumerate peers mechanically, not just "where I'm editing now".

**Friction sources (system-level, not attributable to α):**
- `dist/` is committed; `src/packages/` edits on main repackage content; no merge queue or dist-on-CI generation → D1/D1' race class. β flagged this as a γ/process-level L7 candidate with three candidate MCAs: (a) move dist/ out of git, I3 compares CI-produced tarball checksums against a committed manifest; (b) merge queue that re-runs I3 at merge-time; (c) freeze main-side `src/packages/` edits while a PR touching packaged files is open. Not α or β to fix inside this PR.

## Skill gap candidates for γ

1. **α pre-review gate #1 is ritual-shaped, not enforcement-shaped.** Proposal: promote "branch rebased onto current main" to a mechanical pre-push check. Could be a `git` alias, a pre-push hook, or a CI pre-check on the branch ref. D1-class findings would stop being filed as β-surfaced findings.

2. **`eng/go` §2.13 (determinism) should name the cross-toolchain axis.** The existing text addresses within-binary determinism but not across-binary-versions. `compress/gzip`'s flate heuristics are not a stable contract across minor Go versions. Proposal: extend §2.13 with either (a) "pin toolchain in go.mod + all CI `setup-go` calls" or (b) "use `gzip.NoCompression` for reproducible cross-version bytes" or (c) "generate deterministic artifacts on CI, not at author-time". Whichever the repo adopts, it deserves to be generative in the skill, not discovered per-PR.

3. **Single-source-of-truth audits need peer enumeration, not just local reuse.** α §2.3 peer-enumeration rule is the right shape; worth a worked example in the skill that explicitly lists "other consumers of the same constant or literal" as a mandatory grep before claiming AC closure. C1 surfaced because β grepped the tree; I had grepped only the file I was editing.

4. **CDD process-level MCA** for the `dist/` + main-side-packaged-edit race (see "What didn't"). Not an α skill gap; filed here for β's close-out to pick up.
