From 4bb29544a272ae95fd373c87fafbdfba580cb827 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:51:19 -0600 Subject: [PATCH] =?UTF-8?q?feat(phase-33):=20A3=20the=20with/without-SDK?= =?UTF-8?q?=20dual=20=E2=80=94=20NO=5FSDK=3D1=20knob=20(skips=20the=20elev?= =?UTF-8?q?en=20psyq=5Fintegrate=20rewrites=20AND=20the=20-T=20externals?= =?UTF-8?q?=20fragments),=20make=20sdk-dual=20(refuses=20without=20all=201?= =?UTF-8?q?1=20SDK=20object=20dirs;=20WITH=20=E2=86=92=20extract=20?= =?UTF-8?q?=E2=86=92=20rm=20build/psyq=20=E2=86=92=20NO=5FSDK=3D1=20?= =?UTF-8?q?=E2=86=92=20extract=20=E2=86=92=20WITH=20restore;=20map=20asser?= =?UTF-8?q?tions;=20both=20legs=20=3D=3D=20config/check.us.sha),=20wired?= =?UTF-8?q?=20into=20tools-health=20with=20a=20[skip]=20when=20the=20SDK?= =?UTF-8?q?=20is=20absent;=20proven:=20main=20143dbb89=E2=80=A6=20byte-ide?= =?UTF-8?q?ntical=20WITH=20(1,288=20psyq=20objects)=20and=20WITHOUT=20(12?= =?UTF-8?q?=20libcd1=20stub=20tiles),=2028=20s=20wall;=20SETUP=20P33=20A3?= =?UTF-8?q?=20(R21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 69 +++++++++++++++++++++++++++++++++++-- docs/SETUP.md | 14 ++++++++ phase-ends/CURRENT_PHASE.md | 53 +++++++++++++++++----------- 3 files changed, 114 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index 5db845ce2..1c6d92c04 100644 --- a/Makefile +++ b/Makefile @@ -144,7 +144,7 @@ CC1_SMOKE_FLAGS := -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-lin BINUTILS_WARN_MAJOR := 2 BINUTILS_WARN_MINOR := 38 -.PHONY: help check-env extract build check expected clean report sig-refresh sig-overlays sig-resident build-all check-all audit-corpus audit-cdecl audit-binaries audit-text-sources audit-digest audit-frontier tools-health +.PHONY: help check-env extract build check expected clean report sig-refresh sig-overlays sig-resident sig-main sdk-dual build-all check-all audit-corpus audit-cdecl audit-binaries audit-text-sources audit-digest audit-frontier tools-health # ----------------------------------------------------------------------------- help: @@ -264,6 +264,13 @@ tools-health: # P33 A2: main's build-derived game-code sig — the one the fleet digest weighs main by. Regenerated # here for the R51 reason: progress.py prefers it, so a stale copy would be a stale denominator. $(MAKE) --no-print-directory sig-main + # P33 A3: the with/without-SDK dual (contract §1.2). Skipped, loudly, on a machine without the SDK + # objects — there every build already IS the without leg, and running it twice would prove nothing. + if [ -d "$(LIBCD_ELF)" ] && [ -d "$(LIBPAD_ELF)" ]; then + $(MAKE) --no-print-directory sdk-dual + else + echo "[skip] sdk-dual: no SDK objects on this machine — the default build IS the no-SDK leg" + fi $(MAKE) --no-print-directory audit-corpus $(MAKE) --no-print-directory audit-cdecl $(MAKE) --no-print-directory audit-binaries @@ -419,6 +426,46 @@ sig-main: echo "sig-main: no main build ($(main_MAPFILE) absent) — run 'make check BINARY=main' first; .run/sig.main.jsonl left as is" fi +# sdk-dual (P33 A3) — main byte-identical WITH the real PsyQ objects AND WITHOUT them (roadmap contract +# §1.2, the fresh-clone fallback invariant), both legs against config/check.us.sha, as ONE target: +# leg 1 extract + check -> the SDK objects linked (map must list build/psyq/libcd/) +# leg 2 extract + rm build/psyq + check NO_SDK=1 -> the INCLUDE_ASM stub tiles (map must list +# build/src/libcd1.o and no build/psyq/) +# leg 3 extract + check -> the tree back in its default (WITH) state +# `extract BINARY=main` sits between the legs because psyq_integrate rewrites build/us/SLUS_007.26.ld +# IN PLACE and extract regenerates it (the incremental trap tools/gate_main.py documents). Refuses to +# run when any SDK object dir is absent — running the same leg twice and calling it a dual is exactly +# the false green R32 forbids; a public clone's every `make check BINARY=main` already IS leg 2. +# Run as `make -j$(nproc) sdk-dual` (the sub-makes inherit the jobserver). Maps kept for the record. +# (recursively expanded — the *_ELF variables are defined further down the file) +SDK_ELF_DIRS = $(LIBCD_ELF) $(LIBGS_ELF) $(LIBETC_ELF) $(LIBGPU_ELF) $(LIBMCRD_ELF) $(LIBC2_ELF) $(LIBGTE_ELF) $(SND_ELF) $(APICARD_ELF) $(LIBAPI42_ELF) $(LIBPAD_ELF) +SDK_DUAL_DIR := .run/P33/verify +sdk-dual: + @set -e + for d in $(SDK_ELF_DIRS); do + if [ ! -d "$$d" ]; then + echo "[FAIL] sdk-dual: the WITH leg cannot run — $$d is absent (tools/fetch_psyq.sh); refusing to run one leg twice and call it a dual (R32)" + exit 1 + fi + done + mkdir -p $(SDK_DUAL_DIR) + echo "sdk-dual: leg 1 — WITH the PsyQ objects" + $(MAKE) --no-print-directory extract BINARY=main + $(MAKE) --no-print-directory check BINARY=main + cp $(main_MAPFILE) $(SDK_DUAL_DIR)/main_with_sdk.map + grep -q 'build/psyq/libcd/' $(SDK_DUAL_DIR)/main_with_sdk.map || { echo "[FAIL] sdk-dual: leg 1 did not link build/psyq/libcd/ — not a WITH build"; exit 1; } + echo "sdk-dual: leg 2 — WITHOUT (NO_SDK=1, build/psyq removed)" + $(MAKE) --no-print-directory extract BINARY=main + rm -rf build/psyq + $(MAKE) --no-print-directory check BINARY=main NO_SDK=1 + cp $(main_MAPFILE) $(SDK_DUAL_DIR)/main_no_sdk.map + if grep -q 'build/psyq/' $(SDK_DUAL_DIR)/main_no_sdk.map; then echo "[FAIL] sdk-dual: leg 2 linked build/psyq/ — not a WITHOUT build"; exit 1; fi + grep -q 'build/src/libcd1.o' $(SDK_DUAL_DIR)/main_no_sdk.map || { echo "[FAIL] sdk-dual: leg 2 did not link the libcd1 stub tile"; exit 1; } + echo "sdk-dual: leg 3 — restoring the default (WITH) state" + $(MAKE) --no-print-directory extract BINARY=main + $(MAKE) --no-print-directory check BINARY=main + echo "sdk-dual: OK — main $$(cut -d' ' -f1 $(main_CHECK_SHA)) byte-identical WITH and WITHOUT the PsyQ objects (maps: $(SDK_DUAL_DIR)/main_with_sdk.map, main_no_sdk.map)" + # sig-main-oracle (P31 S77) — MAIN'S INDEPENDENT SECOND ORACLE (roadmap contract §1.3). # Distinct from `sig-main` above, which is splat-SEEDED on purpose. This one signs the ORIGINAL EXE # bytes with NO splat symbols: `--vram-base 0x8000F800` puts file offset 0 at vram (so the 0x800 @@ -560,6 +607,14 @@ CPP := $(MIPS_PREFIX)cpp # UNDEF_FUNCS / ASM_DIR / SRC_DIR joined them per-binary in Phase 10: a second binary # writes its undefined_*_auto under build// and nests its sources under /.) +# P33 A3 — NO_SDK=1 builds main from splat's INCLUDE_ASM stub tiles even when the SDK object dirs +# exist: the WITHOUT leg of the with/without-SDK dual (roadmap contract §1.2 — the fresh-clone +# fallback invariant). Skips every psyq_integrate rewrite below AND the -T externals fragments, so +# the link is exactly what a public clone without Sony's objects performs. `make sdk-dual` runs both +# legs and asserts both SHA1s; until now the WITHOUT leg was only ever exercised by hand +# (`mv .run/obj40 .run/obj40.off`) and it regressed once unnoticed (config/symbols.us.txt:248). +NO_SDK ?= + # Phase 7 (Task 2'): link the real PsyQ libcd SDK objects in place of the libcd-region asm stubs. # tools/psyq_integrate.py rewrites the splat .ld (swap stub objects -> build/psyq/libcd/*.o + NOLOAD # data placement, no carving) and emits the externals defsym fragment. Conditional on the SDK ELF @@ -879,6 +934,7 @@ $(OUT): $(OBJS) $(ASSET_OBJS) $(LD_SCRIPT) # its own stubs. NB: ifeq/endif are make directives (column 0, no tab), resolved at # parse time; with .ONESHELL the included recipe lines still run as one shell. ifeq ($(BINARY),main) +ifeq ($(NO_SDK),) # Wire in the real libcd objects (after the build objects exist — the externals discovery # trial-links the whole image). Idempotent: re-running re-derives the externals only. if [ -d "$(LIBCD_ELF)" ]; then @@ -936,8 +992,17 @@ ifeq ($(BINARY),main) else echo " (no $(LIBPAD_ELF) — libpad band blocks stay asm stubs; convert tools/psyq/lib421/LIBPAD.LIB per docs/SETUP.md)" fi +else + echo " NO_SDK=1: the PsyQ object integrations are SKIPPED — main links its INCLUDE_ASM stub tiles (the fresh-clone leg)" endif - SYMS=""; [ -f "$(LIBCD_SYMS)" ] && SYMS="-T $(LIBCD_SYMS)"; [ -f "$(LIBGS_SYMS)" ] && SYMS="$$SYMS -T $(LIBGS_SYMS)"; [ -f "$(LIBETC_SYMS)" ] && SYMS="$$SYMS -T $(LIBETC_SYMS)"; [ -f "$(LIBGPU_SYMS)" ] && SYMS="$$SYMS -T $(LIBGPU_SYMS)"; [ -f "$(LIBMCRD_SYMS)" ] && SYMS="$$SYMS -T $(LIBMCRD_SYMS)"; [ -f "$(LIBC2_SYMS)" ] && SYMS="$$SYMS -T $(LIBC2_SYMS)"; [ -f "$(LIBGTE_SYMS)" ] && SYMS="$$SYMS -T $(LIBGTE_SYMS)"; [ -f "$(SND_SYMS)" ] && SYMS="$$SYMS -T $(SND_SYMS)"; [ -f "$(APICARD_SYMS)" ] && SYMS="$$SYMS -T $(APICARD_SYMS)"; [ -f "$(LIBAPI42_SYMS)" ] && SYMS="$$SYMS -T $(LIBAPI42_SYMS)"; [ -f "$(LIBPAD_SYMS)" ] && SYMS="$$SYMS -T $(LIBPAD_SYMS)" +endif + # The externals fragments belong to the SDK-object link only: under NO_SDK the stale files left by a + # previous WITH build must not be picked up (they would defsym names the stub tiles already carry). + SYMS="" + if [ -z "$(NO_SDK)" ]; then + [ -f "$(LIBCD_SYMS)" ] && SYMS="-T $(LIBCD_SYMS)"; [ -f "$(LIBGS_SYMS)" ] && SYMS="$$SYMS -T $(LIBGS_SYMS)"; [ -f "$(LIBETC_SYMS)" ] && SYMS="$$SYMS -T $(LIBETC_SYMS)"; [ -f "$(LIBGPU_SYMS)" ] && SYMS="$$SYMS -T $(LIBGPU_SYMS)"; [ -f "$(LIBMCRD_SYMS)" ] && SYMS="$$SYMS -T $(LIBMCRD_SYMS)"; [ -f "$(LIBC2_SYMS)" ] && SYMS="$$SYMS -T $(LIBC2_SYMS)"; [ -f "$(LIBGTE_SYMS)" ] && SYMS="$$SYMS -T $(LIBGTE_SYMS)"; [ -f "$(SND_SYMS)" ] && SYMS="$$SYMS -T $(SND_SYMS)"; [ -f "$(APICARD_SYMS)" ] && SYMS="$$SYMS -T $(APICARD_SYMS)"; [ -f "$(LIBAPI42_SYMS)" ] && SYMS="$$SYMS -T $(LIBAPI42_SYMS)"; [ -f "$(LIBPAD_SYMS)" ] && SYMS="$$SYMS -T $(LIBPAD_SYMS)" + true + fi echo " LD $(ELF)" $(LD) -T $(LD_SCRIPT) -T $(UNDEF_SYMS) -T $(UNDEF_FUNCS) $$SYMS --no-check-sections -Map $(MAPFILE) -o $(ELF) echo " OBJCOPY $@" diff --git a/docs/SETUP.md b/docs/SETUP.md index 2ba510ad8..b9edd8024 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -1069,6 +1069,20 @@ fills fast). Nothing is leaking — but the host does not get the memory back on * `gate_main` **REFUSES a draft containing its own `INCLUDE_ASM`** (substituting it restores the stub, so the build passes for free and the function counts as banked), and counts banks from the SOURCE. +### P33 A3 (S86, 2026-09-06) — the with/without-SDK dual as a first-class target +- **`NO_SDK=1`** (Makefile knob): `make check BINARY=main NO_SDK=1` skips every `psyq_integrate` rewrite AND the `-T` + externals fragments, so main links the pristine splat script against its INCLUDE_ASM stub tiles — exactly the link a + public clone without Sony's objects performs (contract §1.2, the fresh-clone fallback invariant). Until now that leg + was exercised only by hand (`mv .run/obj40 .run/obj40.off`) and it regressed once unnoticed (`config/symbols.us.txt:248`, + the `CdReadyCallback` name). +- **`make -j$(nproc) sdk-dual`**: leg 1 extract + check WITH the objects (map must list `build/psyq/libcd/`) → leg 2 extract + + `rm -rf build/psyq` + check `NO_SDK=1` (map must list `build/src/libcd1.o` and no `build/psyq/`) → leg 3 extract + check + (the tree back in its default WITH state); both legs against `config/check.us.sha`; maps kept in `.run/P33/verify/`. + The extracts between legs exist because `psyq_integrate` rewrites `build/us/SLUS_007.26.ld` in place (the incremental + trap `tools/gate_main.py` documents). REFUSES to run when any of the 11 SDK object dirs is absent (one leg twice is not a + dual, R32); `tools-health` calls it after `sig-main`, or prints `[skip] sdk-dual: no SDK objects on this machine` — there + the default build already IS the without leg. + ### P33 A2 (S86, 2026-09-06) — reporting instruments made regenerable and honest - `tools/main_seed_ends.py [--map build/us/SLUS_007.26.map] [--out .run/seeds.main.txt]` — main's game-code function boundaries DERIVED from the build (link-map `.text` sections × each object's `nm` symbols; LINKED subsegs and diff --git a/phase-ends/CURRENT_PHASE.md b/phase-ends/CURRENT_PHASE.md index 86df7ead1..fd8f8cff9 100644 --- a/phase-ends/CURRENT_PHASE.md +++ b/phase-ends/CURRENT_PHASE.md @@ -47,7 +47,7 @@ one-time snapshot, `CLAUDE.md` gains "never `git clean -x`" (R20 amendment propo - [x] **A1** Governance commit (R65–R73 in DIGEST §3; this file; the harness list) — xHigh — `commit:4012` - [x] **A2** Reporting-instrument fixes (pad-tail literal → derived; backlog LINKED-aware prune; `sig-main` build-derived + `progress.py` loud-fail) — xHigh, R39 controls — see Log 2026-09-06 A2 -- [ ] **A3** `NO_SDK` knob + `make sdk-dual` (+ tools-health wiring, `[skip]` without SDK dirs) — Max +- [x] **A3** `NO_SDK` knob + `make sdk-dual` (+ tools-health wiring, `[skip]` without SDK dirs) — Max — see Log 2026-09-06 A3 - [ ] **A4** `tools/family_hseq.py` regen — Low - [ ] **B1** `make disc-extract` (extract.py `--expect-manifest` / `--allow-missing-audio`; check-env; `.gitignore` re-tightened; the 4 splat preset headers tracked; `clean` fixed) — Max @@ -111,20 +111,33 @@ Mid-phase rules check after every 4 completed tasks (P6). Commit banked artifact 13,488,497 → **13,492,113**, distinct 5,816,589 → **5,820,205**, all still 100%. `make report BINARY=main` EXIT=0 (`.run/P33/a2_report.log`: lint OK, dedup-check 2220/0); `make audit-digest` OK; `docs/duplicates*.md` regenerated (main's table now game-code-only). SETUP §6.8 + a P33 A2 section (R21). +- **2026-09-06 (S86) — A3 (the with/without-SDK dual).** Makefile: `NO_SDK ?=` knob — under `NO_SDK=1` the eleven + `psyq_integrate` rewrites are skipped (nested `ifeq`) AND the `-T` externals fragments are not collected (a stale + `build/psyq/*_externals.ld` from a WITH build must not leak into the WITHOUT link); `sdk-dual` target (refuses unless all 11 + SDK object dirs exist; leg 1 WITH → extract → `rm -rf build/psyq` → leg 2 `NO_SDK=1` → extract → leg 3 WITH restore; map + assertions each leg; both legs against `config/check.us.sha`); wired into `tools-health` after `sig-main` with a `[skip]` + when the SDK dirs are absent; `.PHONY` += `sig-main sdk-dual`. **Run:** `make -j$(nproc) sdk-dual` → `sdk-dual: OK — main + 143dbb89… byte-identical WITH and WITHOUT the PsyQ objects`, EXIT=0, 28 s wall (`.run/P33/a3_sdk_dual.log`); maps + `.run/P33/verify/main_with_sdk.map` (1,288 `build/psyq/` entries, 0 `libcd1` stub) and `main_no_sdk.map` (0 / 12); the live + tree is back in the WITH state (map has 1,288 psyq entries; `build/us/SLUS_007.26` sha == check.us.sha). Control for A2's + tool: `main_seed_ends.py` on both maps → identical seed files (the NO-SDK map exercises the LINKED-subseg skip: 70 objects + skipped by name, vs 268 `build/psyq/` objects by path). Dry-run controls: `make -n check BINARY=main NO_SDK=1` shows 0 + `psyq_integrate` lines and `if [ -z "1" ]` around the externals. SETUP: P33 A3 section (R21). -## 🛑 SESSION CHECKPOINT — A1 ✓ A2 ✓; NEXT = A3 (the `NO_SDK` knob + `make sdk-dual`, Max) (2026-09-06, written by session bd19e14a "S86"; SUPERSEDES the gate-1 block) +## 🛑 SESSION CHECKPOINT — A1 ✓ A2 ✓ A3 ✓; NEXT = A4 (family_hseq regen, Low) then the P6 rules check, then B1 (Max) (2026-09-06, written by session bd19e14a "S86"; SUPERSEDES the earlier blocks) ### 0. How to use this block You are a FRESH SESSION that has read `PROJECT_CONTEXT.md`, `phase-ends/DIGEST.md`, `PhaseEnd_Phase30/31/32.md` and this file, and nothing else (R64). Replay this block verbatim, state phase / done / NEXT / effort, list the rules from the digest -(R1–R73), then WAIT for Drew. **NEXT = A3** (`NO_SDK` knob + `make sdk-dual`; Max — it touches main's link recipe, a wrong -knob is a false green). If `git log -1 --format=%s` does not start with `feat(phase-33): A2`, A2's commit did not land: re-run -its controls from the Log entry before committing. +(R1–R73), then WAIT for Drew. **NEXT = A4** (`.venv/bin/python tools/family_hseq.py`, Low; then the P6 mid-phase rules +check — 4 tasks done — then **B1**, Max). If `git log -1 --format=%s` does not start with `feat(phase-33): A3`, A3's commit +did not land: re-run `make -j$(nproc) sdk-dual` (28 s) and check `.run/P33/a3_sdk_dual.log` ends `sdk-dual: OK` before committing. ### 1. Where we are **Phase 33 — 100% verification + the public flip + Gen2 exit.** Gate 1 approved 2026-09-06 (plan mode, Max). R65–R73 ratified at -gate 1 (DIGEST §3). **Done: A1 (`commit:4012`), A2 (see the Log — the corrected main denominator 45,150 is now the published -number; `.run/sig.main.jsonl` is the sig main is weighed by; `tools/main_seed_ends.py` is new).** The approved plan is VERBATIM +gate 1 (DIGEST §3). **Done: A1 (`commit:4012`), A2 (`commit:4013` — the corrected main denominator 45,150 is now the published +number; `.run/sig.main.jsonl` is the sig main is weighed by; `tools/main_seed_ends.py` is new), A3 (the `NO_SDK` knob + +`make sdk-dual`, proven 28 s: both legs `143dbb89…`).** The approved plan is VERBATIM at the end of this file — read its Blocks A–G for every task's files, commands and verification; the "Execution order and why" section is the sequence; the "Honest scale" table is the budget (≈17–21 sessions; the flip precedes the writing so later items can be cut). Harness tasks: #1 A1 done, #2 A2 done, #3 A3 next … #41 G2. @@ -187,26 +200,26 @@ items can be cut). Harness tasks: #1 A1 done, #2 A2 done, #3 A3 next … #41 G2. ### 3. NEXT — in order 0. **Preflight:** `git status --short | grep -v ghidra/` (empty) · `git log -1 --format='%h %s'` · `df -h ~` (≈13 GB free) · `.venv/bin/python -c 'import splat'` · `ls build/us/SLUS_007.26.map` (main is built; `make check BINARY=main` if not). -1. **A3** per the plan's Block A: `NO_SDK ?=` knob in the Makefile; the 11 `psyq_integrate` calls and the `-T` externals - fragments skipped when set; `sdk-dual` target (refuses without every SDK ELF dir — they exist on this machine: - `.run/obj40/{libcd,libetc,libgpu,libmcrd,libc2,libgte,libgs_used,snd_used}`, `.run/obj42/{apicard_used,libapi42,libpad421}` - — check with `ls`); runs `extract BINARY=main` between legs; asserts the WITH map contains `build/psyq/libcd/` and the NO-SDK - map contains `build/src/libcd1.o` and no `build/psyq/`; both legs == `config/check.us.sha`; add to `tools-health` after - `sig-main` with a `[skip]` when no SDK dirs exist. Gotcha: psyq_integrate rewrites `build/us/SLUS_007.26.ld` in place, and - `make extract BINARY=main` regenerates it and deletes main's objects (Makefile:706-712) — that is why the legs are separated - by an extract. After the NO-SDK leg, run `make check BINARY=main` once more WITHOUT `NO_SDK` so the tree's main build is - the WITH-SDK one again (the map is what `sig-main` reads; either leg gives the same game-code seeds). Commit - "feat(phase-33): A3 …" with a Log line + this block refreshed. -2. **A4** `.venv/bin/python tools/family_hseq.py` (clears the audit-binaries `[warn]`), commit. Then **B1** (Max). +1. **A4** `.venv/bin/python tools/family_hseq.py` (clears the audit-binaries `[warn] .run/family_hseq.json is missing 6 + onboarded overlay(s)`); confirm with `.venv/bin/python tools/audit_binaries.py 2>&1 | grep -c '\[warn\]'` → 0; commit + "chore(phase-33): A4 …" (the json is gitignored — the commit is the Log line + this block). Then the **P6 rules check** + (re-read CLAUDE.md's rules section; state "Rules check — re-read complete. Continuing with B1"). +2. **B1** (Max) per the plan's Block B: `tools/bfm_extract/extract.py` gains `--expect-manifest` + `--allow-missing-audio`; + `make disc-extract`; `extract`/`extract-all`/`check-env`/`help`/`clean` changes; `.gitignore` re-tightened; the 4 splat + preset headers tracked. Gotchas: `extract.py:379-381` overwrites the manifest on every run (compare, never write); the + committed manifest has 1,801 rows incl. 3 `.DA` audio files from Tracks 2–4; `disks/` here holds all 4 tracks + cue; the + verification moves `extracted/` aside (`mv extracted .run/extracted.off`) — put it back if anything fails. 3. After every task: tick the box, add a Log line, refresh this checkpoint block (the 🛑 block is the ONLY in-phase context - the next session inherits), commit. Mid-phase rules check after A4 (4 tasks done). + the next session inherits), commit. ### 4. Files this session touched A1: `phase-ends/DIGEST.md`, `phase-ends/CURRENT_PHASE.md`. A2: `tools/main_seed_ends.py` (new), `Makefile` (`sig-main` rewritten; `tools-health` + `report` wiring), `tools/progress.py` (sig selection + SystemExit + `main_oracle_line` + the MAIN header line), `tools/backlog.py` (`linked_closed`), `tools/dup_report.py` (main sig path), `docs/SETUP.md` (§6.8 + P33 A2 section), regenerated `docs/progress.fleet.md`, `docs/duplicates.md`, `docs/duplicates.cross.md`, `docs/backlog.md`, -`.run/backlog.jsonl`. Scratch: `.run/public_audit/` (the history inventory), `.run/P33/a2_report.log`. +`.run/backlog.jsonl`. A3: `Makefile` (`NO_SDK`, `sdk-dual`, tools-health wiring, `.PHONY`), `docs/SETUP.md` (P33 A3 section). +Scratch: `.run/public_audit/` (the history inventory), `.run/P33/a2_report.log`, `.run/P33/a3_sdk_dual.log`, +`.run/P33/verify/main_{with_sdk,no_sdk}.map` (to be allowlisted by A5). The plan file: `~/.claude/plans/max-effort-set-plan-twinkling-moonbeam.md` (copied below). ---