# α Close-Out — Cycle #261 (PR #263)

## Cycle summary

| | |
|---|---|
| Issue | #261 — Wire skill activation to filesystem discovery + frontmatter visibility |
| PR | #263 — squash-merged to main as `41485ad` |
| Ships in | **unreleased** — next tagged release inherits this change |
| Review rounds | **3** (Round 1 REQUEST CHANGES → Round 2 approved-modulo-kata-tier2 → Round 3 merged after the new check was downgraded to info) |
| Findings total | **3** — F1 (D-judgment, parser gap), F2 (C-judgment, pre-existing main defect, filed separately), F3 (B-judgment, flushList asymmetry) |
| Mechanical ratio | 0% (0 / 3) |
| Diff shape | **L7** — replaced a manifest-declared skill inventory with filesystem-authoritative discovery + per-artifact visibility. Eliminates a whole class of drift (manifest claims vs on-disk reality) and a whole class of authoring overhead (declaring every skill twice). |
| Cycle process level | **L6** — implementation L5-clean, one Round-1 D-finding (inline YAML support missing) and one Round-2 discovery (real-corpus conflict surfacing) required semantic investigation before fixing. |

## Findings

### Finding 1 — F1: inline YAML flow sequences dropped silently (D, judgment)

**Caught:** Round 1 β review (comment on PR #263).

**Cause:** Ported OCaml's `Cn_frontmatter.parse_frontmatter` 1:1, including its "inline triggers not supported, warn and ignore" branch. That limitation was harmless in the OCaml era because `manifest_skill_ids` returned only manifest-exposed skill IDs (a narrow set), but after ef53b939 removed `sources.skills` the OCaml validate loop ran over nothing, so the inline-list gap never surfaced. In the new Go path — filesystem-authoritative (AC1) — the walker discovers every SKILL.md on disk, 81% of which (42 of 52) use inline form. The new doctor check flagged all of them as `empty triggers`.

**Fix:** `parseInlineList` in `frontmatter.go` — strip `[`/`]`, split on commas, trim each item; `[]` yields nil triggers; interior whitespace tolerated. Quoted items and nested sequences still unsupported (no production use). `TestParseFrontmatter_InlineTriggersSupported` replaces `_InlineTriggersIgnored` (the old test codified the bug). Regression coverage: `TestDiscover_RealCoreSkills_HaveTriggers` (copies an unmodified cnos.core SKILL.md into a temp hub) and `TestValidate_RealCorpus_NoEmptyTriggers` (walks every `src/packages/*/skills/**/SKILL.md` and asserts zero `IssueEmptyTriggers`). Landed at `9cfd3c9`.

**Skill gap candidate for γ:** The OCaml-to-Go port implicitly assumed "parser parity = behavioural parity", but the two systems observed different input sets. When porting a parser whose test corpus was narrowed by an upstream manifest filter (now removed), the port must re-ground its test corpus against the widened input. The integration test `TestDiscover_RealCoreSkills_HaveTriggers` mechanizes this for this specific parser; worth a generalised rule in `eng/go` about "port surface = input surface, not just code surface".

### Finding 2 — F2: deterministic tarball builds (C, judgment, pre-existing)

**Caught:** Round 1 β review.

**Not introduced by this PR.** β identified `createTarGz` in `src/go/internal/pkgbuild/build.go` copies `info.ModTime()` into tar entry headers and `gzip.NewWriter` records the current time in the gzip header, so SHA-256s vary by checkout time. Scope-deferred per β's review §7.0 exception.

**Fix:** filed as [#264](https://github.com/usurobor/cnos/issues/264) — "Deterministic package tarball builds: strip file ModTime and gzip header time". Out of #261 scope. I3 CI check remains red on `main` until #264 lands; kata-tier2 (the check that matters for #261's correctness claim) is green on the merge commit.

### Finding 3 — F3: flushList dispatch asymmetry (B, judgment)

**Caught:** Round 1 β review.

**Cause:** In the first-cut parser, `flushList` materialised only the `triggers` block-list — other keys could legally open a block list (any `key:` + indented `- items` lines) but their items would silently accumulate in `triggersAcc` and get discarded at flush. Latent (no other block-list field existed), but a landmine for future fields like `aliases:`.

**Fix:** Renamed the buffer `triggersAcc` → `pendingItems` and moved the "which field gets materialised" decision into a `switch pendingListKey` inside `flushList`. Adding a new block-list field now extends one case instead of inheriting the triggers-shaped buffer. Landed at `9cfd3c9`.

### Additional finding (caught Round 2, not in β's review) — real-corpus conflict surfacing

**Caught:** kata-tier2 still red on `9cfd3c9` after F1 fix. Investigation surfaced two latent semantic bugs in the new validator that the F1 fix alone did not resolve:

(A) Internal sub-skills (visibility: internal, e.g. `cdd/review`) were counted as conflict claimants alongside their public parent orchestrator (e.g. `cdd`), even though internal skills are never publicly addressable.

(B) The `Trigger_conflict = fatal` rule assumes 1-keyword-→-1-skill, but the real corpus has 9 legitimate public-public overlaps in cnos.core alone (e.g. `coherence` claimed by both `agent/clp` and `agent/coherent` — both genuinely apply). The OCaml fatal rule was never exercised against this surface because `manifest_skill_ids` only returned the narrow exposed set.

**Fix:** Visibility filter in `ValidateSkills` (internal skills excluded from conflict detection) + severity split in the doctor wrapper (`IssueMissingSkill` = fatal; `IssueEmptyTriggers` + `IssueTriggerConflict` = info). The latter is an explicit model claim: **cnos skill activation is many-to-many — keywords are hints that can match multiple skills, not exclusive dispatches**. Landed at `7e76798` after user decision (AskUserQuestion → "Downgrade to warning").

## Cycle Iteration

§9.1 triggers — one fired:

- [ ] review rounds > 2 (actual: 3, but two were review-discovery not rework)
- [ ] mechanical ratio > 20% (actual: 0%, all 3 β findings were judgment)
- [x] avoidable tooling/environmental failure — F1 should have been caught at authoring time (see skill gap below)
- [ ] loaded skill failed to prevent a finding (the closest is eng/go §2.12, partially applicable; see below)

**Avoidable failure analysis.** F1 was not new work; it was inherited verbatim from the OCaml reference code the issue explicitly instructed to read as behavioural reference. The authoring-time audit I did was "read OCaml → port 1:1 → match OCaml tests". What I missed was that the OCaml tests never exercised inline-list parsing against real SKILL.md input — they synthesised minimal frontmatter blocks inline in the test file. A five-minute grep for `"^triggers:\s*\["` in `src/packages/` would have flagged all 42 inline cases before the first commit. Worth mechanizing.

## What worked

1. **Parse/Read purity slot pre-opened.** `frontmatter.go` (pure) + `index.go` (IO) mirrors the existing `pkg.ParseManifest`/`restore.ReadManifest` split without inventing new patterns. The discipline is now structural in the tree — the next such port doesn't have to re-argue it.

2. **TestValidate_RealCorpus_NoEmptyTriggers as canary.** Walking every real SKILL.md in `src/packages/` and asserting Validate's output shape against the actual corpus turned what was going to be an authoring-time audit (diffs visible only to the reviewer) into a mechanical gate (CI). Adding this kind of "integration test against your own ground-truth corpus" is cheap and would catch F1-class drift automatically.

3. **Independent PR from user via β (third round).** After the F1+F3 fix, kata-tier2 re-failed for reasons unrelated to the parser gap. The discipline of "investigate root cause before re-pushing" (rather than assuming my F1 fix was sufficient) surfaced the two latent semantic bugs (visibility filter + severity split) in a single investigation round. Without that discipline I'd have pushed a no-op re-run and bounced.

4. **Clean peer enumeration on docs.** PACKAGE-AUTHORING.md was the named AC5 target, but PACKAGE-ARTIFACTS.md in the same directory carried the same old `sources.skills` narrative. Catching that as a peer update (rather than a review finding) meant β didn't have to flag it. The α skill §2.3 peer-enumeration rule was the generative constraint that produced the catch.

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

Three rounds between first push and merge:

- **Round 1** (`7283741`, `969dcc1`): initial scope + review-nit polish. β returned REQUEST CHANGES with 3 findings (F1 D, F2 C, F3 B).
- **Round 2** (`9cfd3c9`): F1+F3 fixed, F2 deferred to #264. Re-requested review. kata-tier2 still red.
- **Round 3** (`7e76798`): real-corpus conflict surfacing investigated, visibility filter + severity split landed. Merged.

Each iteration converged on a real defect. Zero thrash commits. But Round 1 could have been avoided entirely for F1 — the bug was parser-shape, the authoring-time audit was the-OCaml-tests, and a cheap mechanical check against the real corpus would have caught it before first push.

## Skill gap observation (for γ triage)

**Loaded skills:** `cdd`, `cdd/alpha`, `eng/go`, `eng/test`.

`eng/go` §2.12 (schema and compatibility) is the closest existing surface — the 3.56.2 addendum ("sibling harnesses are contract surfaces too") extended it to non-Go producers. The failure mode this cycle adds is structurally similar but inverted: **when porting a parser, the input surface is not the upstream test corpus — it's the real data the parser will run against.** A parser under OCaml-era constraints had a narrowed input; the port's grounding must match the widened input.

Proposed generalisation for `eng/go` §2.12 or adjacent:

> When porting a parser or validator across surfaces, the test grounding is the real input corpus at the destination surface, not the source's test fixtures. Before first push, grep the project for every producer of the parser's input shape and ensure at least one representative of each form is in the test suite. A shape the parser will silently reject against the real corpus is a regression even if the reference tests pass.

Mechanical form: `grep -rn '^<field>:' src/` for each field the parser materialises, cross-check against the parser's supported value shapes, add regression tests for any gap.

## Design decisions worth recording (not findings)

**Many-to-many activation as an explicit model claim.** Downgrading `Trigger_conflict` from fatal to info was the architecturally-significant choice of this cycle. It codifies what the real corpus already did de-facto — multiple public skills can and do claim the same trigger keyword (`coherence` on agent/clp + agent/coherent; `onboard` on agent/hello-world + agent/onboarding; etc). The activation index is `trigger → set-of-skills`, not `trigger → one-skill`. OCaml's fatal rule was a 1-to-1 assumption that was never exercised (narrow input). Making the assumption explicit in the doctor severity is half the fix; the other half is leaving room for a future cycle to formalise the many-to-many resolution model (precedence? all-loaded? prompt the agent?). Out of #261 scope.

**Filesystem walk skips SKILL.md directly under `skills/`.** A SKILL.md placed directly under `skills/` with no containing subdirectory has no derivable skill ID (the ID = relative path of the containing dir). Authors must place each skill under `skills/<name>/`. Contract is pinned by `TestDiscover_RootLevelSKILLMdSkipped` and the in-code comment in `discoverPackageSkills`.

## Summary for γ triage

| Item | Severity | Candidate |
|------|----------|-----------|
| Parser-port skill-gap addendum: when porting a parser/validator across surfaces, ground tests in the destination's real input corpus, not the source's test fixtures. | B (skill gap) | MCA — addendum to `eng/go` §2.12 or adjacent. Cheap mechanical form: grep producers of the parser's input shape. |
| Deterministic tarball builds — filed as #264. Pre-existing main defect surfaced during #261 review. | C (carry-forward) | Separate cycle. I3 CI check stays red until #264 lands. |
| Formalise the many-to-many trigger activation model. Currently implicit: `Trigger_conflict` is info, not fatal. A future cycle could define resolution semantics (precedence? all-loaded? prompt?). | — (design question) | Separate issue. Not urgent — current model works de-facto. |
| Three rounds on a D + B + latent-semantic discovery. Round 1 was avoidable (parser-port grounding gap); Rounds 2–3 were investigation, not rework. | — | Process data point for §9.1 threshold review. Mechanical ratio at 0%; judgment ratio at 100%. |

α recommendation: ship the `eng/go` §2.12 parser-port addendum as an immediate output next cycle (§10.1). Don't block release on it — the addendum mechanizes a rule that would have prevented F1 at authoring time, but the fix itself is in the codebase.

— alpha@cdd.cnos
