diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b0f569a..bd46ec8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,7 +20,7 @@ concurrency:
cancel-in-progress: true
env:
- GO_VERSION: '1.26.2'
+ GO_VERSION: '1.27.1'
jobs:
# ── Tier 1: Fast file-level checks (no toolchain, seconds) ──
@@ -127,9 +127,8 @@ jobs:
cache: true
- uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
- # v2.12.2 is built with Go 1.26.2 — earlier 2.0.x was built
- # with Go 1.24 and refused to lint a Go-1.26-targeted repo.
- version: v2.12.2
+ # Keep the linter's build toolchain at least as new as go.mod.
+ version: v2.14.0
args: --timeout=5m
# ── Tier 3: Unit + integration tests (parallel with lint) ──
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index ea25242..2d32fb0 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -44,7 +44,7 @@ jobs:
- if: matrix.language == 'go'
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v6.0.0
with:
- go-version: '1.26.2'
+ go-version: '1.27.1'
cache: true
- uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1057378..0b4fe77 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,7 +7,7 @@ By participating, you agree to uphold the [Code of Conduct](.github/CODE_OF_COND
## Getting started
1. Fork the repository and clone it locally.
-2. Make sure you have **Go ≥ 1.26** installed (`go version`).
+2. Make sure you have **Go ≥ 1.27** installed (`go version`).
3. Install the local hooks once:
```bash
pre-commit install
diff --git a/README.md b/README.md
index 3e04a7e..bd5100f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,4 @@
-
-
-
+
# noxctl
@@ -60,8 +58,8 @@ For each managed tag, noxctl writes two things to Bear:
Atoms keep their human-authored body; noxctl only owns the canonical line at the top and the master/hub layout. Here is one `#library/books` tag in Bear, before and after a first `noxctl apply`:
-| Before | After |
-|-----------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
+| Before | After |
+|-----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
|  |  |
(Demo vault is at `examples/demo-vault/` — `setup.sh` populates it under `#nox-demo/books` and the paired `noxctl.toml` manages exactly that tag.)
@@ -132,7 +130,7 @@ noxctl has two entry paths that share the same install and the same `validate
**Before you start.**
- You need macOS with [Bear](https://bear.app/) installed. noxctl talks to Bear through Bear's bundled `bearcli`.
-- You need Go 1.26 or newer:
+- You need Go 1.27 or newer:
```bash
go version
```
@@ -224,13 +222,13 @@ Bear ships a built-in backup in **File → Backup Database…** — recommended
**How those map to a blueprint:**
-| Observed shape | Recommendation |
-|-------------------------------------------------|-----------------------------------------------------------|
-| No usable bucket signal | `flat-list` (high confidence when small, medium when large) |
-| Top-level tag, few atoms per bucket | `grouped-vertical` (alternative: `hub-routed-with-subtag`) |
-| Top-level tag, many atoms per bucket | `hub-routed-with-subtag` (alternative: `grouped-vertical`) |
-| Nested tag, strong author signal or many buckets| `hub-routed` |
-| Nested tag, a small declared bucket set | `grouped-vertical` |
+| Observed shape | Recommendation |
+|--------------------------------------------------|-------------------------------------------------------------|
+| No usable bucket signal | `flat-list` (high confidence when small, medium when large) |
+| Top-level tag, few atoms per bucket | `grouped-vertical` (alternative: `hub-routed-with-subtag`) |
+| Top-level tag, many atoms per bucket | `hub-routed-with-subtag` (alternative: `grouped-vertical`) |
+| Nested tag, strong author signal or many buckets | `hub-routed` |
+| Nested tag, a small declared bucket set | `grouped-vertical` |
This follows the *Choosing a blueprint* decision tree, with one extra shortcut the eyeball version omits: on a nested tag, a high distinct-bucket count alone is enough for `hub-routed` (even without an author signal), so the master stays a scannable list of hubs.
@@ -345,10 +343,10 @@ Exit-code shape:
noxctl has two operating modes. Start with the one-shot mode until you trust the plan output on your own vault.
-| Mode | What it does | When to use it | Persistence |
-|------|--------------|----------------|-------------|
-| `validate → plan → apply` | Runs once, shows a diff, then applies it only when you ask | First runs, careful changes, manual control | Exits after the command finishes |
-| `daemon` | Runs continuously and reconciles managed structures when Bear changes | Mature configs you want kept in sync automatically | Lives only while the process is running unless you install a LaunchAgent |
+| Mode | What it does | When to use it | Persistence |
+|---------------------------|-----------------------------------------------------------------------|----------------------------------------------------|--------------------------------------------------------------------------|
+| `validate → plan → apply` | Runs once, shows a diff, then applies it only when you ask | First runs, careful changes, manual control | Exits after the command finishes |
+| `daemon` | Runs continuously and reconciles managed structures when Bear changes | Mature configs you want kept in sync automatically | Lives only while the process is running unless you install a LaunchAgent |
Run the daemon manually when testing continuous reconciliation:
@@ -411,15 +409,15 @@ launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.barad1tos.noxctl.plist
noxctl is useful when Bear is still the place you want to write, but manual structure maintenance has become the annoying part.
-| You gain | You trade away | Why it matters |
-|----------|----------------|----------------|
-| Generated index/hub notes | Some generated structure is now tool-owned | Edit your content freely, but do not hand-tune generated masters/hubs and expect those edits to survive regeneration |
-| Reviewable `plan` output before writes | You need to understand the plan before applying it | Safer than silent automation, but still requires attention |
-| Readiness and verification gates | A few extra commands before the first write | `doctor` catches local setup issues before mutation; `verify` confirms the vault and daemon are clean afterward |
-| Consistent structure across many tags | A config file becomes part of your note system | Your organization rules live in `noxctl.toml`, not only in your head |
-| Optional live reconciliation | A long-running process may be running on your Mac | Great for stable configs, but start manually before installing a LaunchAgent |
-| Obsidian-like organization depth inside Bear | macOS + Bear + terminal are required | This is for Bear power users on macOS, not a cross-platform no-code workflow |
-| Faster cleanup of drift | Bad config can produce bad structure quickly | Always back up before first use and inspect `plan` output |
+| You gain | You trade away | Why it matters |
+|----------------------------------------------|----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
+| Generated index/hub notes | Some generated structure is now tool-owned | Edit your content freely, but do not hand-tune generated masters/hubs and expect those edits to survive regeneration |
+| Reviewable `plan` output before writes | You need to understand the plan before applying it | Safer than silent automation, but still requires attention |
+| Readiness and verification gates | A few extra commands before the first write | `doctor` catches local setup issues before mutation; `verify` confirms the vault and daemon are clean afterward |
+| Consistent structure across many tags | A config file becomes part of your note system | Your organization rules live in `noxctl.toml`, not only in your head |
+| Optional live reconciliation | A long-running process may be running on your Mac | Great for stable configs, but start manually before installing a LaunchAgent |
+| Obsidian-like organization depth inside Bear | macOS + Bear + terminal are required | This is for Bear power users on macOS, not a cross-platform no-code workflow |
+| Faster cleanup of drift | Bad config can produce bad structure quickly | Always back up before first use and inspect `plan` output |
A good first test is one low-risk tag with a small number of notes. Import it, review the generated config, run `plan`, read the diff, back up Bear, and only then run `apply`.
@@ -439,13 +437,13 @@ Five rendering blueprints, each fitting a distinct tag shape. Walk the decision
Full comparison table & structure diagram
-| Blueprint | When to use | Required fields beyond the basics | Bucket source | Output shape |
-|--------------------------|-----------------------------------------------------------------|-----------------------------------|------------------------------------------|---------------------------------------------------|
-| `flat-list` | inbox / capture tags, no grouping | none | n/a | one master with bullet list of every atom |
-| `grouped-vertical` | bucketed collection in one master, operator-declared buckets | `buckets`, `unknown_bucket` | operator-declared (sub-tag or canonical 3rd segment, auto by tag depth) | one master with `## Bucket (N)` H2 per bucket |
-| `hub-routed` | author / source grouping where bucket names live in atom bodies | `unknown_bucket`, `hub_h2_prefix` | atom body H2 (author/source), stamped into the canonical line | Tier-2: master lists hubs, each hub lists atoms |
-| `hub-routed-with-subtag` | hub-style layout but bucket names are sub-tags | `buckets`, `unknown_bucket` | atom sub-tag `#tag/bucket` | Tier-2: master lists hubs, each hub lists atoms |
-| `umbrella` | aggregate multiple existing domains under one master | `children`, `default_child` | n/a | master lists every child domain |
+| Blueprint | When to use | Required fields beyond the basics | Bucket source | Output shape |
+|--------------------------|-----------------------------------------------------------------|-----------------------------------|-------------------------------------------------------------------------|-------------------------------------------------|
+| `flat-list` | inbox / capture tags, no grouping | none | n/a | one master with bullet list of every atom |
+| `grouped-vertical` | bucketed collection in one master, operator-declared buckets | `buckets`, `unknown_bucket` | operator-declared (sub-tag or canonical 3rd segment, auto by tag depth) | one master with `## Bucket (N)` H2 per bucket |
+| `hub-routed` | author / source grouping where bucket names live in atom bodies | `unknown_bucket`, `hub_h2_prefix` | atom body H2 (author/source), stamped into the canonical line | Tier-2: master lists hubs, each hub lists atoms |
+| `hub-routed-with-subtag` | hub-style layout but bucket names are sub-tags | `buckets`, `unknown_bucket` | atom sub-tag `#tag/bucket` | Tier-2: master lists hubs, each hub lists atoms |
+| `umbrella` | aggregate multiple existing domains under one master | `children`, `default_child` | n/a | master lists every child domain |
Required fields beyond the basics — every blueprint also needs `tag`, `index_title`, `blueprint`. See `examples/.toml` for a copy-pasteable starter per blueprint.
@@ -521,7 +519,7 @@ See `examples/minimal.toml` for a tested starter and `examples/personal.toml` fo
## Status & scope
- **Platform:** macOS only. Bear is macOS-only; the watcher uses FSEvents via `fsnotify`'s Darwin backend; the CLI bridge is `bearcli` at `/Applications/Bear.app/Contents/MacOS/bearcli`.
-- **Runtime:** Go ≥ 1.26. Direct dependencies are intentionally small: TOML parsing, Cobra CLI wiring, fsnotify for the daemon watcher, and a small set of Go `x/*` support packages. Adding a runtime dependency is deliberate and requires justification.
+- **Runtime:** Go ≥ 1.27. Direct dependencies are intentionally small: TOML parsing, Cobra CLI wiring, fsnotify for the daemon watcher, and a small set of Go `x/*` support packages. Adding a runtime dependency is deliberate and requires justification.
- **Heritage:** descended from `regen-watchd`, a personal FSEvents daemon that managed a 28-domain Bear corpus; the closed catalog of five blueprints covers every shape that production used.
- **Acceptance test:** byte-equivalent vault output against the legacy daemon for the maintainer's 28-domain corpus.
- **License:** MIT.
diff --git a/bear/domain/canonical.go b/bear/domain/canonical.go
index 02564a7..c19491e 100644
--- a/bear/domain/canonical.go
+++ b/bear/domain/canonical.go
@@ -103,7 +103,7 @@ func (s *atomicParseState) consumeLeadingBlank(trimmed string, p *AtomicParts) b
// switch documents that order matters (H1 before header before blank-skip).
func (d *Domain) parseAtomicContent(content, author string) AtomicParts {
authorH2Marker := "## " + author
- family := strings.SplitN(d.Tag, "/", 2)[0]
+ family, _, _ := strings.Cut(d.Tag, "/")
var parts AtomicParts
state := atomicParseState{stripAuthor: d.StripLegacyAuthorH2}
for line := range strings.SplitSeq(content, "\n") {
diff --git a/bear/domain/routing.go b/bear/domain/routing.go
index 91f7f64..ecb3096 100644
--- a/bear/domain/routing.go
+++ b/bear/domain/routing.go
@@ -469,7 +469,7 @@ func (d *Domain) computeTagOverrides(notes []Note) (overrides map[string]string,
if d.CanonicalTagFor == nil {
return nil, 0
}
- family := strings.SplitN(d.Tag, "/", 2)[0]
+ family, _, _ := strings.Cut(d.Tag, "/")
prefix := family + "/"
for _, note := range notes {
if !hasFamilyMembership(note.Tags, family) {
diff --git a/cmd/noxctl/daemon.go b/cmd/noxctl/daemon.go
index 9cffb70..517da27 100644
--- a/cmd/noxctl/daemon.go
+++ b/cmd/noxctl/daemon.go
@@ -85,18 +85,16 @@ func runDaemon(cmd *cobra.Command, _ []string) error {
return fmt.Errorf(errFmtNoxctlDaemon, bearDBErr)
}
opts := engine.DaemonOpts{
- ApplyOpts: engine.ApplyOpts{
- Domains: domains,
- Pins: pins,
- StatePath: dc.StatePath,
- LockPath: dc.LockPath,
- Features: cliutil.ResolveFeatures(cat, dc),
- AuditEnabled: dc.AuditEnabled,
- BearcliConcurrency: dc.BearcliConcurrency,
- Stderr: os.Stderr,
- DailyDefaultTag: dailyDefaultTagFromCatalog(cat),
- PromotionRules: promotionRulesFromCatalog(cat),
- },
+ Domains: domains,
+ Pins: pins,
+ StatePath: dc.StatePath,
+ LockPath: dc.LockPath,
+ Features: cliutil.ResolveFeatures(cat, dc),
+ AuditEnabled: dc.AuditEnabled,
+ BearcliConcurrency: dc.BearcliConcurrency,
+ Stderr: os.Stderr,
+ DailyDefaultTag: dailyDefaultTagFromCatalog(cat),
+ PromotionRules: promotionRulesFromCatalog(cat),
BearDBDir: bearDBDir,
DebouncePause: dc.DebouncePause,
MaxBurstWindow: dc.MaxBurstWindow,
diff --git a/go.mod b/go.mod
index b23b2bf..e3d6dc2 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,13 @@
module github.com/barad1tos/noxctl
-go 1.26.2
+go 1.27
require (
github.com/BurntSushi/toml v1.6.0
github.com/fsnotify/fsnotify v1.10.1
github.com/spf13/cobra v1.10.2
- golang.org/x/sys v0.48.0
golang.org/x/sync v0.23.0
+ golang.org/x/sys v0.48.0
)
require (
diff --git a/go.sum b/go.sum
index 09e7222..0260809 100644
--- a/go.sum
+++ b/go.sum
@@ -12,9 +12,8 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
-golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
-golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
-
+golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
+golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/tests/bear/config/daemon_test.go b/tests/bear/config/daemon_test.go
index a019ec6..23065f1 100644
--- a/tests/bear/config/daemon_test.go
+++ b/tests/bear/config/daemon_test.go
@@ -34,7 +34,7 @@ type loadDaemonCase[V any] struct {
//
// Closure-capture safety: `c` is captured by the `t.Run` closure, which
// would alias to the last iteration on Go < 1.22. The module pins
-// `go 1.26.2` in go.mod, so per-iteration scoping is guaranteed by the
+// `go 1.27` in go.mod, so per-iteration scoping is guaranteed by the
// language spec and no defensive `c := c` shadow is needed.
func runLoadDaemonCases[V any](
t *testing.T,
diff --git a/tests/bear/engine/daemon_test.go b/tests/bear/engine/daemon_test.go
index baae52c..d0c9a7e 100644
--- a/tests/bear/engine/daemon_test.go
+++ b/tests/bear/engine/daemon_test.go
@@ -80,14 +80,12 @@ func newTestDaemonOpts(t *testing.T) engine.DaemonOpts {
t.Helper()
dir := t.TempDir()
return engine.DaemonOpts{
- ApplyOpts: engine.ApplyOpts{
- Domains: nil,
- Pins: nil,
- StatePath: filepath.Join(dir, "state.json"),
- LockPath: filepath.Join(dir, ".lock"),
- Features: engine.Features{}, // all false — no bearcli traffic
- Stderr: os.Stderr,
- },
+ Domains: nil,
+ Pins: nil,
+ StatePath: filepath.Join(dir, "state.json"),
+ LockPath: filepath.Join(dir, ".lock"),
+ Features: engine.Features{}, // all false — no bearcli traffic
+ Stderr: os.Stderr,
BearDBDir: dir,
DebouncePause: 50 * time.Millisecond,
MaxBurstWindow: 500 * time.Millisecond,