diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# 0.1.0.0 — 2026-10-24
+
+## New Features
+
+Initial optional framework reporter with stable test IDs and complete attempt-bound evidence. Framework dependencies remain outside the core package.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, Nadeem Bitar
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+3. Neither the name of the copyright holder nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# heni-tasty
+
+Replace Tasty.defaultMain with heniMain. Native Tasty selection is intersected with HENI_SELECTOR during an attempt. Outside Heni, normal framework behavior is preserved.
+
+Add `heni-tasty ==0.1.0.0` to the test component and select `"adapter": "heni-v1"` in the Heni case. Complete attempt-bound records prove selected/executed IDs and named failures. Missing, stale or partial evidence cannot count as detection.
+
+See the [adapter guide](https://github.com/shinzui/heni/blob/master/docs/design/adapters.md) and packaged test fixture for runnable examples.
diff --git a/heni-tasty.cabal b/heni-tasty.cabal
new file mode 100644
--- /dev/null
+++ b/heni-tasty.cabal
@@ -0,0 +1,74 @@
+cabal-version: 3.4
+name: heni-tasty
+version: 0.1.0.0
+synopsis: Attempt-bound Tasty evidence for Heni
+license: BSD-3-Clause
+license-file: LICENSE
+author: Nadeem Bitar
+maintainer: nadeem@gmail.com
+copyright: 2026 Nadeem Bitar
+category: Testing
+build-type: Simple
+tested-with: ghc ==9.12.4
+homepage: https://github.com/shinzui/heni
+bug-reports: https://github.com/shinzui/heni/issues
+description: Optional framework reporter for trustworthy named mutation evidence.
+extra-doc-files:
+  CHANGELOG.md
+  README.md
+
+common common
+  default-language: GHC2024
+  default-extensions:
+    DuplicateRecordFields
+    OverloadedStrings
+
+  ghc-options: -Wall
+  build-depends:
+    base >=4.21.2 && <4.22,
+    containers >=0.7 && <0.9,
+    heni ==0.1.0.0,
+    text >=2.1.4 && <2.2,
+
+library
+  import: common
+  hs-source-dirs: src
+  exposed-modules: Test.Heni.Tasty
+  build-depends:
+    stm >=2.5.3.1 && <2.6,
+    tasty >=1.5.5 && <1.6,
+
+executable heni-tasty-fixture
+  import: common
+  hs-source-dirs: test/fixture
+  main-is: Main.hs
+  build-depends:
+    heni-tasty ==0.1.0.0,
+    stm >=2.5.3.1 && <2.6,
+    tasty >=1.5.5 && <1.6,
+    tasty-hunit >=0.10.2 && <0.11,
+
+  ghc-options: -threaded
+
+test-suite heni-tasty-test
+  import: common
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  main-is: Main.hs
+  build-depends:
+    directory >=1.3.10.1 && <1.4,
+    filepath >=1.5.5 && <1.6,
+    heni-tasty ==0.1.0.0,
+    process >=1.6.26.1 && <1.7,
+    unix >=2.8.8 && <2.9,
+
+  build-tool-depends:
+    heni-cli:heni,
+    heni-tasty:heni-tasty-fixture,
+
+  ghc-options: -threaded
+
+source-repository head
+  type: git
+  location: https://github.com/shinzui/heni.git
+  subdir: adapters/heni-tasty
diff --git a/src/Test/Heni/Tasty.hs b/src/Test/Heni/Tasty.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Heni/Tasty.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE CPP #-}
+
+-- | Replace @defaultMain@ with 'heniMain'. Outside a Heni attempt, Tasty's
+-- ordinary console, filters and options remain available.
+module Test.Heni.Tasty (heniMain) where
+
+import Control.Concurrent.STM
+import Control.Monad (unless)
+import Data.IntMap.Strict qualified as IntMap
+import Data.Text qualified as Text
+import System.Environment (getArgs, lookupEnv, withArgs)
+import Test.Heni.Adapter
+import Test.Tasty qualified as Tasty
+import Test.Tasty.Ingredients.Basic (consoleTestReporter)
+import Test.Tasty.Runners qualified as R
+
+-- | Under Heni, a dedicated reporter owns completion after resource finalizers.
+-- HENI_SELECTOR is a native Tasty pattern, intersected with existing patterns.
+heniMain :: Tasty.TestTree -> IO ()
+heniMain tree =
+  currentEvidenceAttempt >>= \case
+    Nothing -> Tasty.defaultMain tree
+    Just attempt -> guardEvidenceAttempt attempt $ do
+      args <- getArgs
+      selector <- lookupEnv "HENI_SELECTOR"
+      let extra = maybe [] (\s -> ["--pattern", s]) selector
+      withArgs (args ++ extra) (R.defaultMainWithIngredients [reporter attempt] tree)
+
+reporter :: EvidenceAttempt -> R.Ingredient
+reporter attempt = R.TestReporter consoleOptions $ \opts tree -> Just $ \statuses -> do
+  let ids = map stableTestId (R.foldTestTree paths opts tree)
+  results <- atomically (mapM waitDone (IntMap.elems statuses))
+  unless (length ids == length results) (ioError (userError "Tasty status/selection identity mismatch"))
+  pure $ \_ -> do
+    property <- readPropertyReplay attempt
+    let observations = zipWith observation ids results
+    emitSuite attempt (SuiteEvidence ("tasty/" <> Text.pack VERSION_tasty <> ";heni-tasty/0.1.0.0") Nothing ids observations [] (fst <$> property) (maybe [] snd property))
+    pure (all R.resultSuccessful results)
+  where
+    consoleOptions = case consoleTestReporter of R.TestReporter options _ -> options; _ -> []
+    waitDone status = readTVar status >>= \case R.Done result -> pure result; _ -> retry
+    paths =
+      R.trivialFold
+        { R.foldSingle = \_ name _ -> [[Text.pack name]],
+          R.foldGroup = \_ name groups -> map (Text.pack name :) (concat groups)
+        }
+    observation ident result = case R.resultOutcome result of
+      R.Success -> TestPassed ident
+      R.Failure R.TestFailed -> TestFailed ident
+      R.Failure R.TestDepFailed -> TestSkipped ident
+      R.Failure reason -> TestSetupFailed ident (Text.pack (show reason))
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,66 @@
+module Main (main) where
+
+import Control.Exception
+import Control.Monad (forM_, unless)
+import Data.Map.Strict qualified as Map
+import Data.Set qualified as Set
+import Data.Text qualified as Text
+import System.Directory qualified as Dir
+import System.Exit (ExitCode (..))
+import System.FilePath ((</>))
+import System.Posix.Temp (mkdtemp)
+import System.Process (readProcessWithExitCode)
+import System.Timeout (timeout)
+import Test.Heni
+
+checked :: (Show e) => Either e a -> a
+checked = either (error . show) id
+
+assert' :: String -> Bool -> IO ()
+assert' why good = unless good (error why)
+
+main :: IO ()
+main = timeout 120000000 checks >>= \case Nothing -> error "adapter integration watchdog"; Just () -> pure ()
+
+checks :: IO ()
+checks = bracket (Dir.getTemporaryDirectory >>= \p -> mkdtemp (p </> "heni-tasty-")) Dir.removePathForcibly $ \root -> do
+  let source = root </> "source"
+      exe = "heni-tasty-fixture"
+      bid = checked (mkCommandId "build")
+      tid = checked (mkCommandId "test")
+      cid = checked (mkCaseId "named")
+      command args = Command exe args Set.empty Nothing [] (checked (mkStageBudget 5000000)) (OutputPolicy 4096)
+      definition mode selector intended replay =
+        CaseDefinition
+          cid
+          "framework evidence"
+          Set.empty
+          [Replace (checked (mkRelativeTarget "input")) "anchor" mode 1]
+          bid
+          (Just tid)
+          selector
+          (NamedTests (Set.singleton intended) replay)
+          StructuredTestsV1
+      run name def hooks args =
+        runCampaign
+          (defaultRunOptions source (root </> name))
+          (checked (mkCampaign (Map.fromList [(bid, command ["--heni-build"]), (tid, command args)]) [(def, hooks)]))
+      is verdict RunResult {results = [CaseResult {verdict = actual}]} = actual == verdict
+      is _ _ = False
+  Dir.createDirectory source
+  writeFile (source </> "input") "anchor"
+  old <- Dir.getCurrentDirectory
+  direct <- bracket_ (Dir.setCurrentDirectory source) (Dir.setCurrentDirectory old) (readProcessWithExitCode exe [] "")
+  assert' "ordinary direct execution" (case direct of (ExitSuccess, _, _) -> True; _ -> False)
+  forM_ [("intended", Caught), ("wrong", WrongFailure), ("setup", InfrastructureError)] $ \(mode, expected) -> do
+    result <- run mode (definition (Text.pack mode) Nothing "suite/intended" NoReplay) noHooks []
+    assert' (mode ++ ": " ++ show result) (is expected result)
+  empty <- run "empty" (definition "intended" (Just "absent") "suite/intended" NoReplay) noHooks []
+  assert' "empty selection baseline prevents candidate" (is NotRun empty)
+  filtered <- run "filtered" (definition "intended" (Just "intended") "suite/intended" NoReplay) noHooks ["--pattern", "intended"]
+  assert' ("native and Heni filters: " ++ show filtered) (is Caught filtered)
+
+  writeFile (root </> "manifest.json") "{\"schemaVersion\": 1, \"sourceRoot\": \"source\", \"commands\": {\"build\": {\"executable\": \"heni-tasty-fixture\", \"arguments\": [\"--heni-build\"]}, \"test\": {\"executable\": \"heni-tasty-fixture\"}}, \"cases\": [{\"id\": \"named\", \"description\": \"framework evidence\", \"build\": \"build\", \"test\": \"test\", \"adapter\": \"heni-v1\", \"edits\": [{\"kind\": \"replace\", \"path\": \"input\", \"old\": \"anchor\", \"new\": \"intended\"}], \"expect\": {\"kind\": \"named-tests\", \"failures\": [\"suite/intended\"]}}]}"
+  cli <- readProcessWithExitCode "heni" ["run", root </> "manifest.json", "--output", root </> "cli", "--json"] ""
+  assert' ("CLI adapter parity: " ++ show cli) (case cli of (ExitSuccess, _, _) -> True; _ -> False)
+  putStrLn "tasty direct execution and named subprocess evidence passed"
diff --git a/test/fixture/Main.hs b/test/fixture/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/fixture/Main.hs
@@ -0,0 +1,19 @@
+module Main (main) where
+
+import System.Environment (getArgs)
+import Test.Heni.Tasty
+import Test.Tasty
+import Test.Tasty.HUnit
+
+main :: IO ()
+main =
+  getArgs >>= \case
+    ["--heni-build"] -> pure ()
+    _ -> do
+      mode <- readFile "input"
+      heniMain $
+        testGroup
+          "suite"
+          [ testCase "intended" (assertBool "intended assertion" (mode /= "intended")),
+            testCase "other" (if mode == "setup" then ioError (userError "fixture setup failed") else assertBool "other assertion" (mode /= "wrong"))
+          ]
