Skip to content

docs: Phase 8 entry research (Step 0 capability matrix + GO verdict) - #8

Merged
artyhoo merged 14 commits into
mainfrom
docs/phase-8-entry-research
May 8, 2026
Merged

docs: Phase 8 entry research (Step 0 capability matrix + GO verdict)#8
artyhoo merged 14 commits into
mainfrom
docs/phase-8-entry-research

Conversation

@artyhoo

@artyhoo artyhoo commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 8 entry research per EXECUTION-PLAN.md §5.5. Step 0 «existing solutions research» across 5 capability areas; per-capability go/no-go decisions; Phase 8 task list ordered C1 → C4 → C2 → C3 → C5; GO verdict.

This is the first phase where Step 0 ran proactively (not retrofit), validating the §5.5 gate as workable in burn mode.

Capability verdicts

# Capability Verdict
C1 Next 15 → 16 breaking changes diff Reuse version-16.mdx (15 items found vs §3.5 snapshot's 7)
C2 Canonical regen diff metric Build preset-similarity.ts (≤80 LOC, no deps)
C3 /aif-verify integration spike Reuse AIF aif-gate-result contract (mapping ≤60 LOC)
C4 Recipe expansion R12/R14/R20 Build via mechanical lift (~45 min)
C5 Gate 5 cost-scoping Reuse AIF review-sidecar mapping; defer impl

≥1 reuse decision per §5.5 acceptance: 3/5. No all-build red flag.

Dependencies

Test plan

  • npm test --workspace=@rules-as-tests/core --run → 220/220
  • make self-audit → green
  • phase-8-research.md ≤200 lines (199)
  • retros/phase-8-entry.md verdict GO
  • All 7 Phase 8 entry commits conventional (English subjects)
  • git diff main --name-only non-docs/ paths empty

artyhoo added 14 commits May 8, 2026 16:30
…vot)

Add §6.0 «v1 deterministic stance (locked Phase 4-7)» after the §6
Phase 5/6/7 reordering note. Lists deterministic v1 deliverables for
L1-L5, the 4 hard stop-rules held through Phase 7 retros, and the 5
v2-trigger areas with layer mapping. open-questions.md §13.10 is the
SSOT for trigger conditions; this section is a name + pointer index.

Mirrors the §5.5 «Phase entry gate» pattern in placement and tone. Does
not renumber Phase 0.5+ headings.
…/versioning/BC backlog

Add §13.10 (LLM v2 trigger conditions per area — 5 entries with trigger
+ verification gate), §13.11 (cost model and tracking, deferred to first
LLM v2 invocation), §13.12 (real-corpus validation strategy, deferred to
first real consumer or Phase 8 acceptance), §13.13 (preset/recipe
versioning policy, deferred to Phase 11 entry), §13.14 (rules-lock.json
migration policy, deferred to first additive schema change).

§13.10 is the SSOT for v2 trigger conditions referenced from
EXECUTION-PLAN.md §6.0. Mirrors the «OPEN, v2 trigger» pattern
established in §13.7.
…ON-PLAN §11 pointer + roadmap.md outdated marker

EXECUTION-PLAN §11 «AIF integration» line expanded to a pointer block
listing the three Phase 11 subtasks with their trigger condition
(Phase 8 + 9 stable + first consumer onboard).

