diff --git a/.ctx/HANDOFF.md b/.ctx/HANDOFF.md
index 54c4cd9..fa200a3 100644
--- a/.ctx/HANDOFF.md
+++ b/.ctx/HANDOFF.md
@@ -1,4 +1,4 @@
-# Handoff — notfiles (2026-07-07)
+# Handoff — notfiles (2026-07-14)
**Branch:** main | **Build:** unknown | **Tests:** unknown
@@ -11,6 +11,7 @@
## Log
- 20260606.135918: done=13 running=0 pending=0 blocked=0
+- 2026-07-08: hexagonal architecture refactor with adapters and integration tests, notsecrets cleanup, config payload moved to notfiles-config repo
- 2026-04-17: Added notnet crate with YubikeySource and threaded Tailscale into notstrap; design spec complete
- 2026-04-15: Hardened bootstrap and hook-state failure handling, made notfiles FileStore usage real, and fixed copy-drift safety coverage
- 2026-04-15: Pruned completed items from handoff; GitHub issues plus doob are now the source of truth for active work
diff --git a/.ctx/HANDOFF.notfiles.notfiles.yaml b/.ctx/HANDOFF.notfiles.notfiles.yaml
index 07eaa4a..f6032de 100644
--- a/.ctx/HANDOFF.notfiles.notfiles.yaml
+++ b/.ctx/HANDOFF.notfiles.notfiles.yaml
@@ -1,6 +1,6 @@
project: notfiles
id: notfile
-updated: 2026-07-08
+updated: 2026-07-14
items:
- id: uncommitted-work
doob_uuid: null
@@ -20,14 +20,12 @@ items:
completed: null
extra: []
log:
-- date: 2026-07-08
- summary: hexagonal architecture refactor with adapters and integration tests, notsecrets cleanup, config payload moved to notfiles-config repo
- commits:
- - 6427d18
- - 25624c9
- date: '20260606.135918'
summary: done=13 running=0 pending=0 blocked=0
commits: []
+- date: 2026-07-08
+ summary: hexagonal architecture refactor with adapters and integration tests, notsecrets cleanup, config payload moved to notfiles-config repo
+ commits: []
- date: 2026-04-17
summary: Added notnet crate with YubikeySource and threaded Tailscale into notstrap; design spec complete
commits: []
diff --git a/.superpowers/brainstorm/72758-1776397235/content/architecture.html b/.superpowers/brainstorm/72758-1776397235/content/architecture.html
deleted file mode 100644
index 097794b..0000000
--- a/.superpowers/brainstorm/72758-1776397235/content/architecture.html
+++ /dev/null
@@ -1,71 +0,0 @@
-
notfiles + Tailscale — Bootstrap Architecture
-How the crates fit together for ephemeral machine setup
-
-
-
-
-
-
-
-
-
New Machine
-
TS_AUTHKEY env var
-
YubiKey (PIV slot 9d)
-
→ interactive prompt
-
-
-
→
-
-
-
-
notstrap
-
1. prereqs check
-
2. → notnet
-
3. clone Gitea
-
4. → notsecrets
-
5. link dotfiles
-
6. run hooks
-
-
-
-
-
-
→
-
-
-
notnet NEW
-
detect existing Tailscale
-
install if missing
-
resolve auth key
-
join tailnet
-
verify peer reachable
-
-
→
-
-
Tailnet
-
minibox (VPS)
-
Gitea repo
-
-
-
-
-
→
-
-
-
notsecrets +YubiKey
-
YubikeySource (PIV 9c)
-
BitwardenSource
-
FileSource
-
PromptSource
-
-
-
-
-
-
-
-
- Already on Tailnet? notstrap detects active Tailscale → skips notnet entirely
-
-
-
diff --git a/.superpowers/brainstorm/72758-1776397235/state/server-stopped b/.superpowers/brainstorm/72758-1776397235/state/server-stopped
deleted file mode 100644
index ac70b7f..0000000
--- a/.superpowers/brainstorm/72758-1776397235/state/server-stopped
+++ /dev/null
@@ -1 +0,0 @@
-{"reason":"idle timeout","timestamp":1776399095365}
diff --git a/.superpowers/brainstorm/72758-1776397235/state/server.log b/.superpowers/brainstorm/72758-1776397235/state/server.log
deleted file mode 100644
index 584bbb4..0000000
--- a/.superpowers/brainstorm/72758-1776397235/state/server.log
+++ /dev/null
@@ -1,3 +0,0 @@
-{"type":"server-started","port":63245,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:63245","screen_dir":"/Users/joe/dev/notfiles/.superpowers/brainstorm/72758-1776397235/content","state_dir":"/Users/joe/dev/notfiles/.superpowers/brainstorm/72758-1776397235/state"}
-{"type":"screen-added","file":"/Users/joe/dev/notfiles/.superpowers/brainstorm/72758-1776397235/content/architecture.html"}
-{"type":"server-stopped","reason":"idle timeout"}
diff --git a/.superpowers/brainstorm/72758-1776397235/state/server.pid b/.superpowers/brainstorm/72758-1776397235/state/server.pid
deleted file mode 100644
index 436a082..0000000
--- a/.superpowers/brainstorm/72758-1776397235/state/server.pid
+++ /dev/null
@@ -1 +0,0 @@
-72766
diff --git a/CLAUDE.md b/CLAUDE.md
index 2b6e0fc..7418e3a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,5 +1,11 @@
# CLAUDE.md
+
+ Read:
+ - @docs/DESIGN.md
+ - @.ctx/HANDOFF.md
+
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What is notfiles?
@@ -24,19 +30,19 @@ cargo fmt --check # check formatting
This is a Cargo workspace with 7 crates under `crates/`:
-| Crate | Purpose |
-| ------------ | -------------------------------------------------------------------------- |
-| `notcore` | Shared types: `Config`, `NotfilesError`, `Reporter`, `LinkEvent`, |
-| | `expand_tilde`, `suggest_package`, `HookPhase`, `HookSpec`, `Report` |
-| `notfiles` | Dotfiles linker — lib + `notfiles` binary. Subcommands: `init`, `link`, |
-| | `unlink`, `status`, `check`, `diff`, `adopt`, `completions` |
-| `notsecrets` | Multi-provider secret resolution (`SecretResolver`), age encryption/ |
-| | decryption, identity management |
-| `nothooks` | Nushell hook runner with dot/setup phases and state persistence |
-| `notnet` | Network utilities — Tailscale integration, YubikeySource |
-| `notstrap` | New-machine bootstrap orchestrator — ties all crates together |
-| `notgraph` | Dependency/import graph for Rust files — HTML/MD/JSON/Mermaid output, |
-| | heatmap, cycle detection |
+| Crate | Purpose |
+| ------------ | ----------------------------------------------------------------------- |
+| `notcore` | Shared types: `Config`, `NotfilesError`, `Reporter`, `LinkEvent`, |
+| | `expand_tilde`, `suggest_package`, `HookPhase`, `HookSpec`, `Report` |
+| `notfiles` | Dotfiles linker — lib + `notfiles` binary. Subcommands: `init`, `link`, |
+| | `unlink`, `status`, `check`, `diff`, `adopt`, `completions` |
+| `notsecrets` | Multi-provider secret resolution (`SecretResolver`), age encryption/ |
+| | decryption, identity management |
+| `nothooks` | Nushell hook runner with dot/setup phases and state persistence |
+| `notnet` | Network utilities — Tailscale integration, YubikeySource |
+| `notstrap` | New-machine bootstrap orchestrator — ties all crates together |
+| `notgraph` | Dependency/import graph for Rust files — HTML/MD/JSON/Mermaid output, |
+| | heatmap, cycle detection |
## Architecture
diff --git a/Cargo.lock b/Cargo.lock
index e52b1aa..cc7c348 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1084,7 +1084,6 @@ dependencies = [
name = "notcore"
version = "0.1.0"
dependencies = [
- "anyhow",
"dirs",
"serde",
"strsim",
@@ -1114,9 +1113,7 @@ dependencies = [
name = "notforge"
version = "0.1.0"
dependencies = [
- "anyhow",
"base64",
- "clap",
"miette",
"notcore",
"serde",
@@ -1173,7 +1170,6 @@ dependencies = [
"bech32",
"chacha20poly1305",
"curve25519-dalek",
- "dirs",
"ed25519-dalek",
"hkdf",
"hmac",
@@ -1188,7 +1184,6 @@ dependencies = [
"toml",
"x25519-dalek",
"yubikey",
- "zeroize",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index fb2e762..d40e6ac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ homepage = "https://github.com/89jobrien/notfiles"
keywords = ["dotfiles", "symlink", "stow", "config"]
categories = ["command-line-utilities", "filesystem"]
readme = "README.md"
+rust-version = "1.87.0"
[workspace]
members = [
@@ -22,31 +23,33 @@ resolver = "2"
[workspace.dependencies]
anyhow = "1"
+assert_fs = "1"
base64 = "0.22"
+chrono = { version = "0.4", default-features = false, features = ["clock"] }
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
-chrono = { version = "0.4", default-features = false, features = ["clock"] }
dirs = "6"
globset = "0.4"
+miette = { version = "7", features = ["derive"] }
+notcore = { path = "crates/notcore" }
+notfiles = { path = "crates/notfiles" }
+notforge = { path = "crates/notforge" }
+nothooks = { path = "crates/nothooks" }
+notnet = { path = "crates/notnet" }
+notsecrets = { path = "crates/notsecrets" }
+notstrap = { path = "crates/notstrap", version = "^0.1.0" } #unified
+rpassword = "7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
strsim = "0.11"
+tempfile = "3"
thiserror = "2"
toml = "0.8"
-miette = { version = "7", features = ["derive"] }
ureq = { version = "2", features = ["json"] }
which = "7"
-rpassword = "7"
-tempfile = "3"
-assert_fs = "1"
-notcore = { path = "crates/notcore" }
-notfiles = { path = "crates/notfiles" }
-notsecrets = { path = "crates/notsecrets" }
-nothooks = { path = "crates/nothooks" }
-notnet = { path = "crates/notnet" }
-notforge = { path = "crates/notforge" }
[profile.release]
+codegen-units = 1
opt-level = 3
lto = "thin"
strip = true
diff --git a/HANDOFF.md b/HANDOFF.md
deleted file mode 100644
index 4da5ec7..0000000
--- a/HANDOFF.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# HANDOFF.md
-
-Legacy note as of 2026-04-15.
-
-This repo no longer uses `HANDOFF.md` as a running history log.
-
-Active workflow:
-
-1. Sync GitHub issues into `doob`
-2. Read short-lived handoff context from `.ctx/HANDOFF.*.*.yaml`
-3. Update issue state in `doob`
-4. Sync `doob` back to GitHub issues
-
-Rules:
-
-- Handoff context should be brief and ephemeral
-- Only open work should appear in handoff files
-- Closed issues should be removed from handoff files after sync
-- Historical status belongs in GitHub issues and git history, not here
-
-Current state:
-
-- No open handoff items are tracked locally
-- Use GitHub issues plus `doob` as the task source of truth
diff --git a/cliff.toml b/cliff.toml
new file mode 100644
index 0000000..81e398f
--- /dev/null
+++ b/cliff.toml
@@ -0,0 +1,93 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+[remote.github]
+owner = "89jobrien"
+repo = "notfiles"
+
+[changelog]
+body = """
+{% if version %}\
+ ## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+ ## Unreleased
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ### {{ group | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\
+ {{ commit.message | split(pat="\n") | first | upper_first | trim_end }}\
+ {% if commit.links %} \
+ ({% for link in commit.links %}[#{{ link.text }}]({{ link.href }}){% if not loop.last %}, {% endif %}{% endfor %})\
+ {% endif %}\
+ {% endfor %}
+{% endfor %}\n
+"""
+
+footer = """
+{%- macro remote_url() -%}
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
+{%- endmacro -%}
+
+{% for release in releases -%}
+ {% if release.version -%}
+ {% if release.previous.version -%}
+ [{{ release.version | trim_start_matches(pat="v") }}]: \
+ {{ self::remote_url() }}/compare/{{ release.previous.version }}...{{ release.version }}
+ {% endif -%}
+ {% else -%}
+ [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}...HEAD
+ {% endif -%}
+{% endfor %}
+
+
+"""
+
+trim = true
+render_always = true
+output = "CHANGELOG.md"
+
+postprocessors = [
+ { pattern = '', replace = "https://github.com/89jobrien/notfiles" },
+]
+
+
+[git]
+conventional_commits = true
+filter_unconventional = true
+require_conventional = false
+split_commits = false
+protect_breaking_commits = false
+filter_commits = false
+fail_on_unmatched_commit = false
+use_branch_tags = false
+topo_order = false
+topo_order_commits = true
+sort_commits = "oldest"
+recurse_submodules = false
+
+commit_preprocessors = [
+ { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" },
+]
+
+commit_parsers = [
+ { message = "^feat", group = "Features" },
+ { message = "^fix", group = "Bug Fixes" },
+ { message = "^refactor", group = "Refactor" },
+ { message = "^doc", group = "Documentation" },
+ { message = "^perf", group = "Performance" },
+ { message = "^style", group = "Styling" },
+ { message = "^test", group = "Testing" },
+ { message = "^chore\\(release\\):.*", skip = true },
+ { message = "^chore\\(deps.*\\)", skip = true },
+ { message = "^chore\\(pr\\)", skip = true },
+ { message = "^chore\\(pull\\)", skip = true },
+ { message = "^chore|^ci", group = "Miscellaneous" },
+ { body = ".*security", group = "Security" },
+ { message = "^revert", group = "Reverts" },
+ { message = ".*", group = "Other" },
+]
+
+link_parsers = [
+ { pattern = "#(\\d+)", href = "https://github.com/89jobrien/notfiles/issues/$1", text = "$1" },
+]
diff --git a/crates/notcore/Cargo.toml b/crates/notcore/Cargo.toml
index 42a6b54..1bd452f 100644
--- a/crates/notcore/Cargo.toml
+++ b/crates/notcore/Cargo.toml
@@ -10,7 +10,6 @@ categories.workspace = true
description = "Shared types and config for the notfiles dotfiles manager"
[dependencies]
-anyhow = { workspace = true }
dirs = { workspace = true }
serde = { workspace = true }
strsim = { workspace = true }
diff --git a/crates/notforge/Cargo.toml b/crates/notforge/Cargo.toml
index e1c1979..9db30b1 100644
--- a/crates/notforge/Cargo.toml
+++ b/crates/notforge/Cargo.toml
@@ -18,9 +18,7 @@ name = "notforge"
path = "src/main.rs"
[dependencies]
-anyhow = { workspace = true }
base64 = { workspace = true }
-clap = { workspace = true }
miette = { workspace = true }
notcore = { workspace = true }
serde = { workspace = true }
diff --git a/crates/notsecrets/Cargo.toml b/crates/notsecrets/Cargo.toml
index 7977b1b..8ca2b2e 100644
--- a/crates/notsecrets/Cargo.toml
+++ b/crates/notsecrets/Cargo.toml
@@ -10,7 +10,6 @@ base64 = "0.22"
bech32 = "0.11"
chacha20poly1305 = "0.10"
curve25519-dalek = "4"
-dirs = { workspace = true }
ed25519-dalek = "2"
hkdf = "0.12"
hmac = "0.12"
@@ -24,7 +23,6 @@ thiserror = { workspace = true }
toml = { workspace = true }
x25519-dalek = { version = "2", features = ["static_secrets"] }
yubikey = { version = "0.8", optional = true }
-zeroize = "1"
[features]
default = []
diff --git a/preflight.md b/preflight.md
index 42f7b8a..71040b7 100644
--- a/preflight.md
+++ b/preflight.md
@@ -3,7 +3,7 @@
A `rust-script` preflight that surfaces repo context in one shot: shell detection, git state,
tracked-file tree, and HANDOFF document read/update.
-```rust
+````rust
#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
@@ -238,7 +238,7 @@ fn main() -> Result<()> {
println!("\n{}", "╚══ done ══╝".bold().yellow());
Ok(())
}
-```
+````
## Usage
@@ -253,13 +253,13 @@ rust-script preflight.rs
## What it surfaces
-| Section | Details |
-|---------|---------|
-| Environment | Current shell (`$SHELL`), cwd, git root |
-| Git status | Branch name, short status (clean or dirty file list) |
-| Git history | Last 7 commits, one-line with decoration |
-| Tracked file tree | `git ls-files` output rendered as an indented tree, depth ≤ 3 |
-| HANDOFF docs | Full content (first 40 lines) of any `HANDOFF.*` file at repo root; stamps `last_preflight:` timestamp |
+| Section | Details |
+| ----------------- | ------------------------------------------------------------------------------------------------------ |
+| Environment | Current shell (`$SHELL`), cwd, git root |
+| Git status | Branch name, short status (clean or dirty file list) |
+| Git history | Last 7 commits, one-line with decoration |
+| Tracked file tree | `git ls-files` output rendered as an indented tree, depth ≤ 3 |
+| HANDOFF docs | Full content (first 40 lines) of any `HANDOFF.*` file at repo root; stamps `last_preflight:` timestamp |
## Dependencies
diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml
index 77e1240..2edae83 100644
--- a/tests/integration/Cargo.toml
+++ b/tests/integration/Cargo.toml
@@ -3,7 +3,7 @@ name = "integration"
version = "0.1.0"
edition = "2024"
license.workspace = true
-publish = false
+publish = true
[[test]]
name = "bootstrap"
@@ -16,9 +16,9 @@ path = "tests/cross_crate.rs"
[dev-dependencies]
anyhow = { workspace = true }
assert_fs = { workspace = true }
-notcore = { path = "../../crates/notcore" }
-notfiles = { path = "../../crates/notfiles" }
-nothooks = { path = "../../crates/nothooks" }
-notsecrets = { path = "../../crates/notsecrets" }
-notstrap = { path = "../../crates/notstrap" }
+notcore = { workspace = true } #unified
+notfiles = { workspace = true } #unified
+nothooks = { workspace = true } #unified
+notsecrets = { workspace = true } #unified
+notstrap = { workspace = true } #unified
tempfile = { workspace = true }