diff --git a/src/cli.ts b/src/cli.ts index f75b866..7741535 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1335,6 +1335,12 @@ async function runDaemon(): Promise { log: (line) => process.stderr.write(`${JSON.stringify({ ts: new Date().toISOString(), ...line })}\n`), }), controlOptions: liveControlOptions, + // PRD-018k / NEC-023: gate the WHOLE brood/watch activation on the brood prerequisites, not + // just the describe deps. Brooding is dormant out of the box: without Deep Lake creds AND + // Portkey enabled, resolve() reports zero active projects, so the supervisor auto-broods + // nothing (previously the structural brood — walk/mint/embed — still ran for any bound + // project even with no inference configured, pegging CPU to no purpose). + broodReady: () => broodPrereqs.ready, onError: (err) => process.stderr.write( `nectar daemon: active-project resolve failed (non-fatal): ${err instanceof Error ? err.message : String(err)}\n`, diff --git a/src/hive-graph/live-active-projects.ts b/src/hive-graph/live-active-projects.ts index 3766034..f0f7fa6 100644 Binary files a/src/hive-graph/live-active-projects.ts and b/src/hive-graph/live-active-projects.ts differ diff --git a/test/live-active-projects.test.ts b/test/live-active-projects.test.ts index c370adb..b16a283 100644 --- a/test/live-active-projects.test.ts +++ b/test/live-active-projects.test.ts @@ -86,6 +86,43 @@ test("W1-N: a projects.json binding written AFTER boot is picked up on the next } }); +test("NEC-023: brood prerequisites unmet (no Portkey) => dormant even with credentials + a binding", () => { + const cacheDir = tempDir("nectar-live-cache-"); + const stateDir = tempDir("nectar-live-state-"); + let broodReady = false; + const live = new LiveActiveProjects({ + loadCredentials: () => CREDS, + createStore: () => fakeStore, + buildContext: ({ project }): RunningContext => ({ + projectId: project.projectId, + path: project.path, + watcherState: () => "running", + start: async () => {}, + stop: async () => {}, + }), + controlOptions: { cacheDir, broodingState: { dir: stateDir } }, + // The Portkey/creds prereq gate. False => the daemon must NOT auto-brood. + broodReady: () => broodReady, + }); + + try { + // Credentials resolve AND a project is bound, but Portkey is not enabled + // (broodReady=false) => brooding is dormant out of the box: zero active projects, + // so the supervisor stands up no brood/watch context (the auto-brood-without-Portkey fix). + writeProjectsCache(cacheDir, CREDS.orgId, CREDS.workspaceId, [{ path: "/work/repo-a", projectId: "proj-a" }]); + assert.equal(live.resolve().active.length, 0, "no Portkey => dormant even with a binding present"); + + // Once the prereqs are satisfied (Portkey configured), the SAME binding activates. + broodReady = true; + const after = live.resolve(); + assert.equal(after.active.length, 1, "prereqs satisfied => the binding activates"); + assert.equal(after.active[0].projectId, "proj-a"); + } finally { + rmSync(cacheDir, { recursive: true, force: true }); + rmSync(stateDir, { recursive: true, force: true }); + } +}); + test("W1-N: with credentials absent the resolver is dormant and fail-soft (empty resolution, factory is a no-op)", async () => { const cacheDir = tempDir("nectar-live-cache-"); const stateDir = tempDir("nectar-live-state-");