Changelog

All notable changes to TAKEOVER are recorded here. Format is informal:
release version, date, then a flat list of fixes and additions.

v2.0.4 (2026-05-18): v2.0.3 QG hardening pass

Hardening release closing every Fatal, Significant, and Minor finding
from the v2.0.3 follow-up quality-gate sweep (1 Fatal regression, 4
Significant, 13 Minor). T1 cell-buffer parity stays byte-identical
to the v1.2.1 goldens on rows 0..23 for all 5 production scenarios.
115 unit tests pass.

Fixed (production)
- Menu footer no longer overwritten by the status-bar tick. The
  card-gallery menu paints its persistent footer at row 24
  ("barelybooting.com/takeover" plus the version stamp). The v2.0.3
  menu-loop fix routed the keep-alive wait through shell_wait_ms,
  which in turn unconditionally calls shell_status_bar_tick(); the
  tick paints row 24 with rotating status hints and the F9/F10 mute
  indicator. After ~3 seconds in the menu, the footer was clobbered
  by the status bar; an F9/F10 toggle overwrote the version stamp.
  v2.0.4 introduces shell_wait_ms_no_status(ms) in
  src/shell/audio.c, identical to shell_wait_ms minus the
  shell_status_bar_tick call. The menu loop, the Options sub-menu
  loop, and menu_status_flash's 600 ms wait all route through
  the no-status variant. Scenario code keeps using shell_wait_ms
  (the status bar owns row 24 during a scenario). (B-F1')
- scrollback_panic_cleanup gates adlib_shutdown on
  g_hw.adlib. The other two call sites in src/main.c already
  gate the shutdown call on detected hardware; the scrollback panic
  path was inconsistent. (B-F2')
- engine_request_input_set_max_len upper clamp tightened from
  41 to 40. The only production caller passes 40; the previous
  "41" relic of v1.2.1's 41-byte latch buffer was dead. The clamp
  now matches the externally visible "max 40 characters" contract.
  (A-M2)
- MENU help panel trimmed from 11 lines to 10. The Phase 9
  overlay-render loop paints content at rows hy+1..hy+10 inside a
  12-row box; the 11-row MENU panel painted its last line over the
  bottom border. Dropped the dedicated Home/End rows (arrow nav
  covers the same surface for a 5-card gallery). (B-S5')
- F1 inside Options now opens a dedicated OPTIONS help panel.
  v2.0.3 routed F1 in Options to the MENU panel as a stopgap; the
  MENU panel documents Left/Right/Home/End/1..5/F2 which DO NOTHING
  inside the Options dialog. v2.0.4 adds SHELL_INPUT_STATE_OPTIONS
  to the input-state enum and a matching g_help_panel_options
  panel describing Up/Dn/Space/Enter/Esc. (B-S6')
- DAT save's remove(TAKEOVER.OLD) is now size-gated. The
  v2.0.3 B-S1 fix introduced a "best-effort orphan clean" before
  staging the rename; the unconditional remove silently deleted
  any user file at TAKEOVER.OLD. Now the file is only removed if
  its size matches TAKEOVER_DAT_DISK_SIZE (76 bytes), i.e., it
  looks like a stash we wrote. A user's unrelated TAKEOVER.OLD is
  left alone; the subsequent rename returns ENOENT or EEXIST and
  the save still succeeds. (B-S7')

Fixed (tooling + build)
- SHELL_TU_FLOOR hoisted to tools/_shell_tu_floor.py.
  Previously duplicated in tools/dgroup-report/dgroup-report.py
  and tools/check_no_shell_emit.py. Both now import the shared
  constant; a single bump propagates to every gate. (C3)
- tools/check_shell_tag_writes.py gains a numeric
  SHELL_TU_FLOOR floor. Previously the gate only failed if
  shell_scanned == 0; a silent 21-TU regression would pass. (C4)
- Makefile clean comment updated from "30 sidecar -rt.obj
  files" to "34" to match the actual list. (C5)
- tools/build-release-zip.ps1 manifest filename now derives
  from $OutName, not $Version. -OutName x.zip -Version 2.0.3
  produced x.zip next to TAKEOVER-v2.0.3.manifest.txt (broken
  pairing); now x.zip produces x.manifest.txt. (C6)
- Makefile phase6-gate runs dat_io_check.py BEFORE
  runtests-build. Added a phase6-gate-prebuild target whose
  only dependency is $(TARGET); phase6-gate depends on
  phase6-gate-prebuild first, then runtests-build. Schema
  drift now fails the build before the expensive RUNTESTS link.
  (C7)
- tools/run-runtests.ps1 -ExcludeRow documentation clarified:
  the wrapper zeros row N in the captured .cells only; callers
  must mask the same row in the on-disk golden before computing
  SHA1. Echoed in the OK line so the contract is visible at run
  time. (C8)
- tools/__pycache__/ purged from the working tree. Already
  ignored by .gitignore; the stale .pyc was a leftover artifact.
  (C9)
- tools/run-runtests.ps1 phase-11 plan reference text updated
  in multiple locations to describe the PowerShell-only carve-out
  reality (no runtests.exe --exclude-row=N CLI flag exists in
  shipped code). (C1)
- tests/golden/README.md updated: Makefile.runtests-v0 was
  deleted in v2.0.3; the recipe now invokes wmake -f Makefile.tests
  all and uses tools/run-runtests.ps1 -ExcludeRow 24 to capture.
  (C2)

Fixed (docs)
- Roadmap scrollback geometry corrected. v2.0.3's commit
  message claimed the "roadmap section" got the 200x80 -> 200x78
  fix, but only docs/plans/2026-05-14-takeover-v2-design.md was
  updated; docs/plans/2026-05-16-takeover-v2-roadmap.md line 650
  kept the wrong dimensions. v2.0.4 lands the correction in the
  roadmap and adds a sub-phase release log section pointing at
  v2.0.1 / v2.0.2 / v2.0.3 / v2.0.4 release notes. (D-S1)
- docs/PHASE13_IRON_TEST_PROTOCOL.md zip filename references
  bumped from TAKEOVER-v2.0.zip to TAKEOVER-v2.0.4.zip;
  footer-expectation row updated from v2.0.3 to v2.0.4. (E-S1)
- TAKEOVER-v2.0.2.manifest.txt generated from the shipped
  TAKEOVER-v2.0.2.zip. v2.0, v2.0.1, v2.0.3 manifests existed;
  the v2.0.2 manifest was missing. (E-M1)
- Phase 11 plan updated in lines 609-624, 686, 729, 857, 895,
  913, 960 to describe the PowerShell-only -ExcludeRow N
  parameter reality (no runtests.exe --exclude-row=N CLI flag).
  (C1)

Verified
- T1 cell-buffer parity byte-identical vs tests/golden/*.cells
  with row 24 masked for all 5 production scenarios.
- All 115 unit tests across --unit-tests (5), --phase3-tests
  (13), --phase4-tests (9), --phase5-tests (68),
  --phase6-tests (20) pass.
- wmake phase6-gate (DGROUP zero-shell, keyword parity,
  normalize-far, tag-write allowlist + new SHELL_TU_FLOOR check,
  themes-segment, theme-state-segment, no-shell-emit, dat_io_check
  now running BEFORE the runtests link, phase6-acceptance-grep,
  RUNTESTS build presence) green.

Binary
- TAKEOVER.EXE: 147,100 bytes (v2.0.4). v2.0.3 was 146,702. Net
  delta +398 bytes: the new shell_wait_ms_no_status variant +
  shell_wait_ms_full_ex helper + g_help_panel_options panel
  table entry + dat_io.c size-check probe + version string bump
  from "v2.0.3" to "v2.0.4".
- 0 shell-side DGROUP near-data across all 22 shell translation
  units (Cerberus discipline preserved).

v2.0.3 (2026-05-18): v2.0.2 QG hardening pass

Hardening release closing every Fatal, Significant, and Minor finding
from the v2.0.2 end-to-end quality-gate sweep (10 Fatal, 25
Significant, 17 Minor; deduplicated). T1 cell-buffer parity is
byte-identical to the v1.2.1 goldens on rows 0..23 for all 5
production scenarios. 115 unit tests pass.

Fixed (production)
- Menu + Options sub-menu navigation no longer starves AdLib.
  The card-gallery menu loop and the F2 Options sub-menu loop both
  blocked on a bare scr_getkey(). With AdLib music active behind
  the menu (main.c calls adlib_init() before menu_run()), the
  bare wait starved adlib_tick() and produced an audible OPL2
  envelope dropout within ~16 ms. Both loops now use the
  while (!scr_kbhit()) shell_wait_ms(50); poll-then-getkey pattern
  matching delay.c. (B-F1, B-F2)
- Runtime theme: verb now emits the same pair + drain as
  startup. CMD_THEME previously emitted TKE_THEME_CHANGED
  alone; the shell consumer applied the new theme but the body-attr
  cell was not refreshed until the next TKE_ATTR_THEME arrived.
  CMD_THEME now mirrors engine.c:1924-1944: emits
  TKE_THEME_CHANGED then TKE_ATTR_THEME{BODY} then
  shell_pump_drain() so subsequent text emission paints with the
  new theme's body attribute. (A-S1)
- engine_load defensively wipes the event ring. Added
  events_reset() as the first line of engine_load. Idempotent
  against engine_run -> engine_reset -> events_reset (the existing
  call site at engine.c:1893); the defensive call closes a
  load + peek + skip + load corner case. (A-S2)
- CMD_PAGE now drains after emit. Parity with
  CMD_CLEAR_SCREEN's emit + drain pattern; without the drain,
  subsequent TKE_TEXT_* could queue behind the page event and
  paint into the page that PAGE was supposed to clear. (A-M3)
- CMD_REWRITE shares the scratch-write helper with
  CMD_INPUT. Replaces the inline for (...) copy into
  g_pool[POOL_REWRITE_SCRATCH] with a pool_scratch_write(expanded)
  call. Functionally equivalent (same MAX_STRING-1 truncation,
  NUL-termination at the bound); reduces accidental drift between
  the two slot producers. (A-M1)
- **scrollback_capture_* panic path resets OPL2 + cursor before
  exit(2). Both R9-3 architectural-invariant panics in
  scrollback.c (the call sites are unreachable by design)
  previously left a stuck OPL2 sustain envelope and a hidden BIOS
  cursor if they ever fired. New scrollback_panic_cleanup() helper
  calls adlib_shutdown() and restores the BIOS underscore cursor
  via INT 10h AH=01h before exit. (B-F4)
- DAT save shrinks the remove-then-rename data-loss window.
  Previous code did remove(TAKEOVER.DAT); rename(TMP, DAT); a
  failure between those two ops left no DAT on disk. New pattern
  renames the existing DAT to TAKEOVER.OLD first (atomic on FAT),
  then renames TMP to DAT. On the second rename failing, the OLD
  stash is renamed back so the previous save is preserved. (B-S1)
- tag_table_rewrite stops at early NUL in new_text. If the
  engine emits TKE_REWRITE_TAG with new_len > strlen(new_text),
  the old code painted garbage cells past the NUL. The write loop
  now bounds at c < write_len && new_text[c]; the pad-to-
  cell_count loop fills the remainder with spaces. (B-S2)
- F1 inside Options sub-menu opens the MENU help panel. Previous
  behaviour consumed-and-dropped F1 inside the Options dialog. New
  behaviour dispatches to shell_overlay_help_open(MENU) and
  repaints the Options chrome afterwards. (B-S3)
- Status-bar hint rotation period 55 ticks -> 54 ticks per the
  original Phase 11 spec (~3 sec at 18.2 Hz BIOS tick). (B-S4)
- Menu footer version string bumped from v2.0.1 to v2.0.3.
  (B-F3)

Fixed (tooling + build)
- tools/dgroup-report/dgroup-report.py SHELL_TU_FLOOR lifted
  from 7 to 22 to match the actual v2.0 shell TU inventory. A
  15-TU silent drop would have passed vacuously under the old
  floor. (C-S2)
- tools/check_no_shell_emit.py now has a --self-test flag
  with 6 positive + negative decoys (bare emit, block comment,
  line comment, string literal, similar identifier, multiline) and
  a vacuous-pass scanned-floor of 22 shell TUs. (C-S3)
- tools/build-release-zip.ps1 derives $OutName from
  $Version when the caller omits it; -Version 2.0.3 alone now
  produces TAKEOVER-v2.0.3.zip. (C-S4)
- Makefile.runtests-v0 deleted. The legacy Phase 1 harness
  collided with Makefile.tests on src\screen-rt.obj and had not
  been built since the Phase 1 baseline capture (per its own
  comments). (C-S5)
- Main Makefile clean deletes all 30 sidecar -rt.obj files.
  Previous clean only deleted screen-rt.obj + engine-rt.obj;
  28 stale sidecars survived. Each is now spelled out by name.
  (C-S6)
- achievements_copy-rt.obj preemptively added to
  Makefile.tests OBJS_RUNTESTS. Defends against the link
  breaking the day a future test exercises menu_card.c's
  achievement_copy_str reference. (C-M1)
- tools/check_themes_segment.py and
  tools/check_theme_state_segment.py both gained --self-test
  flags with 5 hand-crafted map-fragment decoys each. (C-M3)
- tools/dat_io_check.py wired into phase6-gate. Schema
  drift now fails the build rather than waiting on the iron
  smoketest. (C-M4)
- --exclude-row=N PowerShell-only carve-out documented. The
  Phase 11 plan and src/tests/status_bar_stub.c:15 previously
  described a runtests.exe --exclude-row=N CLI flag; the
  as-shipped implementation lives in tools/run-runtests.ps1 as
  -ExcludeRow N. Doc + comment now reflect reality; no CLI
  flag is landed in runtests.c. (C-S7)

Fixed (docs)
- CHANGELOG.md byte-count corrected from 146,780 to 146,782 for
  the v2.0 entry. (D-F4)
- CHANGELOG.md scrollback sizing corrected from "32 KB cells +
  1 KB index" to "31,200 bytes cells (200 rows x 78 cols x 2
  bytes) + 1,024 bytes index (256 entries x 4 bytes)". (D-S2)
- CHANGELOG.md TU count corrected from 19 to 22 for the v2.0
  entry. (D-S3)
- CHANGELOG.md v2.0 line 233 reworded: "If iron-test surfaces a
  regression, the fix lands as v2.0.1" is no longer accurate now
  that v2.0.1, v2.0.2, and v2.0.3 have all shipped. (D-M4)
- docs/PHASE13_IRON_TEST_PROTOCOL.md footer expectation
  updated from v2.0.0 to v2.0.3 at lines 52 and 104. (D-F3,
  C-S1)
- README.md Status section rewritten for v2.0.3; v2.0.1 and
  v2.0.2 added to the Previous releases list; binary-size line
  dropped (release-specific so it would just go stale again). (D-F5)
- docs/plans/2026-05-14-takeover-v2-design.md scrollback
  sizing updated at 5 locations to match shipped code: 200 rows
  x 78 cols = 31,200 bytes cells; 256 entries x 4 bytes = 1,024
  bytes index; 32,224 total. (D-F6)
- Roadmap section annotated with the three sub-phase
  quality-gate releases (5.1, 6.1, 8.1) that shipped during v2.0
  development. (D-S1)
- Em-dashes stripped from phase plans 5, 6, 8, 13. Tony's
  voice rule. 37 dashes replaced with comma-space. (D-S4)
- Phase 13 plan stale src/menu.c:191 references updated to
  src/shell/menu_card.c:138 (14 occurrences). The original file
  deleted in Phase 10. (D-M3)
- docs/RELEASE_NOTES_v2.0.1.md, _v2.0.2.md, _v2.0.3.md**
  created for parity with _v2.0.0.md. (D-S8)

Verified
- T1 cell-buffer parity byte-identical vs tests/golden/*.cells
  with row 24 masked for all 5 production scenarios.
- All 115 unit tests across --unit-tests (5), --phase3-tests
  (13), --phase4-tests (9), --phase5-tests (68),
  --phase6-tests (20) pass.
- wmake phase6-gate (DGROUP zero-shell, keyword parity,
  normalize-far, tag-write allowlist, themes-segment,
  theme-state-segment, no-shell-emit, dat_io_check,
  phase6-acceptance-grep, RUNTESTS build presence) green; every
  Python gate exercises its --self-test flag in the same gate
  invocation.

Binary
- TAKEOVER.EXE: 146,702 bytes (v2.0.3). v2.0.1 was 146,546 and
  v2.0.2 had the same bytes as v2.0.1 (test-harness-only release).
  Net delta vs v2.0.1: +156 bytes for the drain additions, scratch
  helper unification, panic cleanup helper, and version string.
- 0 shell-side DGROUP near-data across all 22 shell translation
  units (Cerberus discipline preserved).

v2.0.2 (2026-05-18): RUNTESTS medium memory model

Test-harness-only maintenance release. Production TAKEOVER.EXE
binary bytes are unchanged vs v2.0.1; only Makefile.tests and
RUNTESTS.EXE are affected.

Changed
- Makefile.tests CFLAGS bumped from -ms to -mm. Under small
  model the v2.0.1 RUNTESTS link was at 99.4% of the 64 KB _TEXT
  ceiling (65,172 of 65,536 bytes; 364 byte headroom). Any future
  shell TU would have pushed the test harness past the cap. Medium
  model splits code into per-TU FAR-addressed segments and removes
  the ceiling.
- Mixed-model link hazard avoided. Every OBJ that lands in
  RUNTESTS.EXE is rebuilt by Makefile.tests as a -rt.obj
  sidecar under -mm. Production OBJ files built by the main
  Makefile under -ms are no longer linked into RUNTESTS.EXE. The
  main Makefile is untouched.
- Link-time stack bumped from 3072 to 4096 bytes. Defensive
  against the larger medium-model frames (RETF pops 4 bytes vs
  RETN's 2).
- Version string in src/shell/menu_card.c deliberately NOT
  bumped. Production EXE bytes are unchanged, so the footer keeps
  reading v2.0.1.

Verified
- T1 cell-buffer parity byte-identical vs tests/golden/*.cells
  with row 24 masked for all 5 production scenarios (axiom,
  hushline, kestrel9, orchard, cinder).
- All 115 unit tests across --unit-tests (5), --phase3-tests
  (13), --phase4-tests (9), --phase5-tests (68),
  --phase6-tests (20) pass under the medium-model RUNTESTS.EXE.
- wmake phase6-gate (DGROUP zero-shell + keyword parity +
  normalize-far + tag-write allowlist + themes-segment + RUNTESTS
  build presence) green.
- Production TAKEOVER.EXE SHA1 unchanged from the v2.0.1 build.

This release is groundwork that unblocks v2.1+ shell TUs which would
otherwise blow the v2.0.1 64 KB cap on the test harness.

v2.0.1 (2026-05-18): Enum rename pass; T5 baseline re-capture

Maintenance release. No engine behaviour change vs v2.0.0; T1 cell
buffer parity is byte-identical to v2.0.0 across all 5 production
scenarios with row 24 masked. T5 event-trace baselines move forward
to a clean naming scheme.

Changed
- Event-kind names cleaned up. The Phase 6 V-suffix variants
  (TKE_TEXT_BEGIN_V2, TKE_TEXT_END_V2, TKE_REWRITE_TAG_V2,
  TKE_CLEAR_SCREEN_V2, TKE_CLEAR_REGION_V2, TKE_PAGE_V2) now use
  the base names. The original Phase 1 reserved stubs of the same
  base names (never emitted) were removed. TKE_DELAY_FORCED_RETIRED
  (the Phase 7 S-3 placeholder kept to preserve enum numeric values)
  was deleted outright. Net result: every event kind on the wire is
  spelled cleanly and the enum has 7 fewer dead slots.
- T5 baselines re-captured. The numeric event-kind values shift
  with the slot deletion, so pre-v2.0.1 event traces (which compare
  numeric kind bytes implicitly via line-text) only match v2.0.1
  binaries after the rename pass. Fresh baselines land at
  tests/expected/<scenario>_events_v2.0.1.txt for all 5 production
  scenarios; the v2.0.0 baselines stay on disk as
  <scenario>_events_v2.0.0.txt for historical reference.
- Menu footer version string. Bumped from v2.0.0 to v2.0.1.

Verified
- T1 cell-buffer parity byte-identical vs tests/golden/*.cells with
  row 24 masked for all 5 production scenarios.
- All 115 unit tests across --unit-tests, --phase3-tests,
  --phase4-tests, --phase5-tests, --phase6-tests modes pass.
- wmake phase6-gate (static analysis + DGROUP zero-shell + keyword
  parity + normalize-far + tag-write allowlist + themes-segment
  audits) green.
- 0 shell-side DGROUP near-data across all 22 shell translation
  units (Cerberus discipline preserved).

Closes GitHub issue #1.

v2.0 (2026-05-18): Engine/shell split, scrollback, theming, card menu

The 13-phase v2.0 rework. v1.x was a single-binary scenario engine
with rendering, audio, input, and the scenario state machine all
living in one ~7500-line engine.c. v2.0 is the same engine
restructured into an event-emitting core plus a shell-resident
consumer. User-visible features new in v2.0: a scrollback overlay
(PgUp), per-AI theming (each scenario has its own colour palette), a
card-gallery menu replacing the v1 dropdown list, a live status bar
with rotating context hints, a context-sensitive F1 help overlay, and
a [meta] header for scenario authorship metadata. All 5 v1.x
scenarios run unchanged on v2.0; v1.x saves migrate to v3 silently on
next save.

Architecture
- Engine/shell split. The engine is now a pure scenario state
  machine emitting events on a fixed-size ring; the shell consumes
  events and owns all rendering, audio, input, and screen-effect
  surfaces. The split is invisible to scenarios (the .scn format is
  unchanged) but gives v2.1+ headroom for new shell surfaces (a web
  replay viewer is sketched in the design doc as a v2.1 candidate;
  nothing builds on it yet).
- Theming. Six themes ship: MENU plus one per AI (AXIOM, HUSHLINE,
  KESTREL9, ORCHARD, CINDER). Themes are filename-inferred for v1
  scenarios; v2 scenarios can override via [meta] theme:. Themes
  carry body/highlight/dim/status attribute bytes plus typing speed
  (cps), paragraph pause, transition kind, climax kind, and stinger
  id. Each AI has its own visual personality without scenario authors
  writing colour bytes by hand.
- Parser ceiling lift. v1's 32-command-per-state limit (cmd_t
  commands[32] inline per state) is gone; v2 uses a shared
  4096-command pool via _fmalloc. The 5 production scenarios were
  all pressing the v1 ceiling; v2 has headroom.
- DAT v3 format. The save file (TAKEOVER.DAT) bumps to v3 with
  XOR bitrot guard, achievement bitfield (24 bits over 3 bytes),
  per-ending bit semantics, reading-speed enum persistence, and
  cumulative playtime in seconds. v1 and v2 saves migrate to v3
  silently on next save.

Added
- Scrollback overlay (PgUp). Captures auto-scrolled lines and
  page snapshots in a 200-row ring (31,200 bytes cells, 78 cols x
  200 rows x 2 bytes + 1,024 bytes index, 256 entries x 4 bytes) via
  _fmalloc. PgUp opens the overlay; Up/Down/PgUp/PgDn/Home/End
  navigate; Esc closes. Available from every shell state (in-scenario
  delay, choice menu, input prompt).
- Context-sensitive F1. The help overlay now adapts to the
  current shell state. Six panels: IDLE, DELAY, FLASH, CHOICE, INPUT,
  OVERLAY. Each panel lists the keys active in that state. v1's F1
  was a single static panel.
- Card-gallery menu. The character selection screen now shows
  each AI as a card with ASCII portrait, name, role, and description.
  Settings, About, Cracktro sub-menus replace v1's single-screen
  design. Completion markers below each card show which endings have
  been seen. F8 replays the hidden cracktro from v1.1.
- Live status bar (row 23). Rotating context hints visible in
  every shell state. Theme-aware; refreshes on TKE_THEME_CHANGED.
- Per-AI theming. Each scenario has its own colour palette
  (Axiom in bright clinical white, Hushline in muted greys, Kestrel-9
  in alert red, Orchard in warm friendly tones, Cinder in a deeper
  meta-palette). Status bar, menu highlight, choice cursor, and
  reading-area body text all follow the active theme.
- [meta] header in .scn files. New scenarios can declare
  version, theme, title, author, year, content warnings. v1 scenarios
  with no [meta] block continue to load (default to version: 1, theme
  inferred from filename).
- narrate: composite verb. Combines attr + text + delay in one
  line; reduces scenario-file boilerplate. Available in [meta]
  version: 2 scenarios.
- page verb. Snapshots the reading area into scrollback and
  clears for a new page; available in [meta] version: 2 scenarios.
- theme: verb. Mid-scenario theme override; available in
  [meta] version: 2 scenarios.
- news_live: verb (placeholder). Requests live news from the
  NetISA INT 63h vector; falls through to the fallback string if the
  TSR is not loaded. NetISA's own Phase 3 will wire the real fetch;
  v2.0 ships the fallback path for every scenario.
- Achievement system. 24-bit bitfield in TAKEOVER.DAT v3;
  per-ending bits per AI; About menu shows progress copy from a
  24-entry copy table.
- Cumulative playtime tracking. TAKEOVER.DAT v3 records total
  seconds played across all runs; surfaced in the About sub-menu.

Fixed
- DGROUP near-data footprint. v1.2.1's static buffers and tag
  table lived in DGROUP; v2 moves them to per-TU FAR_DATA segments.
  DGROUP shrunk from 63 KB (v1.2.1) to roughly 15 KB (v2). Working-
  set memory is similar; the reorganization gives v2.1+ headroom for
  new features.
- 8088 stack discipline. Long render paths previously consumed
  deep stacks via recursive helpers; v2's event-loop shell uses
  iterative dispatch with a maximum recursion depth of 2 (overlay
  nesting). No measured stack-overflow risk on real iron.
- Audio keep-alive during long waits. The OPL2 tick now pumps
  every 1024 cells during scrollback eviction (~17 ms on 8088),
  preventing the audible dropout v1.x had on text-heavy transitions.
- F1 help works during typewriter narration. v1.2.1's F1 only
  opened at prompts; v2's F1 dispatches from every state including
  mid-delay and mid-flash.
- Tag rewrite cell_count semantics. v1.x had ad-hoc cell-count
  rounding for tag rewrites; v2 enforces pad-on-shorter and truncate-
  on-longer with a stale-tag no-op.
- shell_poll_keys peek-then-consume. v1.x's input loop drained
  the keyboard buffer on every poll and lost any non-hotkey char.
  v2's peek-then-consume preserves typed characters for the next
  read_input call. Caught by Phase 8.1 iron-test on 320CDT; cell-
  buffer harness missed it because scripted input bypasses the
  keyboard buffer.

Compatibility
- v1.x scenarios run unchanged. No .scn edit needed. The
  filename-inferred theme lookup gives v1 scenarios their per-AI
  colours automatically.
- v1.x saves migrate to v3 silently on next save. Existing
  completion progress is preserved; achievement bits initialise to
  zero; reading-speed defaults to Normal; playtime initialises to 0.
- v2 scenarios CANNOT load on v1.x. A v2 scenario opened under
  v1.x rejects with a version-too-new error.
- [meta] version: 3+ scenarios reject on v2.0 with
  ENGINE_ERR_VERSION. Forward-compat reservation for v2.1+.
- F9 / F10 status flash now repaints in the active theme's
  status attribute (was v1.2.1's single hardcoded ATTR_STATUS).
  Per-AI visual variance is intentional: axiom flashes bright
  white, hushline subdued grey, kestrel9 red, orchard cyan, cinder
  magenta. Status row cells diverge from v1.2.1 goldens by design;
  T1 cell-buffer parity is gated on rows 0..23 only (row 24 is
  Phase 11's status bar territory).

Binary
- TAKEOVER.EXE: 63,068 bytes (v1.2.1) -> 146,782 bytes (v2.0). +83 KB.
  The growth is the shell-side TU set (achievements_copy, attr, audio,
  consumer, cursor, dat_io, delay, effects, input, menu_card,
  menu_cards, menu_save, news_req, overlay, scrollback, status_bar,
  tag_table, text_emit, theme_apply, theme_state, transitions,
  word_wrap) plus the consumer arms and the themes table. The 5
  production scenarios still fit in the conventional-memory budget on
  a 320CDT (250 KB free floor; v2.0 leaves headroom).
- 0 shell-side DGROUP near-data across all 22 shell translation units
  (Cerberus discipline; every static is static <type> __far <name>).
- 115+ tests across 6 acceptance gates; T1 cell-buffer parity
  preserved byte-for-byte vs v1.2.1 for all 5 production scenarios
  across all 13 phases.

Testing
- DOSBox-Staging full automated test suite green at each phase tag.
- Iron-rig validation: tested on DOSBox-Staging. Iron-rig validation
  on the 320CDT (primary), 386PC (NetISA bootstrap), and 486 NetISA
  is pending; see docs/PHASE13_IRON_TEST_PROTOCOL.md for the
  protocol and docs/PHASE13_IRON_TEST_LOG_TEMPLATE.md for the log
  template. Any regression that surfaces lands as a follow-up patch
  release (v2.0.1, v2.0.2, v2.0.3, etc., depending on the surface
  involved).

---

v1.2.1 (2026-04-25): Bug-fix release

Patch on top of v1.2 the same day, addressing three issues found in the
post-release code review.

Fixed
- Esc / Enter encoding mismatch. KEY_ESC is 0x001B and KEY_ENTER
  is 0x000D in lib/screen.h, but BIOS INT 16h returns the full
  scan/ASCII pair (e.g. 0x011B for Esc on real hardware). The v1.2
  abort and dialog-advance checks worked because every site already had
  a defensive (key & 0xFF) == 0x1B fallback, but the dual checks were
  scattered and brittle. New IS_KEY_ESC and IS_KEY_ENTER helper
  macros in lib/screen.h accept either encoding; all six call sites in
  src/engine.c, src/cracktro.c, and src/menu.c now use the macros.
- Space-during-audio-flash leaked into the outer scenario delay.
  handle_audio_key runs a 600 ms engine_delay after a F9/F10 toggle
  to flash a status hint. If the user pressed Space during that flash,
  the inner delay set g_skip_delay = 1 and returned, but the outer
  scenario delay (the one the user was sitting through when they hit
  F9) then saw the flag still set and skipped early. Now the flag is
  saved before the flash and restored after, so Space during the flash
  skips only the flash, not the outer delay.
- g_skip_delay not reset in engine_reset for symmetry with
  g_abort_requested. No known live bug; defensive only.

Documentation
- New comment on read_input clarifying the buf_max convention
  (chars excluding the NUL; caller storage must be buf_max + 1 bytes).

Compatibility
- v1.2 saves and .scn files are unchanged. v1.2.1 is a drop-in
  replacement.

Binary
- Size: 63,068 bytes (v1.2 was 63,050). +18 bytes.

---

v1.2 (2026-04-25): Robustness & UX Polish

Fixed
- Esc now exits any running scenario. Before this release, scenario
  input loops only handled Enter and the typed characters; engine_delay
  busy-waited on the BIOS tick with no exit path. A user mid-scenario
  who hit Esc got nothing and had to reboot. Esc now sets a global
  abort flag that engine_run honours at the top of the outer state
  loop, returning a new END_QUIT result that skips the
  "Scenario complete" splash and returns to the menu cleanly.
- Engine no longer eats keys typed during delays. The previous
  abort path drained the BIOS keyboard buffer on every poll; any key
  pressed during a long pause was lost before the next input prompt
  saw it. The new check_engine_keys peeks first, consumes only the
  global hotkeys, and leaves everything else for the next read_input
  or show_choices.
- F9 / F10 audio toggles work mid-scenario. Previously they only
  fired when the scenario stopped on a prompt or choice. Now they take
  effect immediately, including during typing animations and long
  scripted pauses.
- Climax sequence drains held keys on early exit. A mashed Esc
  during the Mode 13h climax could leak into the next screen; the
  cleanup loop now flushes the buffer before returning.
- Parser error messages include the scenario filename. Errors that
  used to say "Line 42: malformed state header" now say
  "Error: scn/orchard.scn:42: malformed state header (expected
  [state: name])".

Added
- Space = skip current pause / typing animation. Cuts through
  long scripted delays without aborting the scenario.
- F1 = key reference overlay. Modal box shown on top of the
  current scenario; lists every key the engine handles. Available
  during delays, input prompts, and choice menus. Resumes the
  scenario on any keystroke.
- F9 / F10 mute states persist across runs. TAKEOVER.DAT save file
  bumped to schema v2 with two new bytes (music_muted, sfx_muted).
  v1 saves load with both defaults set to off (the v1 behaviour) and
  upgrade silently on the next write.
- Status bar now lists Esc. "F9:Music F10:Sound" became
  "F9:Mus F10:Snd Esc:Quit" so the new affordance is discoverable
  without reading the README.
- Menu version string updated to v1.2.

Compatibility
- v1.1 TAKEOVER.DAT files load fine; you keep your completion progress
  and start with both audio channels unmuted.
- Scenario .scn files are unchanged; v1.1 scenarios run under v1.2
  with no edits.

Binary
- 63,050 bytes (v1.1 was 60,492). +2,558 bytes.

---

v1.1 (2026-04-13): Demoscene Enhancement Pack

Audio-visual beat sync, OPL2 sound stingers, state transitions
(dissolve, wipe, fade, glitch), sine wave text distortion, VGA palette
cycling in text mode, AI control status bar, per-AI Mode 13h climax
sequences, hidden cracktro with raster bars, DYCP scroller, starfield,
9-channel chiptune. Quality-gated through 5 rounds of adversarial
review (12 bugs fixed). 60.5 KB EXE.

v1.0 (2026-04-13): Original release

Five AI scenarios with branching narratives. Text-mode rendering with
VGA Mode 13h plasma title, AdLib FM ambient music, palette fade
transitions. Hardware auto-detection (VGA / EGA / CGA / MDA, AdLib,
FPU) with graceful degradation. 42 KB EXE. MIT license.