aif-comparison.md §7 promoted from generic «Phase deferrals» table to
«Phase 11 backlog» SSOT. 11.1 emits aif-gate-result JSON shape from
L4/L5; 11.2 converts rules-manifest.json ↔ AIF RULE-SCHEMA with
3-rule round-trip acceptance (R1, R7, R12); 11.3 ships
contributing-recipes.md for external authors. Touchpoint 4 closure
(commit b5e16b7, Phase 4) and /aif-verify spike routing (Phase 8 entry
per phase-7.md Open Q #5) are explicitly NOT subtasks. §6.2 RULES.md
downgrade gets a closure pointer to 11.1.

roadmap.md gets an OUTDATED header — phase numbering there is
pre-AIF-analysis (2026-05-07), superseded by EXECUTION-PLAN.md §6
post-2026-05-08.
…l pointer

self-diagnostics-design.md — new shipped reference (≤200 lines, 9
sections). Documents v1 telemetry schema (ruleFireCounts + planDrift),
storage at .ai-factory/synthesizer-output/diagnostics-log.json, ring
buffer 30d / 5MB, read CLI (meta-factory diagnose), write hooks
(diagnostics-init InstallStage + opt-in pre-commit). v1 = local-only,
fail-open; v2 triggers (upload/aggregation/LLM scoring) gated.

self-application.md §2 row L5 + §7 acceptance both extended with the
(c) self-diagnostics emission invariant; (a) and (b) preserved
verbatim from Phase 7 closure.

EXECUTION-PLAN.md §6 Phase 8 gets a parallel-sub-phase note for Phase
8.X. Trigger: Phase 8 acceptance test green; does not block Phase 9
entry.
…v1.5 self-diagnostics pointer

§2.4 (L2 Research Agent): note that v1 ships curated store; LLM (context7
+ web_search_20250305) is v2 trigger per open-questions.md §13.10.
§2.5 (L3 Synthesizer): note that v1 = hand-authored recipes; LLM «picks
from menu» is v2; Path B is a separate v2 trigger.
§2.6 (L4 Validator): note that gates 1/2/4/6 are v1 REQUIRED; gate 3
(mutation) requires Path B; gate 5 (two-AI review) maps to AIF
review-sidecar with cost-scope deferred to Phase 8.
§2.7 (L5 Installer): note that v1 = artifact write only; v1.5 =
self-diagnostics per self-diagnostics-design.md (Phase 8.X); v2 = npm
deps + husky + GHA generation.
§3.3 (Path switching): note that synthesis-mode config is v2-only;
v1 = implicit conservative-equivalent.

Each annotation cross-refs the SSOT trigger condition in
open-questions.md §13.10 (or §13.13/§13.14 for L5 versioning/BC).
Phase 7.5 closure — 5 atomic doc commits (+ this retro) across 6 files
plus self-diagnostics-design.md new shipped reference. Zero code
changes; all tests still green (220/220 core); make self-audit green;
each shipped reference ≤500 lines.

Verdict: GO to Phase 8 entry. Phase 11 backlog (3 subtasks) lives in
aif-comparison.md §7 SSOT; LLM v2 trigger conditions (5 areas) live in
open-questions.md §13.10 SSOT. Wall-clock ≈30 min single-session burn
mode.
@artyhoo
artyhoo merged commit 3d871ef into main May 8, 2026
15 checks passed
artyhoo added a commit that referenced this pull request May 11, 2026
…32#31-#36 (AIF Handoff collision fix)

2026-05-11 readiness review on `fix/wave-5-readiness-revise` identified
BLOCKER B-1: the branch's own AIF Handoff overlap analysis (commits
`5d8cf1c`, `0f7f572`) landed SSOT entries #27-#30 in
`prior-art-evaluations.md` for HANDOFF_MODE / paused-semantic /
handoff:task / implement-coordinator — re-occupying the range previously
remap'd to in `d7a6c87` for Wave 5 §13.25 candidates.

Remap §10 table + all cross-references in §4 (Continue permissions SSOT
ref), §10 narrative (Next ID / entries #1-#N), §12 Implementation
outline, §13.1 Forward-check anti-pattern row, See also block. Added
explicit §10 footnote logging the second remap for future readers.

Mapping:
  27 AIF /aif              → 31
  28 AIF skills.sh         → 32
  29 Continue permissions  → 33
  30 Cline marketplace     → 34
  31 Codex MCP CLI         → 35
  32 Roo Code installer    → 36

Operator-side kickoff (`.claude/orchestrator-prompts/wave-5-tool-bootstrapping/orchestrator-kickoff.md`,
gitignored) updated in lockstep: m2 Windsurf ID #33#37 (next-free
after new range); SCOPE block acknowledges «#27-#30 AIF Handoff
occupied»; ATTN STOP rule baseline updated to existing entries #1-#30.

Closes readiness-review M-1 (research §10 not yet edited for the new
collision) and addresses root cause of BLOCKER B-1. Push + CI-green on
HEAD is the remaining operator action before B-1 is fully closed.

Non-touched: existing SSOT entries #1-#30 (Wave 7 #16-#26 + AIF Handoff
#27-#30); AIF /aif row's «entry #8 cadence» reference unchanged. Doc
errata only — not a capability commit per CLAUDE.md gate; not a §1.7
rule extension.

Prior-art: skipped — doc errata only (second SSOT-ID remap parallel to d7a6c87 precedent), no new capability per CLAUDE.md gate; resolves readiness-review BLOCKER B-1 + MAJOR M-1 (2026-05-11) on `fix/wave-5-readiness-revise` umbrella.
artyhoo added a commit that referenced this pull request May 21, 2026
…ede #81), 3 branches + visible marker (#89)

#81 (cfa28a3, on main) delivered the recap INSTRUCTION via systemMessage, which
is user-only and never reaches the model — verified dual-channel (CC hooks docs
Stop decision-control: "reason must be provided for Claude to know how to proceed"
+ WebSearch). So #81's recap prompt was never seen by the model. d695ac5/redesign
correctly use `reason` (injected; agent gets one more turn). This merges best-of-all-three:

- reason-delivery (model receives) + short systemMessage human note   [supersede #81 delivery bug]
- forced visible «## 🟢 Простыми словами» header                      [from #81 — human scannability]
- session-goal anchor (aiTitle→first-msg fallback) + 3-way drift verdict + recommendation-first  [from redesign]
- 3 branches incl. combined long+question (keeps recommendation-first when a fork is on the table)
- stateless-fire preserved (no /tmp, no aggregation/isMeta)

Tested 2026-05-21: bash -n; branches A/B/C; silent on bare tool_use; AskUserQuestion→B;
anchor aiTitle-primary + first-user-msg fallback + graceful default; §1.8 skip smoke-test exit 0.

Prior-art: skipped — modifies existing <50-LOC-logic CC hook (delivery-channel merge), no new capability per dual-implementation-discipline §2(iv); SSOT #8/#9/#20 cover the anchoring/hook family.
@artyhoo
artyhoo deleted the docs/phase-8-entry-research branch May 22, 2026 18:09
artyhoo added a commit that referenced this pull request May 26, 2026
Maintainer correction 2026-05-26 caught 4 invariant-level violations in
first draft of B3 sub-mechanism:

1. CI cast as primary evaluator (violates README CI=last-resort invariant)
2. Literal "halt-on-first-CI-red" despite REVISE=3 in same kickoff row
3. False claim "companion-projects only in memory" (§1.13 codified 2026-05-22)
4. "Oracle via subagent = expensive/complex" while session was using it for
   cold-review (91k tokens / 3 min / caught 2 MAJOR)

All four are nested T-traps on self (T3 + T16 + T11/T13 + recursive-self-
application gap). Cold-review subagent missed all four because its prompt
was structural-check, not invariant-check.

Patch changes:
- §0: add T-BA-E #ci-as-primary-evaluator trap (specialisation of T3+T16);
  enumerate it alongside existing T-BA-D #bundle-name-match
- §3 B3 table: add 4 new candidates with corrected scoring —
  oh-my-openagent (#8, ADOPT three-attempt+Oracle protocol, ~95% match,
  EXACT problem class), Aider --auto-test (#9, ADOPT success-path,
  ~85%), OpenHands AgentController (#10, ADOPT VOCABULARY + REFERENCE,
  ~80%), Claude Task Master autopilot (#11, ADOPT pause-not-halt
  semantics, ~80%); re-score Anthropic evaluator-optimizer ~40%→~90%
  with reframe (Oracle subagent IS our evaluator-LLM, not CI)
- §5 B3 verdict: rewrite from "ADOPT SDD + vocab from Argo/Anthropic" to
  ADOPT composition of 4 production patterns (oh-my-openagent +
  Aider + Task Master + Anthropic evaluator-optimizer + Superpowers SDD
  reviewer-loop); BUILD-delta ≤130 LOC (per-item-in-bundle wrapper +
  Oracle prompt template + bash glue), only what no upstream provides
- §7 forward-check: explicit T-BA-E listing + meta-insight section
  acknowledging this R-phase demonstrated the Oracle layer is missing in
  our own workflow (not just for B3); the very pattern this patch
  recommends building for B3 is the pattern this patch itself needed
  for its own discipline check

Companion memory codified:
- feedback_oracle_via_subagent_reuse_existing.md (4-checker before BUILD)
- feedback_cold_review_must_check_core_invariants.md (invariant-check
  template for future cold-review subagent prompts)
- feedback_b3_revise_not_halt_on_ci_red.md (REVISE semantics correction)
- feedback_ai_doc_research_priority_pool.md broadened scope (added
  runtime side; corrected oh-my-openagent canonical URL)

Research-only: zero SSOT row mutations, no helpers shipped. Per-sub-mech
verdict unchanged for B1/B2; B3 verdict substantially strengthened by
prior-art composition. Stage 2 maintainer GO/DEFER/DROP decision unblocks
Stage 3 I-phase.

Prior-art: skipped — research-patch only, no capability commit
artyhoo added a commit that referenced this pull request Jul 5, 2026
/#921) (#923)

## Summary

A real `git push` from a consumer install is hard-blocked by 8 checks in `packages/core/hooks/pre-push.ts` that reference **maintainer-only paths** a consumer never receives (`install.sh` ships only `packages/core/{hooks,eslint-rules}`). The absent paths make vitest exit 1, tsx `ERR_MODULE_NOT_FOUND`, npm `ENOENT`, or a raw `ENOENT` bubble → "pre-push hook crashed". `check:shields-up` never caught this — it verifies the hook is *wired*, not that the check chain *runs*; only a real push surfaces it. This wraps each maintainer-only section in the same `existsSync` consumer-skip guard its sibling sections (3b–3f/4b) already use. Closes #920, #921.

## Changes

- **`packages/core/hooks/pre-push.ts`** — 8 consumer-skip guards:
  - #1 §3 self-test → guard on `packages/core/audit-self/audit-ai-docs.test.ts`
  - #2 §4 render drift → guard on `packages/core/render/render-rules.ts`
  - #3 §5–5d meta-tests → one `coreMetaTestsAvailable` boolean (`packages/core/package.json`) over all four sites
  - #4 §6 spec-validate → guard on `packages/core/spec-validation/validate-batch-spec.ts`
  - #6 §7 prior-art / #7 §1.7 → one `isFrameworkRepo` boolean (SSOT register presence — a framework-authoring gate unsatisfiable on a consumer)
  - #5 cmd-script-liveness / #8 guard-liveness → one `hasRulesManifest` boolean (`packages/core/manifest/rules-manifest.json`)
- **`packages/core/hooks/pre-push.consumer-layout.test.ts`** (new) — behavioral regression test (runs in CI via `test:hooks`) that spawns the real orchestrator against a fixture consumer layout (exact `install.sh` copy-list, every maintainer-only path absent) and asserts `exit 0`; paired-negative re-plants `packages/core/package.json` to prove the §5 guard is load-bearing.

### Design note (deviation from the issue's literal fix for #5/#8)

Blockers #5 and #8 share one `rules-manifest.json`-existence guard rather than the issue's "skip-loudly catch" fallback. A consumer has no manifest rules to check, so skipping is semantically correct; keying on the manifest also keeps guard-liveness's ESLint-stack import off the consumer path entirely (the pnpm-strict `@typescript-eslint/parser` non-hoist, #8) **while preserving the maintainer's loud-die-on-real-breakage** — the section still runs fully wherever the manifest exists.

### Backward sweep

Enumerated every `run()`/`requireTool()`/`import()` call site in `main()`. The only unconditional external calls left unguarded are **zizmor** (§2 — scans the rendered `.github/workflows/` that `install.sh` delivers to consumers) and **lychee** (§8 — already skip-loudly on absent binary). Both are genuinely consumer-appropriate and correctly excluded from the 8.

## Prior-art consult

- [x] The new ≥80-LOC test file is flagged as a capability commit by the LOC-based detector; it carries `Prior-art: skipped — …` per the CONTRIBUTING escape hatch for hook-flagged non-capability additions (test + reuse of the existing sibling guard pattern, no new capability).
- [x] No new capability area surfaced — the fix reuses the `existsSync` guard pattern already established at sections 3b–3f/4b; no SSOT entry added or matched.

## Test plan

- [x] `npm --prefix packages/core run test:hooks` green (48 files / 687 tests, incl. the new consumer-layout regression + paired-negative)
- [x] New consumer-layout test: POSITIVE (maintainer paths absent → exit 0) + NEGATIVE (planted `packages/core/package.json` re-engages §5) both pass
- [x] `tsc --noEmit` + `prettier --check` clean on both changed files
- [x] Pre-push hook ran on push (maintainer repo — all guards evaluate true, every section runs as before)
- [x] Branched from `origin/staging` so the 2 unrelated in-flight ahead-commits (settings/SSOT/principle-31 drift) and their pre-existing `harness-config-drift`/`rule-channel-degradation` failures are excluded — verified those 5 tests pass on this base

## §1.7 Self-discipline check (REQUIRED if PR touches discipline-bearing files)

### §1.7 Skipped: mechanical bug fix to hook enforcement code (pre-push.ts) reusing the existing sibling existsSync consumer-skip guard pattern — introduces no new discipline rule, so no forward/backward rule-scope sweep applies
artyhoo added a commit that referenced this pull request Jul 10, 2026
…ory install surface (#946)

## Summary

Retires the legacy `setup.sh` wrapper and redirects every live surface that still routed consumers (or CI) through it to the current `./setup` / `install.sh` path. Root finding (cold backward-sweep, 2026-07-10): `setup.sh:89` ran `dryguard npm install -g ai-factory` — a bare, unpinned global npm install, gate-invisible because `packages/core/hooks/checks/unpinned-tool-install.ts` scans `.github/workflows/*.yml` only.

**Verdict: retire, not fix-in-place.** Evidence:

- npm name `ai-factory` verified with a FRESH npm cache (2026-07-10): maintainer `cutcode <thecutcode@gmail.com>` = lee-to / CutCode (`gh api repos/lee-to/ai-factory` → homepage `aif.cutcode.dev`; the repo's `package.json` name is `ai-factory`), i.e. the legitimate AI Factory upstream — **no name-squat**. The version-float risk stands in full: unpinned, latest 2.17.0 published 2026-07-06, and SSOT #8 records "Velocity: FAST (AIF active 2.x; 1.x→2.x had breaking changes)".
- `setup.sh` was already ruled orphaned dead code — modular-install-fullpack `kickoff-s2` M2: "setup.sh is orphaned dead code at HEAD (nothing sources it; setup→install.sh) … do NOT revive". Its unique blocks were ported earlier: context7 → `setup.d/05-mcp.sh` (S2), tool-decisions seed → `30-templates` layer (S3, PR #724). README already declared it legacy / "will be absorbed by `./setup`".
- Fixing the pin inside a wrapper the project ruled dead would revive it against M2 and keep a second, drifting install path alive.
- The retirement was listed as a maintainer product decision in the final-quality-audit kickoff (§ "absorb legacy `setup.sh`"); this PR executes the maintainer's explicit 2026-07-10 directive to decide and act.

## Changes

- **Delete `setup.sh`** (489 lines, git mode 100644, sourced by nothing at HEAD).
- **INSTALL-FOR-AI.md** — manual-install section routes through `./setup` / `install.sh`; the mandatory `npm install -g ai-factory` prerequisite is dropped (AI Factory = optional companion; the installer creates `.ai-factory/` itself — `setup.d/30-templates.sh:13,41`); stale "`setup.sh` Step 2d" tool-bootstrap claim rewritten to the real mechanism (`30-templates` seed + `05-mcp` `.mcp.json` merge on the full path); AI-prompt step 4 now runs the installer from the project dir (cold-review finding — `install.sh:80-84` refuses to install into the package dir, so the old "from the getff package directory" wording could not work).
- **README.md** — legacy notes → retired; drops the stale "setup.sh auto-detected the stack" contrast (`./setup` auto-detects since GH #780).
- **AUDIT-CHECKLIST.md §1.5/§5.1/§5.2 + AUDIT-PROMPT.md item 19 + severity example** — audit probes now target `./setup` + `install.sh`.
- **`.github/workflows/audit-self.yml`** — the L1 dry-run idempotence step now probes `install.sh` against a minimal tmp consumer (required because install.sh refuses package-dir installs); stale comment in the react-next self-install job fixed. No tools installed by the new step; no LLM (complies with ci-tool-pinning + no-paid-llm-in-ci).
- **`.claude/skills/tool-bootstrapping/SKILL.md` §3** — context7 recursive-bootstrap contract now cites the `05-mcp` layer (was the FQA-B P3 stale "context7-via-setup.sh" contract). Repo-internal skill — the shipped twin `skills/tool-bootstrapping/SKILL.md` (the `10-skills.sh` copy source) was verified already clean of setup.sh refs, so install fingerprints are untouched.
- **`packages/core/audit-self/audit-ai-docs.ts`** — D4 warn message aligned with the shipped `.sh` wording ("re-run install.sh (auto-seeds it)"); the `.ts` is source-repo-side only (not in the install copy-list, not a synth-bundle input).

Kept deliberately (not defects): `setup.d/*` provenance comments (they document where ported code came from); closed kickoffs/research-patches (historical artifacts, read-only per ownership contract); two stale code comments (`packages/core/detector/read-manifest.ts:69`, `packages/core/audit-self/template-render.audit.ts:67` — historical refs with no consumer routing; left untouched to avoid needless shipped-bytes churn).

## Prior-art consult

- [x] The single commit is NOT a capability commit (deletion + doc/CI redirects; no new dependency, no new ≥50/80-LOC files); it carries the self-documenting skip-trailer `Prior-art: skipped — legacy-wrapper retirement + doc/CI redirects to existing ./setup path, no new capability`.
- [x] No new capability area surfaced — no new SSOT entry needed (existing #8 already records AI Factory upstream velocity; the npm-ownership verification above is operational evidence, not a new verdict).
- [x] No existing SSOT entries required a `Last reviewed` touch (no capability decision made).
- [x] context7 queries N/A — no new capability area.

## Test plan

- [x] `packages/core/audit-self/audit-ai-docs.test.ts` — 200/200 green (covers the D4 message edit).
- [x] `npm run --prefix packages/core test:principles` — green (one initial failure was env-only: this worktree lacked the node_modules symlinks; fixed per the documented worktree convention, principle 21 re-run green — unrelated to the diff).
- [x] `install.sh <stack> --dry-run` idempotence verified locally on a tmp consumer: two runs, 144 identical lines — the basis for the replaced CI step.
- [x] markdownlint (pre-commit) 0 errors; YAML parse of `audit-self.yml` OK; pre-push gate suite green on push.
- [x] Residual sweep `\bsetup\.sh\b` over live surfaces — only deliberate historical/provenance mentions remain (enumerated in the commit body).
- [x] Cold adversarial review (read-only agent, T19) on the commit: 0 BLOCKER / 0 MAJOR / 5 INFO — one INFO fixed in this PR (AI-prompt step 4); the rest pre-existing and out of scope (listed below).
- [ ] §1.7-свод lands in squash-body (`gh pr merge --squash --body "$(gh pr view <N> --json body -q .body)"`)

## Class sweep — unpinned global npm installs, whole repo

`grep -rn 'npm install -g'` excluding historical dirs:

- `setup.d/companions.manifest:20` (`@ast-grep/cli`) + `README.md:164` + `packages/core/templates/shared/AGENTS.md.template:57` — deliberate companion policy: "official installers only, no version pin" (`companion-install-principle.md §1`, reconciled with CI pinning in `ci-tool-pinning.md §4`). Not defects.
- `.github/workflows/**` — zero unpinned global installs.
- `.claude/skills/aif-doctor/SKILL.md` (`npm i -g @anthropic-ai/claude-code`, 4 sites) — operator runbook gated on explicit operator GO; different class (not consumer-routed, not CI).
- The deleted `setup.sh:89` was the only unpinned global install on a consumer-routed executable path.

## Out of scope — surfaced separately to the maintainer

1. `packages/core/templates/react-next/storybook-package-additions.json` is now fully orphaned: its only consumer was `setup.sh` (lines 225-245), so react-next consumers stopped getting the storybook package.json merge when `./setup` replaced the wrapper — a pre-existing delivery gap, not a regression of this PR.
2. `unpinned-tool-install.ts` scans `.github/workflows/*.yml` only — unpinned installs in repo shell scripts are gate-invisible (this incident's detection gap).

### §1.7 Skipped: mechanical maintenance — legacy-wrapper retirement + doc/CI redirect to the existing ./setup path; no discipline rule introduced or extended (class sweep above).
artyhoo added a commit that referenced this pull request Sep 6, 2026
…directory payload (#1597 ledger L-4b/L-4c) (#1647)

## What

`#1597` ledger addenda **L-4b + L-4c** (one scan surface) + **C4-ATTN-3**.

Since ledger L-4, `refresh_safe`'s directory arm stopped `rm -rf`'ing a SHARED payload and instead KEEPS any destination file the refresh-baseline cannot attribute to a delivery. That verb is right — deleting what we cannot prove is ours is the issue-1481 defect — but the entire kept set collapsed into ONE anonymous count line (`setup.d/lib.sh`, `_refresh_dir_payload`) that additionally asserted the one fact getff has no evidence for: `(not framework-delivered — consumer-owned)`.

Nothing else looked either. `report_getff_orphans` scans three FIXED locations, all `-maxdepth 1` (consumer root, `.getff/` top level, `.github/workflows/getff-*.yml`), and is called only from the three toolchain lanes (`setup.d/45-python.sh`, `46-cargo.sh`, `47-go.sh`). It never descends into a directory payload, and it never runs at all on the npm/ts lane that delivers `scripts/fences-fire-fixtures`. A file the framework stopped shipping before a consumer's baseline existed was therefore **unattributable forever, survived every `--refresh`, and appeared in no report anywhere** (L-4b).

**L-4c — that residue is live configuration, not inert.** `packages/core/audit-self/check-fences-fire.sh` enumerates `scripts/fences-fire-fixtures/*.manifest.json` by glob, sets its non-vacuity denominator from the count, and probes every one.

**Fix:** `_report_dir_residue` in `setup.d/lib.sh` — a sibling of `report_getff_orphans` sharing its `ORPHAN:` token so one grep finds both — called from the sweep for every kept file. Two classes, because *"getff cannot attribute this"* and *"you edited it"* are different facts:

- **no baseline entry** → named as an `⚠ ORPHAN`, with both readings spelled out (your own file, or residue of a PRIOR getff version) and the live-configuration consequence stated;
- **entry present, bytes differ** → attributable to the consumer's own edit; named quietly for review.

Report-only (J2 decisions log #8) — deletes nothing — and read-only, so `--dry-run` and the real run print the same thing.

**Why the report lives in the sweep, not in `report_getff_orphans`:** the sweep already walks exactly the payload destinations, already knows which files it could not attribute, and runs on EVERY lane. Extending `report_getff_orphans` would need a second registry of payload destinations to keep in sync with the `refresh_safe` call sites, and would still miss the npm/ts lane, which never calls it. `report_getff_orphans`'s header now points here for the directory half.

Also corrects two comments that documented the pre-L-4 wholesale `rm -rf` as still current — `report_getff_orphans`' header and the barrel-prune block in `generate_eslint_barrel` (the latter claimed a consumer file dropped into the fixtures dir «is removed on refresh regardless of this loop», which arm 1 of `tests/install-sh/refresh-dir-payload-ownership.test.sh` disproves). The stale claim is exactly why this residue class read as already-covered.

## Open question answered: is `scripts/fences-fire-fixtures/` a consumer extension point by design?

**Verdict: a DE-FACTO extension point that is nowhere DECLARED as one — which is why the L-4 keep-what-you-cannot-attribute default is the correct verb for it and `framework-exclusive` is not.**

Not declared, as the chip observed:

- `packages/core/audit-self/check-fences-fire.sh:19` and `:43` describe the dir as «copied by setup.d/40-configs.sh» and say nothing about consumer authorship; `install.sh:1116` likewise.
- The only authoring instruction anywhere is framework-side: `.claude/orchestrator-prompts/install-self-verification/kickoff.md:40` tells the FRAMEWORK to ship curated bad/good pairs under `packages/core/audit-self/fixtures/fences-fire/` and copy the subtree out. Search run: `grep -rn 'fences-fire-fixtures' --include='*.md' .` → 3 hits, none consumer-facing.

De-facto, three shipped mechanisms make consumer content there real and supported:

1. the Layer-3 whole-dir escape `scripts/fences-fire-fixtures.override.md` (`setup.d/lib.sh`, `refresh_safe` docstring + the barrel-prune guard) — a signal that exists only because someone may own content in that dir;
2. arm 1 of `tests/install-sh/refresh-dir-payload-ownership.test.sh` pins behaviourally that a consumer-authored file under this exact payload SURVIVES `--refresh`;
3. `check-fences-fire.sh` enumerates by glob, not by an allowlist — any consumer-added triple IS probed, so the dir functions as an extension point whether or not it was designed as one.

Consequence, and the reason `install.sh:1117` is left alone: flipping it to `framework-exclusive` would delete BOTH classes indiscriminately — a consumer's own fixtures and the residue — with no rebuild-from-durable-home equivalent (the one declared-exclusive destination, `.getff/astgrep-rules`, is re-assembled every pass by `_py_join_researched_rules` from `.getff/rules-research`; nothing plays that role here). Loud naming is the honest closure for a dir whose ownership getff genuinely cannot resolve.

**Falsifier:** wrong if a shipped consumer-facing doc does declare the dir writable (searched above, absent), or if the barrel prune already deleted consumer files — it does not: its loop iterates `$PKG_ROOT/packages/core/audit-self/fixtures/fences-fire/*.manifest.json`, the FRAMEWORK source, so it can only ever target framework stems.

## Failure scenarios

**Failure-scenario:** (L-4b) A consumer installed getff at v1, which delivered `scripts/fences-fire-fixtures/legacy-fence.{manifest.json,bad.ts,good.ts}`; the refresh-baseline manifest post-dates that install, so it holds no entry for those paths. getff v2 stops shipping that fixture. The consumer runs `install.sh --refresh`. The sweep cannot attribute the three files, keeps them (correct), and prints `· <dir>: 3 file(s) kept (not framework-delivered — consumer-owned)` — an aggregate that names nothing and asserts consumer ownership getff has no evidence for. The files are unattributable forever and appear in no report on any lane, so nobody can ever trace where they came from.

**Failure-scenario:** (L-4c) The same stale `legacy-fence.manifest.json` is not inert. `scripts/check-fences-fire.sh` globs `*.manifest.json`, so on the very next gate run it enumerates 3 fixtures instead of 2, sets the non-vacuity denominator to 3, and probes the stale one; its `rule-id` is no longer in the consumer's barrel, `linter.verify` throws, and the consumer's own gate exits 1 on a fixture that no longer exists upstream and that no `--refresh` will ever remove or name.

**Failure-scenario:** (C4-ATTN-3) The new in-payload naming fires on a shared payload on every lane, so a false positive there would print `⚠ ORPHAN … remove it manually` next to a consumer's live files — the exact A2-6 harm arm (5) was built for, on a new surface. Without arm (7), a polyglot python+cargo consumer refreshing either lane could be told to delete healthy content and no test would go red.

## RED-first proof

Pre-fix, on a real mktemp ts consumer (`install.sh ts-server`, stale triple planted with no baseline entry, then `install.sh --refresh`):

```
--- any mention of the stale stem in the refresh output:
(NONE — silent)
--- any ORPHAN line:
(none)
--- the anonymous kept line(s):
111:  · /var/folders/.../scripts/fences-fire-fixtures: 3 file(s) kept (not framework-delivered — consumer-owned)
--- survives?
getff-legacy-fence.bad.ts
getff-legacy-fence.good.ts
getff-legacy-fence.manifest.json
```

L-4c measured on that same consumer, running its OWN installed gate (`scripts/check-fences-fire.sh`) with deps resolvable:

```
▶ check-fences-fire: probing 3 fence(s) from /var/folders/.../scripts/fences-fire-fixtures
  ✗ [getff-legacy-fence] probe failed (rc=1): .../eslint/lib/config/config.js:100|	const error = new TypeError(errorMessage);
  fixture arm  (fence FIRING proof):   manifests=3 proved=2 failed=1 skipped=0
EXIT=1
```

Two-fixture corpus, `manifests=3`, denominator inflated, gate RED — a stale file the refresh keeps and never names is live configuration.

Post-fix, same script, same plant:

```
  ⚠ ORPHAN: scripts/fences-fire-fixtures/getff-legacy-fence.manifest.json sits inside the getff-delivered payload scripts/fences-fire-fixtures, is not in the current template set, and has no refresh-baseline entry — getff cannot tell your own file from residue of a PRIOR getff version.
  ⚠ ORPHAN: scripts/fences-fire-fixtures/getff-legacy-fence.good.ts sits inside …
  ⚠ ORPHAN: scripts/fences-fire-fixtures/getff-legacy-fence.bad.ts sits inside …
--- the anonymous kept line(s): (none)
--- survives? getff-legacy-fence.{bad.ts,good.ts,manifest.json}
```

Named loudly, still present — the report-only contract holds.

The new test arm reproduces this at the suite level. Against the pre-fix `setup.d/lib.sh` (restored via `git show HEAD:setup.d/lib.sh`):

```
✗ (6) only 0/3 in-payload orphans named — a stale file survives unnamed (C4 violation):
PASS=24 FAIL=1
```

## Test arms

`tests/install-sh/lane-orphan-residue.test.sh` — arms **1-5 byte-unchanged** (only the file header gained a correction note and the new arms' descriptions). Two arms added:

- **(6)** the L-4b/L-4c contract on a real ts consumer, with its **paired negative run FIRST on the same tree**: a clean `--refresh` must emit ZERO `ORPHAN:` lines *and* must demonstrably have reached the payload (so the zero is a verdict, not vacancy); then the plant → all 3 named, all 3 still present, and the planted triple is the ONLY thing reported.
- **(7) C4-ATTN-3** — a cargo+python polyglot control (the lane pair arm 5 does not cover): both lanes refreshed on a clean tree, ZERO orphan lines from either.

## Fidelity verdict

FIDELITY: skipped — no PR-body-vs-diff fidelity delta to adjudicate; this PR is a single behavioural fix to the existing refresh sweep plus its test arms, and every claim above is backed by quoted command output from this branch.

### §1.7 Forward-check applied

- `.claude/rules/attention-is-not-a-mechanism.md` §1 — the closure is NOT «a reviewer will read the log». The detection layer is the deterministic sweep itself (`setup.d/lib.sh:851` — `_refresh_dir_payload`, the `unattributable` branch), and the claim that it fires is pinned by a paired-negative gate arm at `tests/install-sh/lane-orphan-residue.test.sh:266` — a named test, not attention. The printed line is the *report* the gate proves exists, per the report-only contract (J2 decisions log #8), not the mechanism.
- `.claude/rules/no-paid-llm-in-ci.md` — pure bash + an existing shell test file already wired at `.github/workflows/audit-self.yml:973`; zero API calls added.
- `.claude/rules/build-first-reuse-default.md` §1 — not a capability commit under the `CLAUDE.md` definition: no new dependency, no new file anywhere under `packages/`, no new module. The verb reused is `refresh_safe`'s own sweep; nothing new is built beside it.
- `.claude/rules/language-discipline.md` — all machinery and operator-visible installer output in English.

### §1.7 Backward-check applied

- Sibling sweep over every other consumer of the same L-4 sweep semantics: `tests/install-sh/refresh-dir-payload-ownership.test.sh` (17/17), `refresh-safe-dir-payload.test.sh` (7/7), `refresh-covers-full-delivery.test.sh` (21/21), `refresh-regenerates-barrel.test.sh` (10/10) — all green, so the added naming changes no ownership decision, only what is printed.
- Two stale in-repo claims of the pre-L-4 `rm -rf` semantics found and corrected in the same commit: `setup.d/lib.sh` `report_getff_orphans` header («Directory payloads … already swept wholesale by refresh_safe … the #873 rm-rf-replace branch») and the barrel-prune block in `generate_eslint_barrel` («a consumer file dropped into that dir WITHOUT the override is removed on refresh regardless of this loop»). Both were false as of ledger L-4 and are the reason the residue class read as covered. Same class flagged in the test header at `tests/install-sh/lane-orphan-residue.test.sh:12`.
- Collateral risk from re-using the `ORPHAN:` token: arms (4), (5-go), (5-py), (5-true) all assert ZERO or exactly-one orphan lines on trees the new code path also walks — all still green (25/25), and arm (7) extends that control to the cargo+python pair.

Prior-art: skipped — behavioural fix plus test arms on the existing refresh sweep; no new dependency, no new module, no new capability surface added by this PR.

## Gates

```
bash tests/install-sh/lane-orphan-residue.test.sh          → PASS=25 FAIL=0   (pre-fix baseline: PASS=15 FAIL=0; new arms RED on pre-fix lib.sh: PASS=24 FAIL=1)
bash tests/install-sh/refresh-dir-payload-ownership.test.sh → PASS=17 FAIL=0
bash tests/install-sh/refresh-safe-dir-payload.test.sh      → PASS=7  FAIL=0
bash tests/install-sh/refresh-covers-full-delivery.test.sh  → PASS=21 FAIL=0
bash tests/install-sh/refresh-regenerates-barrel.test.sh    → PASS=10 FAIL=0 SKIP=0
shellcheck --exclude=SC2034,SC2016,SC2317 setup.d/*.sh install.sh packages/core/audit-self/{pre-merge-local,ci-available-probe}.sh  → clean (default severity)
SNAPSHOT_MODE=compare bash tests/install-sh/snapshot.sh     → 15 pass / 0 fail
bash scripts/build-getff-dist.sh --check                    → MANIFEST in sync (1064 files)
.husky/pre-push                                             → green on the pushed head
```

Portability: bash 3.2 / BSD — no GNU-only flags, no `sed` alternation, no multi-line `awk -v`; the new emitter is `echo` + POSIX `${var#prefix}` only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant