Skip to content

PRD-020 + PRD-019: apiary state root and project-scoped brooding activation#21

Merged
thenotoriousllama merged 3 commits into
mainfrom
feature/apiary-root-and-activation
Jul 4, 2026
Merged

PRD-020 + PRD-019: apiary state root and project-scoped brooding activation#21
thenotoriousllama merged 3 commits into
mainfrom
feature/apiary-root-and-activation

Conversation

@thenotoriousllama

@thenotoriousllama thenotoriousllama commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Two PRDs on one branch (019 builds on 020's root helper).

PRD-020 (ADR-0003/0005):

  • Nectar state moves to ~/.apiary/nectar/ via resolveApiaryRoot (absolute-only env roots, never cwd); pid/lock, nectar.json, pending-reviews, telemetry SQLite, usage-telemetry state all follow; one-time idempotent migration with ENOENT-gated legacy fallbacks and boot crash safety.
  • Service units pin APIARY_HOME; launchd logs under ~/.apiary/nectar/logs; doctor-registry window writes with resolved absolute paths; fail-soft boot on a malformed registry (install verb stays fail-loud).

PRD-019 (the original ingestion-scope fix):

  • The daemon is DORMANT BY DEFAULT: zero bound projects = no brood, no watch, no cwd fallback ever; /health reports why.
  • Multi-root: per-project brood+watch contexts from the shared ~/.deeplake/projects.json bindings, reconciled on the poll cadence and on toggle; pathological-root guard ($HOME, drive roots, System32).
  • Brooding on/off: ~/.apiary/nectar/projects.json store, GET/POST /api/hive-graph/projects* (loopback, permission-gated, redacted errors), nectar projects / nectar brooding CLI verbs.
  • Ignore parity: dependency-free .gitignore parser for non-git roots (ReDoS-hardened with bounded-time regression tests), shared predicate on every CLI discovery path, per-project projection pre-warm (PRD-011b rewired).

Close-out (superproject ledger: library/ledger/EXECUTION_LEDGER-apiary-state-root.md)

  • Security audit: 1 High (ReDoS) + 1 Low fixed in place; record at library/qa/security/2026-07-04-security-audit.md.
  • QA: both PRDs passed; all 8 Warnings remediated. Reports in both PRD qa/ folders. Hive-side 019c consumer verified field-for-field against the API.
  • Gates: build + typecheck clean; non-live suite 716 tests / 713 pass / 0 fail / 3 platform skips.
  • Live Deeplake probes: environment-gated on fresh-workspace ingest-visibility lag (manual SQL proves the query path correct); final proof rides this PR's CI against the warm nectar_ci workspace.

Test plan

  • Non-live suite green, hermetic home proven
  • API contract matches hive wire consumer
  • CI green on this PR (includes the live Deeplake leg)

Summary by CodeRabbit

  • New Features
    • Added project dashboard support and brooding controls (view projects; toggle brooding per project or globally) via new CLI commands and daemon endpoints.
    • Introduced multi-root “active projects” handling and expanded /health to include active, paused, and refused project details.
    • Moved to a fleet-root state layout with automated migration from legacy locations.
  • Bug Fixes
    • Improved .gitignore/ignore fallback behavior and default ignoring during discovery.
    • Standardized telemetry/install-id and brooding-state persistence to use the correct current/legacy paths during migration.
  • Security
    • Mitigated glob/regex ReDoS risks and tightened unsafe-root protections (including absolute-only environment path handling) for safer activation and cleaner error responses.

…oding

PRD-020 (ADR-0003/0005): nectar state moves to ~/.apiary/nectar/ via
the shared resolveApiaryRoot chain (absolute-only env roots), one-time
idempotent migration with legacy fallbacks, service-unit adoption, and
doctor-registry window writes with absolute advertised paths.

PRD-019: the daemon is dormant by default and multi-root. It broods
and watches only bound, brooding-enabled projects from the shared
folder bindings, reconciled on the poll cadence and on toggle; adds
the brooding on/off store (~/.apiary/nectar/projects.json), the
GET/POST /api/hive-graph/projects endpoints, nectar projects/brooding
CLI verbs, the pathological-root guard, a dependency-free gitignore
parser for non-git roots (ReDoS-hardened), per-project projection
pre-warm, and shared-ignore parity on every CLI discovery path.

Security audit fixed 1 High (ReDoS) in place; QA passed both PRDs
with all warnings remediated (716 non-live tests, 0 fail). Live
Deeplake probes are environment-gated (fresh-workspace visibility
lag; see the superproject ledger investigation note).
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds fleet-root path resolution and migration, multi-root active-project brooding control with /health, API and CLI wiring, a disk-backed .gitignore fallback, service installer APIARY_HOME pinning, and QA/security documentation.

Changes

Fleet root path resolution and migration

Layer / File(s) Summary
apiary root resolution
src/apiary-root.ts, src/index.ts, library/knowledge/private/architecture/ADR-0005-*.md, test/apiary-root.test.ts
Adds resolveApiaryRoot, nectarStateDir, and legacyRuntimeDir, re-exports them, and updates the ADR resolution/registry contract.
config and config-file wiring
src/config.ts, src/config-file.ts, test/config.test.ts, test/fixes.test.ts
Default runtime/config paths now derive from fleet-root state locations with legacy-dir fallback resolution.
doctor registry and telemetry paths
src/doctor-registry.ts, src/telemetry/db.ts, test/doctor-registry.test.ts, test/telemetry/db.test.ts
Doctor registry and telemetry database defaults now prefer the fleet root, with legacy fallbacks retained.
telemetry usage migration
src/telemetry-usage/emit.ts, test/telemetry-usage.test.ts
Install-id and ledger resolution become migration-aware, and opt-out parsing accepts more falsy forms.

One-time state migration

Layer / File(s) Summary
state migration implementation
src/state-migration.ts, test/state-migration.test.ts
Adds migration markers, read-path selection, legacy-daemon guards, atomic file moves, and fail-soft registry refresh behavior.

Multi-root active projects and brooding control

Layer / File(s) Summary
brooding state store
src/registration/brooding-state.ts, test/brooding-state.test.ts
Adds fail-soft brooding-state loading, atomic writes, and effective brooding helpers.
active-project resolution and health
src/hive-graph/active-projects.ts, test/active-projects.test.ts
Adds pathological-root guards, active/refused project resolution, and /health slice generation.
project supervisor and controller
src/project-supervisor.ts, src/active-projects-runtime.ts, test/project-supervisor.test.ts
Starts and stops per-project contexts and drives reconcile/health publishing.
per-project running context
src/registration/project-context.ts, test/project-context.test.ts
Adds projection pre-warm, auto-brood, and registration-service lifecycle wiring per project.
health model and daemon wiring
src/health.ts, src/daemon.ts, test/daemon-active-projects.test.ts
Extends /health with active-project state and wires the controller into daemon startup/shutdown.
projects API and client
src/projects-control.ts, src/api/projects-api.ts, src/api/loopback-client.ts, test/projects-api.test.ts
Adds GET/POST /api/hive-graph/projects* endpoints and loopback client helpers.
CLI commands and boot rewiring
src/cli.ts, test/projects-cli.test.ts
Adds nectar projects/brooding commands and rewires daemon boot for multi-root activation.

Dependency-free gitignore fallback

Layer / File(s) Summary
gitignore compiler and disk loader
src/registration/gitignore.ts
Compiles gitignore rules with globstar-safe matching and nested-file loading.
shared ignore fallback wiring
src/registration/ignore.ts, src/registration/disk-fs.ts
Consults the fallback only when git is absent and defaults disk registration to the shared ignore predicate.
gitignore and ignore tests
test/gitignore.test.ts
Covers glob semantics, ReDoS bounds, nested ignore precedence, and git-present/absent behavior.

Service installer APIARY_HOME pinning

Layer / File(s) Summary
service plan and templates
src/service/platform.ts, src/service/templates.ts, src/service/argv.ts, src/service/index.ts
Propagates apiaryHome into launchd/systemd/schtasks artifacts and service staging paths.
service installer tests
test/service-*.test.ts
Asserts APIARY_HOME appears in generated service artifacts and resolved plans.

Security audit and QA reports

Layer / File(s) Summary
security audit and QA documents
library/qa/security/*.md, library/requirements/backlog/prd-019.../qa/*.md, library/requirements/backlog/prd-020.../qa/*.md
Adds security audit and PRD-019/020 QA reports with findings, scorecards, traceability, and file inventories.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ProjectsApi
  participant BroodingState
  participant ProjectSupervisor
  participant HealthState

  CLI->>ProjectsApi: POST /api/hive-graph/projects/brooding
  ProjectsApi->>ProjectsApi: parseBroodingToggle(body)
  ProjectsApi->>BroodingState: persistProjectBrooding/persistGlobalBrooding
  ProjectsApi->>ProjectSupervisor: reconcile()
  ProjectSupervisor->>HealthState: setActiveProjects(slice)
  ProjectsApi-->>CLI: updated ProjectsView
Loading

Possibly related issues

Possibly related PRs

  • legioncodeinc/nectar#10: The registry and fleet-root path changes overlap with registry writer path-resolution work in this PR.
  • legioncodeinc/nectar#11: Both changes touch telemetry database default-path resolution and runtime-dir plumbing.

Poem

A rabbit hops through apiary doors,
To new-root nests and brooding scores. 🐰
Gitignore hums, paths stay true,
Projects wake in /health anew.
With carrots, code, and careful light,
The burrow’s path feels just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main themes: apiary state-root migration and project-scoped brooding activation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/apiary-root-and-activation

Comment @coderabbitai help to get the list of available commands.

…index busy-loop, non-gitignore exclude discoverability

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

🧹 Nitpick comments (13)
test/gitignore.test.ts (1)

32-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding regression coverage for the escaped-negation and chained-* cases flagged in gitignore.ts.

The existing negation test (line 32-36) and the globstar ReDoS regression tests (line 44-92) are solid, but neither covers a leading \!/\# escape (compileRule) nor a chained-single-* pathological pattern. Once those are fixed, tests like compileGitignore(["\\!important"]) and a chained-* timing test analogous to the existing globstar one would pin both fixes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gitignore.test.ts` around lines 32 - 92, Add regression tests in
gitignore.test.ts for the missing escape and single-star cases mentioned in the
review. Extend the existing compileGitignore/compileRule coverage by asserting
that leading escaped literals like \! and \# are treated as normal filenames,
not negation/comment markers, and add a bounded-time pathological pattern test
for chained single-* behavior similar to the existing collapseGlobStars timing
test. Use the existing test structure around compileGitignore and the
security-regression block to keep the new cases close to the related logic.
src/service/platform.ts (1)

104-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate nonBlank helper.

This trims-and-blanks-to-undefined helper duplicates nonBlank already defined in src/apiary-root.ts (used there for APIARY_HOME/XDG_STATE_HOME). Consider exporting the one from apiary-root.ts and importing it here to avoid two independently-maintained copies of the same normalization logic.

♻️ Suggested consolidation
-function nonBlank(value: string | undefined): string | undefined {
-  if (value === undefined) return undefined;
-  const trimmed = value.trim();
-  if (trimmed === "") return undefined;
-  return trimmed;
-}
+import { nonBlank } from "./apiary-root.js";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/service/platform.ts` around lines 104 - 108, Duplicate trims-to-undefined
helper: the local nonBlank in platform.ts repeats the normalization logic
already defined in apiary-root.ts. Consolidate by exporting nonBlank from
apiary-root.ts and importing it here, then update any call sites in the platform
service to use the shared helper so APIARY_HOME/XDG_STATE_HOME normalization
stays consistent in one place.
src/hive-graph/active-projects.ts (1)

78-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant/dead outer guard.

platform (Line 66) is always defined (options.platform ?? process.platform never yields undefined), so options.home !== undefined || platform !== undefined is always true; the inner home !== undefined check on Line 80 already does the real work. Harmless but confusing.

♻️ Simplify
-  // $HOME.
-  if (options.home !== undefined || platform !== undefined) {
-    const home = options.home;
-    if (home !== undefined && home.trim() !== "" && norm === normalizeForCompare(home, platform)) return true;
-  }
+  // $HOME.
+  if (options.home !== undefined && options.home.trim() !== "" && norm === normalizeForCompare(options.home, platform)) {
+    return true;
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hive-graph/active-projects.ts` around lines 78 - 81, Remove the redundant
outer guard in active-projects logic: in the code that checks `options.home` and
`platform` before comparing against `normalizeForCompare`, `platform` is always
defined via the existing `options.platform ?? process.platform` assignment, so
the `options.home !== undefined || platform !== undefined` condition is dead.
Simplify the control flow by relying on the existing `home !== undefined` check
inside that block and keep the comparison logic in the same
`normalizeForCompare`/`norm` path.
src/project-supervisor.ts (1)

85-129: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

No bounded timeout on ctx.start()/ctx.stop().

Unlike daemon.ts's own drain path (which bounds waits via raceWithTimeout), reconcileNow and stopAll await a project's start()/stop() with no timeout. A single hung RunningContext (e.g. a stalled durable-store flush in project-context.ts's stop()) stalls the serialized queue indefinitely, blocking reconciliation — and shutdown — for every other project too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/project-supervisor.ts` around lines 85 - 129, The reconciliation flow in
reconcileNow and stopAll can hang forever because ctx.start() and ctx.stop() are
awaited without any deadline. Update the ProjectSupervisor logic to wrap both
lifecycle calls with the same bounded timeout approach used in daemon.ts (for
example via raceWithTimeout), and make sure the timeout handling still reports
failures through onError while preserving the serialized queue behavior.
test/project-supervisor.test.ts (1)

39-104: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No coverage for a throwing factory.

Given the finding on project-supervisor.ts (an unguarded factory() call can wedge the reconcile queue), a test exercising factory throwing synchronously would have caught the regression and should guard against it going forward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/project-supervisor.test.ts` around lines 39 - 104, Add a regression test
in ProjectSupervisor’s reconcile coverage for a synchronous throw from the
factory callback. Extend the existing reconcile tests around ProjectSupervisor,
reconcile, and fakeContext by using a factory that throws on context creation,
then assert the reconcile call does not wedge the queue, surfaces the error
through onError, and leaves existing contexts/state consistent for a subsequent
reconcile.
src/registration/project-context.ts (1)

216-225: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

bridge.hydrate is not guarded by a closed check before it runs.

If stop() races in between context creation (line 191-215) and this await bridge.hydrate(tenancy) call, hydrate still executes even though closed was already set — the closed check only happens afterward (Line 221), before service.start(). In practice the supervisor's serialized reconcile queue and PollLoop.whenIdle() in ActiveProjectsController.stop() should prevent concurrent start/stop on the same context, so this is a narrow, likely-unreachable race rather than an active bug today — worth a defensive check if that invariant ever changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/registration/project-context.ts` around lines 216 - 225, The hydrate path
in project-context startup can still run after the context has been closed
because the closed guard only happens after bridge.hydrate(tenancy). Add a
defensive closed check in the startup flow around the bridge.hydrate call in the
project-context logic so that hydration is skipped when stop() has already
marked the context closed, and keep the existing post-hydrate closed check
before service.start() as well.
src/api/loopback-client.ts (1)

107-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

DaemonSearchError is now a misnomer for the projects/brooding endpoints.

daemonJsonRequest is generic and shared by projectsViaDaemon/setBroodingViaDaemon, but non-2xx/non-JSON failures still throw DaemonSearchError, a name coined for the search endpoint. Purely cosmetic today since callers don't branch on it, but worth a more generic name (e.g. DaemonHttpError) before more endpoints reuse this helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/loopback-client.ts` around lines 107 - 147, The shared
daemonJsonRequest helper currently throws DaemonSearchError for non-2xx and
non-JSON responses, but this path is used by projectsViaDaemon and
setBroodingViaDaemon too. Rename the error type to a generic daemon HTTP error
name (for example DaemonHttpError) and update the throw sites inside
daemonJsonRequest to use the new class so the naming matches its broader use.
src/cli.ts (1)

921-957: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

nectar brooding <on|off> --all has no partial-failure handling.

The --all branch (lines 936-943) issues one setBroodingViaDaemon POST per project sequentially inside the try. If the daemon errors or times out partway through, the command exits via the outer catch having already toggled some projects but not others, with no summary of which succeeded — the user has no way to know the resulting state without re-running nectar projects.

♻️ Proposed fix: isolate per-project failures and report a summary
     } else {
       // --all: set every currently-bound project to the requested state.
       const current = await projectsViaDaemon(target);
       view = current;
-      for (const p of current.projects) {
-        view = await setBroodingViaDaemon(target, { projectId: p.projectId, brooding: invocation.brooding });
-      }
+      const failures: string[] = [];
+      for (const p of current.projects) {
+        try {
+          view = await setBroodingViaDaemon(target, { projectId: p.projectId, brooding: invocation.brooding });
+        } catch (err: unknown) {
+          failures.push(`${p.projectId}: ${err instanceof Error ? err.message : String(err)}`);
+        }
+      }
+      if (failures.length > 0) {
+        process.stderr.write(`nectar brooding: failed to update ${failures.length} project(s):\n  ${failures.join("\n  ")}\n`);
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 921 - 957, The `runBroodingCommand` `--all` path
currently aborts on the first `setBroodingViaDaemon` failure, leaving projects
partially updated with no summary. Update the `invocation.kind === "all"` branch
to handle each project independently, capturing successes and failures around
`setBroodingViaDaemon`, then print a concise summary of which projectIds were
updated and which failed before returning an appropriate exit code. Keep the
existing `DaemonUnreachableError` handling in the outer `catch`, but make
per-project errors in the `--all` flow non-fatal until the end.
library/knowledge/private/architecture/ADR-0005-fleet-directory-ownership-and-neutral-state-root.md (2)

41-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same citation gap for the registry-path-string claim.

The claim that "nectar's entry builder already writes resolved absolute paths" is stated without a path:line citation, inconsistent with the document's own citation convention used in the References section.

As per coding guidelines, library/knowledge/**/*.md: "Ground every claim with a source citation that includes a file path and line range, and never paraphrase signatures."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@library/knowledge/private/architecture/ADR-0005-fleet-directory-ownership-and-neutral-state-root.md`
around lines 41 - 42, The registry-path-string claim in the ADR is missing a
required source citation, so update the “Registry path strings” paragraph to
include a path:line reference for the statement that nectar’s entry builder
writes resolved absolute paths. Use the same citation style already used
elsewhere in the document, and anchor the citation to the relevant source symbol
or section so the claim is grounded without paraphrasing.

Source: Coding guidelines


22-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing source citations for the security-amendment claims.

The security amendment (line 30) asserts implementation facts ("Absoluteness is checked with path.win32.isAbsolute") and audit provenance ("from the W3 security audits") without a file-path/line-range citation, unlike the References section elsewhere in this document which uses path:line format.

As per coding guidelines, library/knowledge/**/*.md: "Ground every claim with a source citation that includes a file path and line range, and never paraphrase signatures."

📝 Suggested citation addition
-Security amendment (2026-07-04, from the W3 security audits): env roots are honored ONLY when absolute; ... Absoluteness is checked with `path.win32.isAbsolute` (a strict superset of the posix check) so a relative value is never mistaken for absolute on any host.
+Security amendment (2026-07-04, from the W3 security audits): env roots are honored ONLY when absolute; ... Absoluteness is checked with `path.win32.isAbsolute` (`src/apiary-root.ts:33-35`), a strict superset of the posix check, so a relative value is never mistaken for absolute on any host.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@library/knowledge/private/architecture/ADR-0005-fleet-directory-ownership-and-neutral-state-root.md`
around lines 22 - 31, The security amendment in ADR-0005 contains uncited
implementation and provenance claims, so add a proper source citation in the
same style used by the document’s References section. Update the amendment text
near the mention of path.win32.isAbsolute and the W3 security audits to include
a file-path and line-range citation that grounds those facts, using the relevant
ADR/reference source so the claim is traceable without paraphrasing any
signatures.

Source: Coding guidelines

src/service/index.ts (1)

42-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicates launchdLogDir's stateDir logic in src/service/templates.ts.

Same ternary as launchdLogDir (minus the /logs suffix). Consider exporting a single serviceStateDir/nectarStateRoot helper from templates.ts (or a shared module) and having this file consume it, to avoid the two copies drifting apart.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/service/index.ts` around lines 42 - 45, The state directory logic is
duplicated between serviceStateDir in src/service/index.ts and launchdLogDir in
src/service/templates.ts, so update them to use a single shared helper instead
of separate ternaries. Move or export a reusable nectarStateRoot/serviceStateDir
helper from templates.ts (or a shared module), then have serviceStateDir consume
it and append the needed suffix locally so both paths stay in sync.
src/service/templates.ts (1)

32-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate stateDir computation vs. src/service/index.ts.

The apiaryHome !== undefined ? ... : ... ternary here is byte-for-byte duplicated in serviceStateDir() in src/service/index.ts (lines 42-44). Since this is the load-bearing fleet-root resolution logic (per ADR-0005), having two independent copies risks one being updated without the other.

♻️ Suggested consolidation
-export function launchdLogDir(plan: Pick<ServicePlan, "home" | "apiaryHome">): string {
-  const stateDir = plan.apiaryHome !== undefined ? `${plan.apiaryHome}/nectar` : `${plan.home}/.apiary/nectar`;
-  return `${stateDir}/logs`;
-}
+export function serviceStateDir(plan: Pick<ServicePlan, "home" | "apiaryHome">): string {
+  return plan.apiaryHome !== undefined ? `${plan.apiaryHome}/nectar` : `${plan.home}/.apiary/nectar`;
+}
+
+export function launchdLogDir(plan: Pick<ServicePlan, "home" | "apiaryHome">): string {
+  return `${serviceStateDir(plan)}/logs`;
+}

Then have src/service/index.ts import and reuse serviceStateDir from here instead of redefining it locally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/service/templates.ts` around lines 32 - 35, The `stateDir` resolution
logic is duplicated between `launchdLogDir` and `serviceStateDir`, so
consolidate it into a single source of truth. Keep the existing `launchdLogDir`
helper in `src/service/templates.ts`, and update `src/service/index.ts` to
import and reuse `serviceStateDir` from this module instead of redefining the
same `apiaryHome !== undefined ? ... : ...` ternary locally.
src/telemetry-usage/emit.ts (1)

274-288: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated context-resolution block in emitUsageEvent and recordDaemonStart.

Both functions independently compute the same env/dir/version/useLegacyFallback tuple:

const env = deps.env ?? process.env;
const dir = deps.dir ?? defaultStateDir(env);
const version = deps.version ?? readPackageVersion();
const useLegacyFallback = deps.dir === undefined;

Consider extracting a small resolveEmitContext(deps) helper to avoid the two call sites drifting apart as the migration logic evolves further.

Also applies to: 375-390

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/telemetry-usage/emit.ts` around lines 274 - 288, Both emitUsageEvent and
recordDaemonStart duplicate the same context-resolution logic for env, dir,
version, and useLegacyFallback, which risks drift as migration behavior changes.
Extract this repeated block into a small helper such as resolveEmitContext(deps)
and have both emitUsageEvent and recordDaemonStart call it, keeping the existing
readPackageVersion, defaultStateDir, and deps.env/deps.dir fallback behavior
centralized in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@library/qa/security/2026-07-04-security-audit.md`:
- Line 5: The audit note in the scope line exposes a machine-specific absolute
working-tree path; replace it with a repo-relative placeholder or generic
workspace reference while keeping the branch and commit context. Update the
affected markdown entry in the security audit note so the `Scope` text no longer
contains the local `C:/Users/...` path, preserving the same meaning without
leaking environment details.

In
`@library/requirements/backlog/prd-019-project-scoped-brooding-activation/qa/2026-07-04-qa-report-prd-019c-hive-dashboard-surface.md`:
- Line 49: The markdown table row for c-AC-5 is being split into extra columns
because the example text includes an unescaped pipe. Update the report entry in
the QA markdown so the cell content no longer contains a raw table delimiter;
either reword the example in the same row or replace/remove the pipe within the
`hive-graph.tsx` reference text so the table renders correctly.

In
`@library/requirements/backlog/prd-019-project-scoped-brooding-activation/qa/prd-019-project-scoped-brooding-activation-qa.md`:
- Line 9: Update the ordering note to reflect the current repository state,
since `library/qa/security/2026-07-04-security-audit.md` now exists. Edit the
note in `prd-019-project-scoped-brooding-activation-qa.md` so it no longer says
no 2026-07-04 security report file exists under nectar, while preserving the
rest of the ordering context about `security-worker-bee`, `collapseGlobStars`,
and `brooding-state.ts`.

In
`@library/requirements/backlog/prd-020-apiary-state-root-migration/qa/prd-020-apiary-state-root-migration-qa.md`:
- Line 9: Update the ordering note in the QA markdown to reflect the current
tree: the statement about no 2026-07-04 security report under nectar is stale
because `library/qa/security/2026-07-04-security-audit.md` now exists. Revise
that observation in the note to acknowledge the security audit file in the
branch while keeping the rest of the ordering context about
`security-worker-bee`, `src/apiary-root.ts`, `test/apiary-root.test.ts`,
`src/state-migration.ts`, and `src/registration/brooding-state.ts` intact.

In `@src/apiary-root.ts`:
- Around line 25-61: The root resolution logic in
resolveApiaryRoot/isAbsoluteRoot is treating Windows-style paths as absolute on
non-Windows hosts. Update the absolute-path validation to be platform-aware
before accepting APIARY_HOME or XDG_STATE_HOME, so win32.isAbsolute() is only
used when resolving for Windows (or otherwise reject drive-letter/UNC paths
outside Windows) and prevent nectarStateDir from inheriting a cwd-relative root.

In `@src/cli.ts`:
- Around line 1069-1092: The projects/brooding control surface in `src/cli.ts`
is incorrectly gated on `creds !== undefined`, which hides `nectar projects` and
`nectar brooding` even though `readActiveProjects`, `persistProjectBrooding`,
and `persistGlobalBrooding` only depend on the shared JSON state. Update the
`mountProjectsApi` wiring so it is attached unconditionally, while keeping the
active-project supervisor/setup (`activeProjects`, `createProjectContext`, and
related tenancy/store wiring) behind the existing creds/store checks. Also,
avoid calling `readActiveProjects(controlOptions)` twice on startup by resolving
it once and reusing that snapshot for both `primary` and `primaryTenancy`.

In `@src/daemon.ts`:
- Around line 999-1007: The shutdown path around activeProjectsController.stop()
is swallowing failures and leaving running contexts alive. In src/daemon.ts,
update the try/catch around activeProjectsController.stop() to add a defensive
fallback when stop() throws: use the activeProjectsController and its supervisor
to access supervisor.contexts() and force-stop or drain each remaining context
before shutdown completes. Keep the existing logging, but ensure the teardown
still fully stops per-project watchers/bridges even if stop() fails.

In `@src/doctor-registry.ts`:
- Around line 111-116: Create the fleet root before doctor registration:
`runInstall()` currently calls `registerWithDoctor({ config })` before
`~/.apiary` exists, so `defaultDoctorRegistryPath()` can still fall back to the
legacy `legacyRuntimeDir()` path. Update the install flow to ensure the root
from `resolveApiaryRoot()` is created first, or pass the fleet-root registry
path explicitly into `registerWithDoctor` so `doctor-registry` resolves to
`registry.json` instead of `doctor.daemons.json`.

In `@src/hive-graph/active-projects.ts`:
- Line 14: normalizeForCompare and isPathologicalRoot are still using host-bound
node:path behavior, so the injected platform is ignored when comparing or
checking roots. Update these helpers in active-projects.ts to choose path.win32
or path.posix based on the provided platform argument, and ensure all
parsing/resolve logic inside those helpers uses that platform-specific path
implementation so Windows roots like C:\ are handled correctly even on POSIX
hosts.

In `@src/project-supervisor.ts`:
- Around line 90-123: The reconcile flow in reconcileNow is letting a
synchronous failure from factory(project) escape, which can reject the shared
queue and block later reconcile/stopAll work. Wrap the ProjectContextFactory
creation in the same try/catch pattern used for ctx.start() and ctx.stop(), call
onError("start", project.projectId, err) for factory failures, and only add the
context to contextsById after successful creation so one bad project does not
poison future reconciles.

In `@src/registration/gitignore.ts`:
- Around line 104-112: The escaped prefix handling in gitignore parsing is being
reinterpreted as negation, so fix `src/registration/gitignore.ts` in the pattern
normalization flow before the `negate` check. Update the logic around the
leading `\#`/`\!` escape handling and the `line.startsWith("!")` branch so an
escaped `!` remains a literal pattern (`!foo`) and does not set `negate`, while
still preserving the existing behavior for unescaped negation. Keep the change
localized to the parser function that processes each line.
- Around line 46-92: The glob compilation in collapseGlobStars/globBodyToRegex
still leaves ReDoS risk because repeated single-star segments compile to
backtracking regex fragments. Replace the regex-based matching for gitignore
patterns with a linear-time glob matcher, or otherwise ensure `*` handling in
globBodyToRegex cannot backtrack on long non-matching paths. Keep the existing
`collapseGlobStars` behavior for `**` normalization, but update the matching
logic around `globBodyToRegex` to eliminate catastrophic backtracking for
patterns like chained `*a*a*...`.

In `@src/registration/ignore.ts`:
- Line 38: `ignore.ts` currently imports `DiskGitignore` only for local use, so
consumers cannot import it through this module. Update the `ignore.ts` module to
explicitly re-export `DiskGitignore` from `gitignore.js` (alongside the existing
`createDiskGitignore` usage) so external `import type { DiskGitignore } from
"../dist/registration/ignore.js"` works correctly.
- Around line 252-261: Pass the directory flag through the disk ignore check so
`DiskGitignore` can apply `build/`-style rules during descent. Update the
`walk()` logic in `src/registration/disk-fs.ts` to call the ignore predicate
with `isDir` for directory entries, matching the existing
`DiskGitignore`/`gitignoreFallback` contract. This should let ignored
directories be pruned before traversal instead of being treated as plain paths.

In `@src/service/templates.ts`:
- Around line 180-191: The cmd.exe wrapper built in templates.ts uses an unsafe
environment assignment pattern in the windowsCommand branch, so update the
wrapper in the code that constructs windowsCommand to use the canonical quoted
set syntax for APIARY_HOME instead of the current double-quoted form. Make sure
any values passed through the same wrapper, especially node and exec from
process.execPath and plan.execPath, are escaped with cmd-safe quoting so
metacharacters cannot leak into the task command.

In `@src/state-migration.ts`:
- Around line 132-164: The migration loop in state-migration treats a successful
copy plus a failed source cleanup as a full failure; split the
`migrateFile`/`copyThenRename` step from the `rmSync(sourcePath)` cleanup in
`src/state-migration.ts` so `moved` is recorded when the copy succeeds, while
delete errors are handled separately and retried later. Update the `migrateFile`
try/catch around `MIGRATION_RELATIVE_PATHS` to avoid marking the file as
`failed` just because source removal failed, and ensure orphaned legacy files
can still be cleaned up on a later boot. Add a `test/state-migration.test.ts`
case covering `migrateFile` success with a stubbed `rmSync` failure, asserting
the path is in `moved` not `failed` and that a subsequent run clears
`anyLegacyStateExists`.
- Around line 119-210: The state migration flow in runStateMigration can still
overlap across concurrent daemon starts because it runs before
acquireSingleInstanceLock, which leaves the legacy file moves and
registerWithDoctor registry refresh unprotected. Update the startup sequence so
the single-instance lock is acquired before invoking runStateMigration, or
otherwise serialize the shared registry update inside runStateMigration around
registerWithDoctor and the migration file moves. Use the existing
runStateMigration and registerWithDoctor symbols to keep the fix scoped to the
migration/registry refresh path.

In `@src/telemetry-usage/emit.ts`:
- Around line 206-212: The migration fallback in loadLedger currently has no
injectable legacy path seam, so tests can end up resolving against the real home
directory. Update loadLedger to accept and use a legacy runtime directory
dependency, mirroring readInstallId and resolveStateReadPath usage, and thread
deps.legacyRuntimeDir through the emitUsageEvent and recordDaemonStart call
sites when useLegacyFallback is enabled.

In `@test/project-context.test.ts`:
- Around line 87-125: The prewarm hermeticity test is missing the same
filesystem overrides used by the neighboring tests, so createProjectContext may
fall back to real createSharedIgnore and createDiskRegistrationFs behavior.
Update this test to pass the in-memory/shared-ignore overrides when constructing
createProjectContext, using the same registrationFs/sharedIgnore symbols as the
other hermetic cases, so the projection prewarm path does not touch real git or
disk machinery.

---

Nitpick comments:
In
`@library/knowledge/private/architecture/ADR-0005-fleet-directory-ownership-and-neutral-state-root.md`:
- Around line 41-42: The registry-path-string claim in the ADR is missing a
required source citation, so update the “Registry path strings” paragraph to
include a path:line reference for the statement that nectar’s entry builder
writes resolved absolute paths. Use the same citation style already used
elsewhere in the document, and anchor the citation to the relevant source symbol
or section so the claim is grounded without paraphrasing.
- Around line 22-31: The security amendment in ADR-0005 contains uncited
implementation and provenance claims, so add a proper source citation in the
same style used by the document’s References section. Update the amendment text
near the mention of path.win32.isAbsolute and the W3 security audits to include
a file-path and line-range citation that grounds those facts, using the relevant
ADR/reference source so the claim is traceable without paraphrasing any
signatures.

In `@src/api/loopback-client.ts`:
- Around line 107-147: The shared daemonJsonRequest helper currently throws
DaemonSearchError for non-2xx and non-JSON responses, but this path is used by
projectsViaDaemon and setBroodingViaDaemon too. Rename the error type to a
generic daemon HTTP error name (for example DaemonHttpError) and update the
throw sites inside daemonJsonRequest to use the new class so the naming matches
its broader use.

In `@src/cli.ts`:
- Around line 921-957: The `runBroodingCommand` `--all` path currently aborts on
the first `setBroodingViaDaemon` failure, leaving projects partially updated
with no summary. Update the `invocation.kind === "all"` branch to handle each
project independently, capturing successes and failures around
`setBroodingViaDaemon`, then print a concise summary of which projectIds were
updated and which failed before returning an appropriate exit code. Keep the
existing `DaemonUnreachableError` handling in the outer `catch`, but make
per-project errors in the `--all` flow non-fatal until the end.

In `@src/hive-graph/active-projects.ts`:
- Around line 78-81: Remove the redundant outer guard in active-projects logic:
in the code that checks `options.home` and `platform` before comparing against
`normalizeForCompare`, `platform` is always defined via the existing
`options.platform ?? process.platform` assignment, so the `options.home !==
undefined || platform !== undefined` condition is dead. Simplify the control
flow by relying on the existing `home !== undefined` check inside that block and
keep the comparison logic in the same `normalizeForCompare`/`norm` path.

In `@src/project-supervisor.ts`:
- Around line 85-129: The reconciliation flow in reconcileNow and stopAll can
hang forever because ctx.start() and ctx.stop() are awaited without any
deadline. Update the ProjectSupervisor logic to wrap both lifecycle calls with
the same bounded timeout approach used in daemon.ts (for example via
raceWithTimeout), and make sure the timeout handling still reports failures
through onError while preserving the serialized queue behavior.

In `@src/registration/project-context.ts`:
- Around line 216-225: The hydrate path in project-context startup can still run
after the context has been closed because the closed guard only happens after
bridge.hydrate(tenancy). Add a defensive closed check in the startup flow around
the bridge.hydrate call in the project-context logic so that hydration is
skipped when stop() has already marked the context closed, and keep the existing
post-hydrate closed check before service.start() as well.

In `@src/service/index.ts`:
- Around line 42-45: The state directory logic is duplicated between
serviceStateDir in src/service/index.ts and launchdLogDir in
src/service/templates.ts, so update them to use a single shared helper instead
of separate ternaries. Move or export a reusable nectarStateRoot/serviceStateDir
helper from templates.ts (or a shared module), then have serviceStateDir consume
it and append the needed suffix locally so both paths stay in sync.

In `@src/service/platform.ts`:
- Around line 104-108: Duplicate trims-to-undefined helper: the local nonBlank
in platform.ts repeats the normalization logic already defined in
apiary-root.ts. Consolidate by exporting nonBlank from apiary-root.ts and
importing it here, then update any call sites in the platform service to use the
shared helper so APIARY_HOME/XDG_STATE_HOME normalization stays consistent in
one place.

In `@src/service/templates.ts`:
- Around line 32-35: The `stateDir` resolution logic is duplicated between
`launchdLogDir` and `serviceStateDir`, so consolidate it into a single source of
truth. Keep the existing `launchdLogDir` helper in `src/service/templates.ts`,
and update `src/service/index.ts` to import and reuse `serviceStateDir` from
this module instead of redefining the same `apiaryHome !== undefined ? ... :
...` ternary locally.

In `@src/telemetry-usage/emit.ts`:
- Around line 274-288: Both emitUsageEvent and recordDaemonStart duplicate the
same context-resolution logic for env, dir, version, and useLegacyFallback,
which risks drift as migration behavior changes. Extract this repeated block
into a small helper such as resolveEmitContext(deps) and have both
emitUsageEvent and recordDaemonStart call it, keeping the existing
readPackageVersion, defaultStateDir, and deps.env/deps.dir fallback behavior
centralized in one place.

In `@test/gitignore.test.ts`:
- Around line 32-92: Add regression tests in gitignore.test.ts for the missing
escape and single-star cases mentioned in the review. Extend the existing
compileGitignore/compileRule coverage by asserting that leading escaped literals
like \! and \# are treated as normal filenames, not negation/comment markers,
and add a bounded-time pathological pattern test for chained single-* behavior
similar to the existing collapseGlobStars timing test. Use the existing test
structure around compileGitignore and the security-regression block to keep the
new cases close to the related logic.

In `@test/project-supervisor.test.ts`:
- Around line 39-104: Add a regression test in ProjectSupervisor’s reconcile
coverage for a synchronous throw from the factory callback. Extend the existing
reconcile tests around ProjectSupervisor, reconcile, and fakeContext by using a
factory that throws on context creation, then assert the reconcile call does not
wedge the queue, surfaces the error through onError, and leaves existing
contexts/state consistent for a subsequent reconcile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ec2238b-4e37-4ea7-836b-057a32cd7762

📥 Commits

Reviewing files that changed from the base of the PR and between d3b19e2 and 65e9c35.

📒 Files selected for processing (50)
  • library/knowledge/private/architecture/ADR-0005-fleet-directory-ownership-and-neutral-state-root.md
  • library/qa/security/2026-07-04-security-audit.md
  • library/requirements/backlog/prd-019-project-scoped-brooding-activation/qa/2026-07-04-qa-report-prd-019c-hive-dashboard-surface.md
  • library/requirements/backlog/prd-019-project-scoped-brooding-activation/qa/prd-019-project-scoped-brooding-activation-qa.md
  • library/requirements/backlog/prd-020-apiary-state-root-migration/qa/prd-020-apiary-state-root-migration-qa.md
  • src/active-projects-runtime.ts
  • src/api/loopback-client.ts
  • src/api/projects-api.ts
  • src/apiary-root.ts
  • src/cli.ts
  • src/config-file.ts
  • src/config.ts
  • src/daemon.ts
  • src/doctor-registry.ts
  • src/health.ts
  • src/hive-graph/active-projects.ts
  • src/index.ts
  • src/project-supervisor.ts
  • src/projects-control.ts
  • src/registration/brooding-state.ts
  • src/registration/disk-fs.ts
  • src/registration/gitignore.ts
  • src/registration/ignore.ts
  • src/registration/project-context.ts
  • src/service/argv.ts
  • src/service/index.ts
  • src/service/platform.ts
  • src/service/templates.ts
  • src/state-migration.ts
  • src/telemetry-usage/emit.ts
  • src/telemetry/db.ts
  • test/active-projects.test.ts
  • test/apiary-root.test.ts
  • test/brooding-state.test.ts
  • test/config.test.ts
  • test/daemon-active-projects.test.ts
  • test/doctor-registry.test.ts
  • test/fixes.test.ts
  • test/gitignore.test.ts
  • test/project-context.test.ts
  • test/project-supervisor.test.ts
  • test/projects-api.test.ts
  • test/projects-cli.test.ts
  • test/service-argv.test.ts
  • test/service-index.test.ts
  • test/service-platform.test.ts
  • test/service-templates.test.ts
  • test/state-migration.test.ts
  • test/telemetry-usage.test.ts
  • test/telemetry/db.test.ts

Comment thread library/qa/security/2026-07-04-security-audit.md
Comment thread src/apiary-root.ts
Comment thread src/service/templates.ts
Comment thread src/state-migration.ts
Comment thread src/state-migration.ts
Comment thread src/telemetry-usage/emit.ts
Comment thread test/project-context.test.ts
… platform's path semantics

isPathologicalRoot parsed paths with the HOST node:path module, so a
win32-shaped fixture (C:\) evaluated on a POSIX CI host was not recognized
as a drive root (posix parse of "C:\" has an empty root), failing the guard
test on macOS/ubuntu while passing on Windows. The guard now selects
path.win32 or path.posix from the injected platform, keeps $HOME comparison
case-sensitive on POSIX (case-folded only on win32), and makes the
WINDIR\System32 leg Windows-only so a POSIX directory that merely spells
/Windows/System32 is never refused. Also defaults the $HOME guard to
os.homedir() so the production daemon (which passes no explicit home)
actually guards the real home directory per the PRD resolved decisions.
Adds per-platform expectation tests covering both hosts.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/active-projects.test.ts`:
- Line 129: The isPathologicalRoot test is using the host process.env.WINDIR
implicitly instead of a deterministic fixture, so make this assertion pass an
explicit env object that represents WINDIR unset. Update the test case in
active-projects.test.ts alongside the isPathologicalRoot call to use a
controlled env fixture, ensuring the win32 fallback behavior is validated
independently of the machine running the suite.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 239ea114-b013-4d9c-86b8-f3a9cbaed8b9

📥 Commits

Reviewing files that changed from the base of the PR and between 3676b55 and 3edba93.

📒 Files selected for processing (2)
  • src/hive-graph/active-projects.ts
  • test/active-projects.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hive-graph/active-projects.ts

Comment thread test/active-projects.test.ts
@thenotoriousllama thenotoriousllama merged commit 0585b34 into main Jul 4, 2026
6 checks passed
@thenotoriousllama thenotoriousllama deleted the feature/apiary-root-and-activation branch July 4, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant