gnosys 5.14.0 → 5.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1097,6 +1097,12 @@ program
1097
1097
  console.error(`\nCould not write upgrade marker: ${err instanceof Error ? err.message : err}`);
1098
1098
  console.error(`Running MCP servers will need to be restarted manually.`);
1099
1099
  }
1100
+ // v5.15: a Node upgrade moves the node path hardcoded in the dream
1101
+ // LaunchAgent plist, silently killing the scheduler. Repair it here.
1102
+ const { repairDreamLaunchAgentAfterUpgrade } = await import("./lib/dreamLaunchd.js");
1103
+ const dreamRepairLine = await repairDreamLaunchAgentAfterUpgrade();
1104
+ if (dreamRepairLine)
1105
+ console.log(`\n${dreamRepairLine}`);
1100
1106
  if (opts.sync === false || opts.yes) {
1101
1107
  console.log(`\nDone. Run 'gnosys setup sync-projects' when you're ready to refresh registered projects.`);
1102
1108
  return;
package/dist/index.js CHANGED
@@ -1703,7 +1703,8 @@ regTool("gnosys_import", "Bulk import structured data (CSV, JSON, JSONL) into Gn
1703
1703
  skipExisting,
1704
1704
  limit,
1705
1705
  offset,
1706
- concurrency,
1706
+ // v5.15: explicit param wins; otherwise config importConcurrency.
1707
+ concurrency: concurrency ?? ctx.config?.importConcurrency,
1707
1708
  batchCommit: true,
1708
1709
  });
1709
1710
  // Reindex after import
package/dist/lib/ask.js CHANGED
@@ -289,7 +289,11 @@ export class GnosysAsk {
289
289
  }
290
290
  return usedArchiveIds;
291
291
  }
292
- catch {
292
+ catch (err) {
293
+ // Degrade gracefully, but leave a trace on stderr — a hard dearchive
294
+ // failure previously looked identical to "no archive hits"
295
+ // (sprint 2026-07-02).
296
+ console.error(`gnosys: dearchive failed: ${err instanceof Error ? err.message : String(err)}`);
293
297
  return [];
294
298
  }
295
299
  }
@@ -132,7 +132,10 @@ export async function runAskCommand(getResolver, question, opts) {
132
132
  const writeTarget = resolver.getWriteTarget();
133
133
  if (writeTarget) {
134
134
  const { GnosysMaintenanceEngine } = await import("./maintenance.js");
135
- await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, result.sources.map((s) => s.relativePath)).catch(() => { });
135
+ await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, result.sources.map((s) => s.relativePath)).catch((err) => {
136
+ // Best-effort, but don't be fully silent (sprint 2026-07-02).
137
+ console.error(`gnosys: reinforcement skipped: ${err instanceof Error ? err.message : String(err)}`);
138
+ });
136
139
  }
137
140
  }
138
141
  }
@@ -42,6 +42,15 @@ export interface LLMTurnResult {
42
42
  result: string;
43
43
  }>;
44
44
  }
45
+ /**
46
+ * Build the system prompt for a chat turn.
47
+ *
48
+ * v5.15: wires two previously-unread config knobs —
49
+ * `chat.systemPromptPrefix` (prepended before the base prompt) and
50
+ * `chat.toolsEnabled` (false omits the gnosys-tool addendum entirely).
51
+ * Exported for tests.
52
+ */
53
+ export declare function composeSystemPrompt(config: GnosysConfig, recalled: RecalledMemory[] | undefined): string;
45
54
  /**
46
55
  * Run a single turn against the LLM. Loops if the assistant emits
47
56
  * gnosys-tool fences: each tool is executed in-process, the result is
@@ -12,9 +12,23 @@ import { CHOOSE_SYSTEM_PROMPT_ADDENDUM } from "./choose.js";
12
12
  import { buildToolsSystemPrompt, findTool } from "./tools.js";
13
13
  import { extractToolFences } from "./toolFence.js";
14
14
  const BASE_SYSTEM_PROMPT = `You are an assistant inside the Gnosys terminal chat — a memory-aware REPL. The user has persistent memory across sessions; relevant memories are injected as <memory id="..."> blocks before their question. Cite memory IDs in square brackets like [deci-037] when you use them. Be concise and direct. Markdown renders.${CHOOSE_SYSTEM_PROMPT_ADDENDUM}`;
15
- function composeSystemPrompt(recalled) {
16
- const toolsAddendum = buildToolsSystemPrompt();
17
- const base = `${BASE_SYSTEM_PROMPT}\n${toolsAddendum}`;
15
+ /**
16
+ * Build the system prompt for a chat turn.
17
+ *
18
+ * v5.15: wires two previously-unread config knobs —
19
+ * `chat.systemPromptPrefix` (prepended before the base prompt) and
20
+ * `chat.toolsEnabled` (false omits the gnosys-tool addendum entirely).
21
+ * Exported for tests.
22
+ */
23
+ export function composeSystemPrompt(config, recalled) {
24
+ const toolsEnabled = config.chat?.toolsEnabled !== false;
25
+ let base = toolsEnabled
26
+ ? `${BASE_SYSTEM_PROMPT}\n${buildToolsSystemPrompt()}`
27
+ : BASE_SYSTEM_PROMPT;
28
+ const prefix = config.chat?.systemPromptPrefix;
29
+ if (typeof prefix === "string" && prefix.trim().length > 0) {
30
+ base = `${prefix.trim()}\n\n${base}`;
31
+ }
18
32
  if (!recalled || recalled.length === 0)
19
33
  return base;
20
34
  return `${base}\n\n${formatRecallForPrompt(recalled)}`;
@@ -47,8 +61,11 @@ export async function runTurn(config, opts) {
47
61
  // falls back to defaultProvider when no chat override is set, so existing
48
62
  // installs keep working without a chat-specific config.
49
63
  const provider = getLLMProvider(config, "chat");
50
- const system = composeSystemPrompt(opts.recalled);
51
- const maxIterations = opts.maxToolIterations ?? 4;
64
+ const system = composeSystemPrompt(config, opts.recalled);
65
+ // v5.15: when chat.toolsEnabled === false the first response is final —
66
+ // no tool-fence execution loop.
67
+ const toolsEnabled = config.chat?.toolsEnabled !== false;
68
+ const maxIterations = toolsEnabled ? (opts.maxToolIterations ?? 4) : 1;
52
69
  const toolCalls = [];
53
70
  let toolPreamble;
54
71
  let combinedText = "";
@@ -64,7 +81,7 @@ export async function runTurn(config, opts) {
64
81
  combinedText += (combinedText ? "\n\n" : "") + chunk;
65
82
  // Look for tool fences; if none, this iteration's chunk IS the answer.
66
83
  const extraction = extractToolFences(chunk);
67
- if (!extraction || extraction.calls.length === 0) {
84
+ if (!toolsEnabled || !extraction || extraction.calls.length === 0) {
68
85
  break;
69
86
  }
70
87
  // Run each tool, accumulate results into the next prompt as a system block
@@ -183,7 +183,6 @@ export declare const GnosysConfigSchema: z.ZodObject<{
183
183
  openai: "openai";
184
184
  }>>;
185
185
  defaultModel: z.ZodOptional<z.ZodString>;
186
- bulkIngestionBatchSize: z.ZodDefault<z.ZodNumber>;
187
186
  importConcurrency: z.ZodDefault<z.ZodNumber>;
188
187
  autoCommit: z.ZodDefault<z.ZodBoolean>;
189
188
  llmRetryAttempts: z.ZodDefault<z.ZodNumber>;
@@ -241,7 +240,6 @@ export declare const GnosysConfigSchema: z.ZodObject<{
241
240
  }, z.core.$strip>>;
242
241
  chat: z.ZodDefault<z.ZodObject<{
243
242
  toolsEnabled: z.ZodDefault<z.ZodBoolean>;
244
- autoSummarizeAfterTurns: z.ZodDefault<z.ZodNumber>;
245
243
  systemPromptPrefix: z.ZodDefault<z.ZodString>;
246
244
  }, z.core.$strip>>;
247
245
  multimodal: z.ZodDefault<z.ZodObject<{
@@ -137,9 +137,6 @@ const RecallConfigSchema = z.object({
137
137
  const ChatConfigSchema = z.object({
138
138
  /** Allow LLM to call gnosys tools via gnosys-tool fences (default: true). */
139
139
  toolsEnabled: z.boolean().default(true),
140
- /** Suggest /save-turn after N consecutive turns where no memory was written.
141
- * 0 disables the nudge. Heuristic only — never writes without consent. */
142
- autoSummarizeAfterTurns: z.number().int().min(0).default(0),
143
140
  /** Prepended to the chat system prompt. Use for persona / style / domain hints. */
144
141
  systemPromptPrefix: z.string().default(""),
145
142
  });
@@ -210,8 +207,6 @@ export const GnosysConfigSchema = z.object({
210
207
  .optional(),
211
208
  /** @deprecated Use llm.anthropic.model or llm.ollama.model instead */
212
209
  defaultModel: z.string().optional(),
213
- /** Max records per batch commit during bulk import */
214
- bulkIngestionBatchSize: z.number().int().min(1).max(10000).default(500),
215
210
  /** Parallel LLM calls during import */
216
211
  importConcurrency: z.number().int().min(1).max(20).default(5),
217
212
  /** Enable auto-commit on every write (disable for manual git) */
@@ -259,7 +254,6 @@ export const GnosysConfigSchema = z.object({
259
254
  /** Chat TUI — interactive chat configuration (v5.8.0) */
260
255
  chat: ChatConfigSchema.default({
261
256
  toolsEnabled: true,
262
- autoSummarizeAfterTurns: 0,
263
257
  systemPromptPrefix: "",
264
258
  }),
265
259
  /** Multimodal ingestion — PDF, audio, image, video processing */
@@ -483,6 +477,30 @@ function deepMergeConfig(base, override) {
483
477
  }
484
478
  return result;
485
479
  }
480
+ // ─── Removed-knob deprecation warnings (v5.15) ──────────────────────────
481
+ //
482
+ // These knobs were documented but never read by any code ("settings that
483
+ // lie"), so v5.15 removed them from the schema. Zod strips unknown keys on
484
+ // parse, so old config files keep loading — but warn once per process (on
485
+ // stderr only; MCP serve stdout must stay clean for JSON-RPC).
486
+ const warnedRemovedKeys = new Set();
487
+ function warnRemovedConfigKeys(raw) {
488
+ const found = [];
489
+ if ("bulkIngestionBatchSize" in raw)
490
+ found.push("bulkIngestionBatchSize");
491
+ const chat = raw.chat;
492
+ if (typeof chat === "object" &&
493
+ chat !== null &&
494
+ "autoSummarizeAfterTurns" in chat) {
495
+ found.push("chat.autoSummarizeAfterTurns");
496
+ }
497
+ for (const key of found) {
498
+ if (warnedRemovedKeys.has(key))
499
+ continue;
500
+ warnedRemovedKeys.add(key);
501
+ console.error(`gnosys: config option "${key}" was removed in v5.15 and is ignored`);
502
+ }
503
+ }
486
504
  export async function loadConfig(storePath) {
487
505
  const configPath = path.join(storePath, "gnosys.json");
488
506
  const globalPath = path.join(getGnosysHome(), "gnosys.json");
@@ -510,6 +528,7 @@ export async function loadConfig(storePath) {
510
528
  if (raw === null) {
511
529
  return DEFAULT_CONFIG;
512
530
  }
531
+ warnRemovedConfigKeys(raw);
513
532
  const migrated = migrateConfig(raw);
514
533
  const parsed = GnosysConfigSchema.parse(migrated);
515
534
  return applyDreamProviderInheritance(parsed, migrated);
@@ -579,7 +598,6 @@ export function generateConfigTemplate() {
579
598
  },
580
599
  },
581
600
  taskModels: {},
582
- bulkIngestionBatchSize: 500,
583
601
  importConcurrency: 5,
584
602
  autoCommit: true,
585
603
  llmRetryAttempts: 3,
@@ -608,7 +626,6 @@ export function generateConfigTemplate() {
608
626
  },
609
627
  chat: {
610
628
  toolsEnabled: true,
611
- autoSummarizeAfterTurns: 0,
612
629
  systemPromptPrefix: "",
613
630
  },
614
631
  }, null, 2);
@@ -1,2 +1,67 @@
1
+ export interface LaunchctlResult {
2
+ ok: boolean;
3
+ message: string;
4
+ }
5
+ /** Pure helper: argv for loading a LaunchAgent plist. Exported for tests. */
6
+ export declare function launchctlLoadArgs(plistFile: string): string[];
7
+ /** Pure helper: argv for unloading a LaunchAgent plist. Exported for tests. */
8
+ export declare function launchctlUnloadArgs(plistFile: string): string[];
9
+ /**
10
+ * Pure helper: interpret a launchctl load failure. "Already loaded" is a
11
+ * success for our purposes (the agent is active). Exported for tests.
12
+ */
13
+ export declare function interpretLaunchctlLoadError(stderr: string): LaunchctlResult;
14
+ /**
15
+ * Best-effort `launchctl load` of the dream LaunchAgent so scheduled runs
16
+ * start immediately instead of waiting for the next login. (v5.14.x sprint,
17
+ * pre-approved.) Never throws.
18
+ */
19
+ export declare function loadDreamLaunchAgent(plistFile: string): LaunchctlResult;
20
+ /**
21
+ * Best-effort `launchctl unload` before removing the plist, so disabling
22
+ * Dream Mode deactivates the schedule immediately. Never throws.
23
+ */
24
+ export declare function unloadDreamLaunchAgent(plistFile: string): LaunchctlResult;
1
25
  export declare function installDreamLaunchAgent(): string | null;
26
+ /**
27
+ * Pure helper: extract the node and cli paths from a dream LaunchAgent
28
+ * plist body. Template shape (installDreamLaunchAgent): the first
29
+ * `<string>` is the Label, the second is the node binary path, the third
30
+ * is the cli path. Exported for tests.
31
+ */
32
+ export declare function parseDreamPlistPaths(body: string): {
33
+ nodePath?: string;
34
+ cliPath?: string;
35
+ };
36
+ export interface DreamLaunchAgentHealth {
37
+ installed: boolean;
38
+ loaded: boolean;
39
+ nodeExists: boolean;
40
+ cliExists: boolean;
41
+ healthy: boolean;
42
+ problems: string[];
43
+ plistFile: string | null;
44
+ }
45
+ /**
46
+ * Health-check the dream LaunchAgent. The plist hardcodes absolute node +
47
+ * cli paths (e.g. ~/.nvm/versions/node/vX.Y.Z/bin/node), so a Node upgrade
48
+ * silently kills the scheduler — this detects that. Never throws.
49
+ */
50
+ export declare function checkDreamLaunchAgent(): DreamLaunchAgentHealth;
51
+ /**
52
+ * Repair the dream LaunchAgent: rewrite the plist with the current
53
+ * process's node + cli paths, then reload it. Best-effort — never throws.
54
+ */
55
+ export declare function repairDreamLaunchAgent(): {
56
+ ok: boolean;
57
+ message: string;
58
+ };
59
+ /**
60
+ * Post-upgrade hook: when Dream Mode is enabled and the LaunchAgent is
61
+ * installed but unhealthy (typically because a Node upgrade moved the
62
+ * hardcoded node path), repair it. Returns a printable status line, or
63
+ * null when nothing applies (non-darwin, dream disabled, agent healthy
64
+ * or not installed). Never throws.
65
+ */
66
+ export declare function repairDreamLaunchAgentAfterUpgrade(): Promise<string | null>;
2
67
  export declare function uninstallDreamLaunchAgent(): string | null;
@@ -1,7 +1,73 @@
1
+ import { execFileSync } from "child_process";
1
2
  import fs from "fs";
2
3
  import os from "os";
3
4
  import path from "path";
4
5
  const LABEL = "com.gnosys.dream";
6
+ /** Pure helper: argv for loading a LaunchAgent plist. Exported for tests. */
7
+ export function launchctlLoadArgs(plistFile) {
8
+ return ["load", "-w", plistFile];
9
+ }
10
+ /** Pure helper: argv for unloading a LaunchAgent plist. Exported for tests. */
11
+ export function launchctlUnloadArgs(plistFile) {
12
+ return ["unload", plistFile];
13
+ }
14
+ /**
15
+ * Pure helper: interpret a launchctl load failure. "Already loaded" is a
16
+ * success for our purposes (the agent is active). Exported for tests.
17
+ */
18
+ export function interpretLaunchctlLoadError(stderr) {
19
+ const text = stderr.trim();
20
+ if (/already loaded|service already loaded|Load failed: 5/i.test(text)) {
21
+ return { ok: true, message: "launchd agent already loaded" };
22
+ }
23
+ return {
24
+ ok: false,
25
+ message: `launchctl load failed${text ? `: ${text}` : ""} — the agent will load at next login`,
26
+ };
27
+ }
28
+ /**
29
+ * Best-effort `launchctl load` of the dream LaunchAgent so scheduled runs
30
+ * start immediately instead of waiting for the next login. (v5.14.x sprint,
31
+ * pre-approved.) Never throws.
32
+ */
33
+ export function loadDreamLaunchAgent(plistFile) {
34
+ if (process.platform !== "darwin")
35
+ return { ok: false, message: "launchctl unavailable (not macOS)" };
36
+ try {
37
+ execFileSync("launchctl", launchctlLoadArgs(plistFile), {
38
+ stdio: ["ignore", "pipe", "pipe"],
39
+ timeout: 10_000,
40
+ });
41
+ return { ok: true, message: "launchd agent loaded — scheduled dream runs are active now" };
42
+ }
43
+ catch (err) {
44
+ const stderr = err && typeof err === "object" && "stderr" in err && err.stderr
45
+ ? String(err.stderr)
46
+ : err instanceof Error
47
+ ? err.message
48
+ : String(err);
49
+ return interpretLaunchctlLoadError(stderr);
50
+ }
51
+ }
52
+ /**
53
+ * Best-effort `launchctl unload` before removing the plist, so disabling
54
+ * Dream Mode deactivates the schedule immediately. Never throws.
55
+ */
56
+ export function unloadDreamLaunchAgent(plistFile) {
57
+ if (process.platform !== "darwin")
58
+ return { ok: false, message: "launchctl unavailable (not macOS)" };
59
+ try {
60
+ execFileSync("launchctl", launchctlUnloadArgs(plistFile), {
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ timeout: 10_000,
63
+ });
64
+ return { ok: true, message: "launchd agent unloaded" };
65
+ }
66
+ catch {
67
+ // Not loaded (or launchctl unavailable) — nothing to deactivate.
68
+ return { ok: true, message: "launchd agent was not loaded" };
69
+ }
70
+ }
5
71
  function plistPath() {
6
72
  return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
7
73
  }
@@ -58,10 +124,151 @@ export function installDreamLaunchAgent() {
58
124
  fs.writeFileSync(file, body, "utf8");
59
125
  return file;
60
126
  }
127
+ function xmlUnescape(value) {
128
+ return value
129
+ .replace(/&quot;/g, '"')
130
+ .replace(/&gt;/g, ">")
131
+ .replace(/&lt;/g, "<")
132
+ .replace(/&amp;/g, "&");
133
+ }
134
+ /**
135
+ * Pure helper: extract the node and cli paths from a dream LaunchAgent
136
+ * plist body. Template shape (installDreamLaunchAgent): the first
137
+ * `<string>` is the Label, the second is the node binary path, the third
138
+ * is the cli path. Exported for tests.
139
+ */
140
+ export function parseDreamPlistPaths(body) {
141
+ const strings = [...body.matchAll(/<string>([^<]*)<\/string>/g)].map((m) => xmlUnescape(m[1]));
142
+ return {
143
+ nodePath: strings.length > 1 ? strings[1] : undefined,
144
+ cliPath: strings.length > 2 ? strings[2] : undefined,
145
+ };
146
+ }
147
+ /**
148
+ * Health-check the dream LaunchAgent. The plist hardcodes absolute node +
149
+ * cli paths (e.g. ~/.nvm/versions/node/vX.Y.Z/bin/node), so a Node upgrade
150
+ * silently kills the scheduler — this detects that. Never throws.
151
+ */
152
+ export function checkDreamLaunchAgent() {
153
+ if (process.platform !== "darwin") {
154
+ return {
155
+ installed: false,
156
+ loaded: false,
157
+ nodeExists: false,
158
+ cliExists: false,
159
+ healthy: false,
160
+ problems: ["launchd unavailable (not macOS)"],
161
+ plistFile: null,
162
+ };
163
+ }
164
+ const file = plistPath();
165
+ if (!fs.existsSync(file)) {
166
+ return {
167
+ installed: false,
168
+ loaded: false,
169
+ nodeExists: false,
170
+ cliExists: false,
171
+ healthy: false,
172
+ problems: ["launchd agent not installed"],
173
+ plistFile: file,
174
+ };
175
+ }
176
+ const problems = [];
177
+ let nodeExists = false;
178
+ let cliExists = false;
179
+ try {
180
+ const body = fs.readFileSync(file, "utf8");
181
+ const { nodePath, cliPath } = parseDreamPlistPaths(body);
182
+ nodeExists = nodePath !== undefined && fs.existsSync(nodePath);
183
+ if (!nodeExists) {
184
+ problems.push(`node binary missing at ${nodePath ?? "(unknown)"} — a Node upgrade likely moved it; run repair to rewrite the agent`);
185
+ }
186
+ // "gnosys" (bare command) counts as existing — resolved via PATH.
187
+ cliExists =
188
+ cliPath !== undefined && (cliPath === "gnosys" || fs.existsSync(cliPath));
189
+ if (!cliExists) {
190
+ problems.push(`gnosys cli missing at ${cliPath ?? "(unknown)"}`);
191
+ }
192
+ }
193
+ catch (err) {
194
+ problems.push(`could not read plist: ${err instanceof Error ? err.message : String(err)}`);
195
+ }
196
+ let loaded = false;
197
+ try {
198
+ execFileSync("launchctl", ["list", LABEL], {
199
+ stdio: ["ignore", "pipe", "pipe"],
200
+ timeout: 10_000,
201
+ });
202
+ loaded = true;
203
+ }
204
+ catch {
205
+ problems.push("launchd agent not loaded");
206
+ }
207
+ return {
208
+ installed: true,
209
+ loaded,
210
+ nodeExists,
211
+ cliExists,
212
+ healthy: loaded && nodeExists && cliExists,
213
+ problems,
214
+ plistFile: file,
215
+ };
216
+ }
217
+ /**
218
+ * Repair the dream LaunchAgent: rewrite the plist with the current
219
+ * process's node + cli paths, then reload it. Best-effort — never throws.
220
+ */
221
+ export function repairDreamLaunchAgent() {
222
+ const file = installDreamLaunchAgent();
223
+ if (!file) {
224
+ return { ok: false, message: "launchd unavailable (not macOS)" };
225
+ }
226
+ unloadDreamLaunchAgent(file);
227
+ const load = loadDreamLaunchAgent(file);
228
+ if (!load.ok) {
229
+ return {
230
+ ok: false,
231
+ message: `agent reinstalled but reload failed: ${load.message}`,
232
+ };
233
+ }
234
+ return { ok: true, message: "dream launchd agent repaired and reloaded" };
235
+ }
236
+ /**
237
+ * Post-upgrade hook: when Dream Mode is enabled and the LaunchAgent is
238
+ * installed but unhealthy (typically because a Node upgrade moved the
239
+ * hardcoded node path), repair it. Returns a printable status line, or
240
+ * null when nothing applies (non-darwin, dream disabled, agent healthy
241
+ * or not installed). Never throws.
242
+ */
243
+ export async function repairDreamLaunchAgentAfterUpgrade() {
244
+ if (process.platform !== "darwin")
245
+ return null;
246
+ try {
247
+ const { loadConfig } = await import("./config.js");
248
+ const { getGnosysHome } = await import("./paths.js");
249
+ const config = await loadConfig(getGnosysHome());
250
+ if (!config.dream?.enabled)
251
+ return null;
252
+ const health = checkDreamLaunchAgent();
253
+ if (!health.installed || health.healthy)
254
+ return null;
255
+ const repair = repairDreamLaunchAgent();
256
+ return repair.ok
257
+ ? "✓ dream launchd agent repaired (node path had changed)"
258
+ : `⚠ dream launchd agent unhealthy and repair failed: ${repair.message}`;
259
+ }
260
+ catch {
261
+ return null;
262
+ }
263
+ }
61
264
  export function uninstallDreamLaunchAgent() {
62
265
  if (process.platform !== "darwin")
63
266
  return null;
64
267
  const file = plistPath();
268
+ // Unload before unlinking — launchctl needs the plist on disk to resolve
269
+ // the label, and this deactivates the schedule immediately. Best-effort.
270
+ if (fs.existsSync(file))
271
+ unloadDreamLaunchAgent(file);
65
272
  try {
66
273
  fs.unlinkSync(file);
67
274
  }
@@ -92,7 +92,10 @@ export async function runHybridSearchCommand(getResolver, query, opts) {
92
92
  const writeTarget = resolver.getWriteTarget();
93
93
  if (writeTarget) {
94
94
  const { GnosysMaintenanceEngine } = await import("./maintenance.js");
95
- await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, results.map((r) => r.relativePath)).catch(() => { });
95
+ await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, results.map((r) => r.relativePath)).catch((err) => {
96
+ // Best-effort, but don't be fully silent (sprint 2026-07-02).
97
+ console.error(`gnosys: reinforcement skipped: ${err instanceof Error ? err.message : String(err)}`);
98
+ });
96
99
  }
97
100
  }
98
101
  search.close();
@@ -33,7 +33,11 @@ export async function runImportCommand(getResolver, fileOrUrl, opts) {
33
33
  const ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
34
34
  const format = opts.format;
35
35
  const mode = opts.mode;
36
- const concurrency = opts.concurrency || 5;
36
+ // v5.15: --concurrency CLI flag wins; otherwise the config's
37
+ // importConcurrency (schema default 5) applies.
38
+ const { loadConfig } = await import("./config.js");
39
+ const config = await loadConfig(writeTarget.store.getStorePath());
40
+ const concurrency = opts.concurrency || config.importConcurrency;
37
41
  // Show estimate for LLM mode
38
42
  if (mode === "llm") {
39
43
  console.error(`Mode: LLM (concurrency: ${concurrency})`);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Guard for interactive prompt flows (v5.14.x sprint, pre-approved).
3
+ *
4
+ * Interactive setup flows crash with ERR_USE_AFTER_CLOSE when stdin is
5
+ * closed (e.g. `gnosys setup dream` run non-interactively from a script
6
+ * or CI). Instead of a stack trace, print a friendly one-line error and
7
+ * exit 1.
8
+ */
9
+ /** Returns true when stdin can host an interactive prompt session. */
10
+ export declare function stdinIsInteractive(): boolean;
11
+ /** Human-readable message for a blocked interactive flow. */
12
+ export declare function nonInteractiveMessage(flow: string): string;
13
+ /**
14
+ * Exit with a friendly one-liner when stdin is not a TTY.
15
+ * Call at the entry of interactive prompt flows, before any readline use.
16
+ */
17
+ export declare function guardInteractiveStdin(flow: string): void;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Guard for interactive prompt flows (v5.14.x sprint, pre-approved).
3
+ *
4
+ * Interactive setup flows crash with ERR_USE_AFTER_CLOSE when stdin is
5
+ * closed (e.g. `gnosys setup dream` run non-interactively from a script
6
+ * or CI). Instead of a stack trace, print a friendly one-line error and
7
+ * exit 1.
8
+ */
9
+ /** Returns true when stdin can host an interactive prompt session. */
10
+ export function stdinIsInteractive() {
11
+ return Boolean(process.stdin.isTTY);
12
+ }
13
+ /** Human-readable message for a blocked interactive flow. */
14
+ export function nonInteractiveMessage(flow) {
15
+ return `gnosys ${flow} is interactive and requires a terminal (stdin is not a TTY). Re-run from an interactive shell.`;
16
+ }
17
+ /**
18
+ * Exit with a friendly one-liner when stdin is not a TTY.
19
+ * Call at the entry of interactive prompt flows, before any readline use.
20
+ */
21
+ export function guardInteractiveStdin(flow) {
22
+ if (stdinIsInteractive())
23
+ return;
24
+ console.error(nonInteractiveMessage(flow));
25
+ process.exit(1);
26
+ }
package/dist/lib/setup.js CHANGED
@@ -14,6 +14,7 @@ import fsSync from "fs";
14
14
  import path from "path";
15
15
  import os from "os";
16
16
  import { execSync } from "child_process";
17
+ import { guardInteractiveStdin } from "./interactiveGuard.js";
17
18
  import { loadConfig, updateConfig, resolveTaskModel, getProviderModel, } from "./config.js";
18
19
  import { isApiKeyValidationError, validateModel } from "./modelValidation.js";
19
20
  import { buildOpenRouterTiers, OPENROUTER_STATIC_TIERS, } from "./openrouterTiers.js";
@@ -1162,6 +1163,7 @@ export async function runSetup(opts) {
1162
1163
  };
1163
1164
  }
1164
1165
  // ─── Interactive mode ─────────────────────────────────────────────────
1166
+ guardInteractiveStdin("setup");
1165
1167
  const rl = createInterface({ input: stdin, output: stdout });
1166
1168
  let setupCompleted = false;
1167
1169
  // Handle Ctrl+C gracefully — only show "cancelled" if setup didn't finish
@@ -2130,6 +2132,8 @@ export function buildTaskModelsPatchFromAccepted(accepted, currentByTask, select
2130
2132
  export async function runModelsSetup(opts = {}) {
2131
2133
  const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
2132
2134
  const ownsRl = !opts.rl;
2135
+ if (ownsRl)
2136
+ guardInteractiveStdin("setup models");
2133
2137
  const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
2134
2138
  try {
2135
2139
  const { Header } = await import("./setup/ui/header.js");
@@ -2519,6 +2523,8 @@ async function runModelsTaskRoutingSetup(ctx) {
2519
2523
  export async function runDreamSetup(opts = {}) {
2520
2524
  const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
2521
2525
  const ownsRl = !opts.rl;
2526
+ if (ownsRl)
2527
+ guardInteractiveStdin("setup dream");
2522
2528
  const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
2523
2529
  try {
2524
2530
  // v5.9.3 Screen 7 — three grouped sub-screens (7.0 enable, 7.1
@@ -2757,8 +2763,11 @@ export async function runDreamSetup(opts = {}) {
2757
2763
  // Reset consecutive failure counter on a fresh setup so Layer 4
2758
2764
  // doesn't fire immediately based on stale history.
2759
2765
  localDb.resetDreamConsecutiveFailures();
2760
- const { installDreamLaunchAgent } = await import("./dreamLaunchd.js");
2766
+ const { installDreamLaunchAgent, loadDreamLaunchAgent } = await import("./dreamLaunchd.js");
2761
2767
  const launchdPath = installDreamLaunchAgent();
2768
+ // Best-effort immediate activation so the schedule doesn't wait for the
2769
+ // next login (v5.14.x sprint, pre-approved).
2770
+ const launchdLoad = launchdPath ? loadDreamLaunchAgent(launchdPath) : null;
2762
2771
  localDb.close();
2763
2772
  remoteDb?.close();
2764
2773
  // Final Diff block per the design — provider/machine + the two
@@ -2789,6 +2798,8 @@ export async function runDreamSetup(opts = {}) {
2789
2798
  printStatus("progress", `scheduled dream checks run nightly (${scheduleStartHour}:00-${scheduleEndHour}:00) on ${dreamerName}`);
2790
2799
  if (launchdPath)
2791
2800
  printStatus("ok", "launchd agent installed", launchdPath);
2801
+ if (launchdLoad)
2802
+ printStatus(launchdLoad.ok ? "ok" : "warn", launchdLoad.message);
2792
2803
  printStatus("progress", "check status anytime with `gnosys status --system`");
2793
2804
  }
2794
2805
  finally {
@@ -1,5 +1,6 @@
1
1
  import { createInterface } from "readline/promises";
2
2
  import { stdin, stdout } from "process";
3
+ import { guardInteractiveStdin } from "./interactiveGuard.js";
3
4
  import fs from "fs/promises";
4
5
  import fsSync from "fs";
5
6
  import os from "os";
@@ -522,6 +523,8 @@ export function renderProviderTable(providers) {
522
523
  }
523
524
  export async function runKeysSetup(opts) {
524
525
  const ownReadline = !opts?.rl;
526
+ if (ownReadline)
527
+ guardInteractiveStdin("setup keys");
525
528
  const rl = opts?.rl ?? createInterface({ input: stdin, output: stdout });
526
529
  try {
527
530
  while (true) {
@@ -86,6 +86,22 @@ export async function runStatusCommand(opts, deps) {
86
86
  }
87
87
  const data = await collectDashboardData(resolver, cfg, deps.pkgVersion, dashDb ?? undefined);
88
88
  console.log(opts.json ? formatDashboardJSON(data) : formatDashboard(data));
89
+ // v5.15: dream launchd agent health (read-only — repair happens in
90
+ // `gnosys upgrade`). macOS + dream enabled only; skip in JSON mode
91
+ // to keep the JSON payload shape stable.
92
+ if (!opts.json && process.platform === "darwin" && cfg.dream?.enabled) {
93
+ const { checkDreamLaunchAgent } = await import("./dreamLaunchd.js");
94
+ const health = checkDreamLaunchAgent();
95
+ console.log("\nDream launchd agent:");
96
+ if (health.healthy) {
97
+ console.log(" ✓ healthy (installed, loaded, node + cli paths valid)");
98
+ }
99
+ else {
100
+ for (const problem of health.problems) {
101
+ console.log(` ⚠ ${problem}`);
102
+ }
103
+ }
104
+ }
89
105
  }
90
106
  catch (err) {
91
107
  console.error(`Error: ${err instanceof Error ? err.message : err}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnosys",
3
- "version": "5.14.0",
3
+ "version": "5.15.1",
4
4
  "description": "Gnosys — Persistent Memory for AI Agents. Sandbox-first runtime, central SQLite brain, federated search, Dream Mode, Web Knowledge Base, Obsidian export.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",