@noir-ai/cli 1.11.2 → 1.12.0-beta.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.
Files changed (45) hide show
  1. package/dist/bin.js +3295 -575
  2. package/dist/bin.js.map +1 -1
  3. package/dist/index.js +6136 -6
  4. package/dist/index.js.map +1 -1
  5. package/dist/{chunk-QIVHIKYD.js → tui/chunk-2RK6AFZQ.js} +37 -16
  6. package/dist/tui/chunk-2RK6AFZQ.js.map +1 -0
  7. package/dist/tui/{chunk-R5YX44UL.js → chunk-ESZTOQEC.js} +211 -2
  8. package/dist/tui/chunk-ESZTOQEC.js.map +1 -0
  9. package/dist/tui/chunk-J77RHAWG.js +5010 -0
  10. package/dist/tui/chunk-J77RHAWG.js.map +1 -0
  11. package/dist/{create-VK2QMXYS.js → tui/create-BTYLBVV6.js} +4 -4
  12. package/dist/tui/{create-WKFVM237.js.map → create-BTYLBVV6.js.map} +1 -1
  13. package/dist/tui/index.js +45 -3454
  14. package/dist/tui/index.js.map +1 -1
  15. package/dist/tui/{install-JR6ZXNYA.js → install-YGTUSLRO.js} +2 -3
  16. package/dist/{sync-VXZKODQA.js → tui/sync-LRHPJ5VV.js} +4 -4
  17. package/dist/tui/{sync-DUWJAF7Z.js.map → sync-LRHPJ5VV.js.map} +1 -1
  18. package/dist/tui/{update-PGAM6OVL.js → update-LET5BWOU.js} +3 -4
  19. package/package.json +12 -12
  20. package/dist/chunk-A5W2E3X2.js +0 -33
  21. package/dist/chunk-A5W2E3X2.js.map +0 -1
  22. package/dist/chunk-CT6TYLHN.js +0 -1105
  23. package/dist/chunk-CT6TYLHN.js.map +0 -1
  24. package/dist/chunk-I2R55ACD.js +0 -369
  25. package/dist/chunk-I2R55ACD.js.map +0 -1
  26. package/dist/chunk-QIVHIKYD.js.map +0 -1
  27. package/dist/chunk-RON2XESF.js +0 -210
  28. package/dist/chunk-RON2XESF.js.map +0 -1
  29. package/dist/create-VK2QMXYS.js.map +0 -1
  30. package/dist/install-4U4UDDNE.js +0 -13
  31. package/dist/sync-VXZKODQA.js.map +0 -1
  32. package/dist/tui/chunk-F6XRS72N.js +0 -1106
  33. package/dist/tui/chunk-F6XRS72N.js.map +0 -1
  34. package/dist/tui/chunk-NLK6UNIA.js +0 -223
  35. package/dist/tui/chunk-NLK6UNIA.js.map +0 -1
  36. package/dist/tui/chunk-QF6777NJ.js +0 -209
  37. package/dist/tui/chunk-QF6777NJ.js.map +0 -1
  38. package/dist/tui/chunk-R5YX44UL.js.map +0 -1
  39. package/dist/tui/create-WKFVM237.js +0 -79
  40. package/dist/tui/sync-DUWJAF7Z.js +0 -90
  41. package/dist/tui/update-PGAM6OVL.js.map +0 -1
  42. package/dist/update-ZDAVLOXB.js +0 -20
  43. package/dist/update-ZDAVLOXB.js.map +0 -1
  44. /package/dist/{install-4U4UDDNE.js.map → tui/install-YGTUSLRO.js.map} +0 -0
  45. /package/dist/tui/{install-JR6ZXNYA.js.map → update-LET5BWOU.js.map} +0 -0
package/dist/bin.js CHANGED
@@ -1,46 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- serve
4
- } from "./chunk-A5W2E3X2.js";
5
- import {
6
- DEFAULT_UPDATE_CONFIG,
7
- buildUpdateNotice,
8
- runAsyncUpdateCheck
9
- } from "./chunk-QIVHIKYD.js";
10
- import "./chunk-RON2XESF.js";
11
- import {
12
- doctor,
13
- init
14
- } from "./chunk-CT6TYLHN.js";
15
- import {
16
- EXIT,
17
- NoirCliError,
18
- accessibleMode,
19
- badge,
20
- c,
21
- definitionList,
22
- fail,
23
- handleError,
24
- inferExitCode,
25
- info,
26
- isInteractive,
27
- json,
28
- log,
29
- requireInteractive,
30
- spinner,
31
- success,
32
- table,
33
- tip,
34
- useColor,
35
- warn
36
- } from "./chunk-I2R55ACD.js";
37
-
38
- // src/bin.ts
39
- import { realpathSync } from "fs";
40
- import { pathToFileURL } from "url";
41
- import { SUPPORTED_HOSTS as SUPPORTED_HOSTS2 } from "@noir-ai/adapters";
42
- import { NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
43
- import { Command, Option } from "commander";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
44
16
 
45
17
  // src/daemon-client.ts
46
18
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
@@ -55,8 +27,6 @@ import {
55
27
  import { createMemoryEngine } from "@noir-ai/memory";
56
28
  import { openStore } from "@noir-ai/store";
57
29
  import { WorkflowEngine } from "@noir-ai/workflow";
58
- var DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
59
- var PROBE_TIMEOUT_MS = 1500;
60
30
  function describeCause(cause) {
61
31
  if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
62
32
  try {
@@ -93,8 +63,21 @@ async function probeDaemon(opts = {}) {
93
63
  return { running: false };
94
64
  }
95
65
  const body = await res.json().catch(() => null);
96
- if (body?.ok !== true) {
97
- if (opts.verbose) process.stderr.write("noir: daemon probe: /health body not ok\n");
66
+ const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
67
+ const expectedProject = opts.project?.id ?? (() => {
68
+ try {
69
+ return loadProjectInfo(process.cwd()).id;
70
+ } catch {
71
+ return void 0;
72
+ }
73
+ })();
74
+ const projectOk = expectedProject === void 0 || body?.projectId !== void 0 && body.projectId === expectedProject;
75
+ if (body?.ok !== true || !pidOk || !projectOk) {
76
+ if (opts.verbose)
77
+ process.stderr.write(
78
+ !pidOk ? "noir: daemon probe: pid missing/mismatched (foreign process on recorded port)\n" : !projectOk ? `noir: daemon probe: daemon serves a different project (${body?.projectId ?? "unknown"}); using in-process fallback
79
+ ` : "noir: daemon probe: /health body not ok\n"
80
+ );
98
81
  return { running: false };
99
82
  }
100
83
  return {
@@ -111,7 +94,12 @@ async function probeDaemon(opts = {}) {
111
94
  }
112
95
  }
113
96
  async function resolveDaemon(opts) {
114
- const project = opts.project ?? loadProjectInfo(process.cwd());
97
+ let project;
98
+ try {
99
+ project = opts.project ?? loadProjectInfo(process.cwd());
100
+ } catch {
101
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
102
+ }
115
103
  const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
116
104
  try {
117
105
  const ensured = await ensureDaemonRunning({ project, idleTimeoutSec });
@@ -202,8 +190,22 @@ async function callDaemonTool(opts, name, args = {}) {
202
190
  return withDaemon(opts, (caller) => caller.callTool(name, args));
203
191
  }
204
192
  async function withInProcessRead(opts, fn) {
205
- const project = opts.project ?? loadProjectInfo(process.cwd());
206
- const store = await openStore({ projectId: project.id, root: project.root, readonly: true });
193
+ let project;
194
+ try {
195
+ project = opts.project ?? loadProjectInfo(process.cwd());
196
+ } catch {
197
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
198
+ }
199
+ let store;
200
+ try {
201
+ store = await openStore({ projectId: project.id, root: project.root, readonly: true });
202
+ } catch (err) {
203
+ fail(
204
+ EXIT.ERROR,
205
+ `could not open the store read-only: ${err instanceof Error ? err.message : String(err)}`,
206
+ opts
207
+ );
208
+ }
207
209
  try {
208
210
  const embedderCfg = resolveEmbedderConfig(project.config.context);
209
211
  const embed = createEmbedFn(embedderCfg).embed;
@@ -233,6 +235,368 @@ async function withInProcessRead(opts, fn) {
233
235
  });
234
236
  }
235
237
  }
238
+ var DAEMON_DOWN_HINT, PROBE_TIMEOUT_MS;
239
+ var init_daemon_client = __esm({
240
+ "src/daemon-client.ts"() {
241
+ "use strict";
242
+ init_bin();
243
+ DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
244
+ PROBE_TIMEOUT_MS = 1500;
245
+ }
246
+ });
247
+
248
+ // src/theme.ts
249
+ import pc from "picocolors";
250
+ function badge(state, label = state) {
251
+ const text = `${BADGE_SYMBOL[state]} ${label}`;
252
+ switch (state) {
253
+ case "ok":
254
+ return c.ok(text);
255
+ case "warn":
256
+ return c.warn(text);
257
+ case "error":
258
+ return c.error(text);
259
+ case "info":
260
+ return c.info(text);
261
+ }
262
+ }
263
+ function envFlagSet(name) {
264
+ const v = process.env[name];
265
+ return v !== void 0 && v !== "";
266
+ }
267
+ function isCiEnv() {
268
+ const v = process.env.CI;
269
+ if (v === void 0 || v === "") return false;
270
+ return v !== "0" && v !== "false";
271
+ }
272
+ function useColor() {
273
+ if (envFlagSet("NO_COLOR")) return false;
274
+ if (process.env.CLICOLOR_FORCE === "1") return true;
275
+ if (isCiEnv()) return false;
276
+ return pc.isColorSupported;
277
+ }
278
+ function accessibleMode() {
279
+ return envFlagSet("NOIR_ACCESSIBLE");
280
+ }
281
+ function terminalWidth() {
282
+ const fromEnv = process.env.COLUMNS;
283
+ if (fromEnv !== void 0 && fromEnv !== "") {
284
+ const n = Number(fromEnv);
285
+ if (Number.isInteger(n) && n > 0) return Math.max(n, 20);
286
+ }
287
+ const cols = process.stdout.columns;
288
+ if (typeof cols === "number" && cols > 0) return Math.max(cols, 20);
289
+ return 80;
290
+ }
291
+ var FORCE, c, BADGE_SYMBOL;
292
+ var init_theme = __esm({
293
+ "src/theme.ts"() {
294
+ "use strict";
295
+ FORCE = pc.createColors(true);
296
+ c = {
297
+ /** Green — success / healthy. */
298
+ ok: (s) => useColor() ? FORCE.green(s) : s,
299
+ /** Yellow — degraded but usable. */
300
+ warn: (s) => useColor() ? FORCE.yellow(s) : s,
301
+ /** Red — reserved strictly for ERROR. Never a table header. */
302
+ error: (s) => useColor() ? FORCE.red(s) : s,
303
+ /** Cyan — informational / brand-neutral accent for labels. */
304
+ info: (s) => useColor() ? FORCE.cyan(s) : s,
305
+ /** Blue — the Midnight Cobalt brand role (banner / accent). */
306
+ accent: (s) => useColor() ? FORCE.blue(s) : s,
307
+ /** Dim — secondary text, table borders when color is on. */
308
+ dim: (s) => useColor() ? FORCE.dim(s) : s,
309
+ /** Bold — emphasis (table headers compose this with `info`). */
310
+ bold: (s) => useColor() ? FORCE.bold(s) : s,
311
+ /** Reverse video (SGR 7) — inverted background for the active list row. */
312
+ inverse: (s) => useColor() ? FORCE.inverse(s) : s
313
+ };
314
+ BADGE_SYMBOL = {
315
+ ok: "\u2713",
316
+ warn: "\u26A0",
317
+ error: "\u2717",
318
+ info: "\u2139"
319
+ };
320
+ }
321
+ });
322
+
323
+ // src/output.ts
324
+ import Table from "cli-table3";
325
+ import { CommanderError } from "commander";
326
+ import ora from "ora";
327
+ function isJsonMode(opts) {
328
+ return opts.json === true;
329
+ }
330
+ function isQuietMode(opts) {
331
+ return opts.quiet === true;
332
+ }
333
+ function isNoInput(opts = {}) {
334
+ return opts.noInput === true || opts.input === false;
335
+ }
336
+ function envFlagSet2(name) {
337
+ const v = process.env[name];
338
+ return v !== void 0 && v !== "";
339
+ }
340
+ function isInteractive(opts = {}) {
341
+ if (isJsonMode(opts) || isNoInput(opts)) return false;
342
+ if (isCiEnv() || envFlagSet2("NO_COLOR")) return false;
343
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
344
+ }
345
+ function resolveInteractive() {
346
+ if (flaggedNonInteractiveCli()) return false;
347
+ return isInteractive();
348
+ }
349
+ function flaggedNonInteractiveCli() {
350
+ const v = process.env.NOIR_NON_INTERACTIVE;
351
+ return v !== void 0 && v !== "";
352
+ }
353
+ function json(obj) {
354
+ process.stdout.write(`${JSON.stringify(obj)}
355
+ `);
356
+ }
357
+ function log(msg2, opts = {}) {
358
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
359
+ process.stderr.write(`${msg2}
360
+ `);
361
+ }
362
+ function info(msg2, opts = {}) {
363
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
364
+ process.stderr.write(`${c.info(msg2)}
365
+ `);
366
+ }
367
+ function success(msg2, opts = {}) {
368
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
369
+ process.stderr.write(`${c.ok(msg2)}
370
+ `);
371
+ }
372
+ function warn(msg2, opts = {}) {
373
+ if (isJsonMode(opts)) return;
374
+ process.stderr.write(`${c.warn(msg2)}
375
+ `);
376
+ }
377
+ function error(msg2, opts = {}) {
378
+ if (isJsonMode(opts)) return;
379
+ process.stderr.write(`${c.error(msg2)}
380
+ `);
381
+ }
382
+ function tip(msg2, opts = {}) {
383
+ if (isJsonMode(opts) || opts.noTips === true) return;
384
+ process.stderr.write(`${c.warn(msg2)}
385
+ `);
386
+ }
387
+ function table(rows, cols, opts = {}) {
388
+ if (isJsonMode(opts)) return;
389
+ if (rows.length === 0) {
390
+ info("(no rows)", opts);
391
+ return;
392
+ }
393
+ const t = new Table({
394
+ head: cols.map((col) => c.bold(c.info(col))),
395
+ // Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the
396
+ // default red header nor a colored border). All color is ours, via theme.
397
+ style: { head: [], border: [] },
398
+ colWidths: computeColWidths(rows, cols),
399
+ wordWrap: true,
400
+ truncate: "\u2026"
401
+ });
402
+ for (const row of rows) {
403
+ t.push(cols.map((col) => formatCell(row[col])));
404
+ }
405
+ process.stderr.write(`${t.toString()}
406
+ `);
407
+ }
408
+ function computeColWidths(rows, cols) {
409
+ const n = cols.length;
410
+ if (n === 0) return [];
411
+ const headerLen = cols.map((col) => col.length);
412
+ const natural = cols.map((col, i) => {
413
+ let max = Math.max(headerLen[i] ?? col.length, 3);
414
+ for (const row of rows) {
415
+ const len = formatCell(row[col]).length;
416
+ if (len > max) max = len;
417
+ }
418
+ return max;
419
+ });
420
+ const contentBudget = Math.max(n * 4, terminalWidth() - 3 * n - 1);
421
+ const naturalSum = natural.reduce((a, b) => a + b, 0);
422
+ if (naturalSum <= contentBudget) {
423
+ return natural.map((w) => w + 2);
424
+ }
425
+ const content = natural.slice();
426
+ let sum = naturalSum;
427
+ const floor = headerLen.map((h) => Math.max(h, 3));
428
+ while (sum > contentBudget) {
429
+ let idx = -1;
430
+ let widest = -1;
431
+ for (let i = 0; i < n; i++) {
432
+ const c2 = content[i] ?? 0;
433
+ const f = floor[i] ?? 3;
434
+ if (c2 > f && c2 > widest) {
435
+ widest = c2;
436
+ idx = i;
437
+ }
438
+ }
439
+ if (idx === -1) break;
440
+ const overshoot = sum - contentBudget;
441
+ const reducible = (content[idx] ?? 0) - (floor[idx] ?? 3);
442
+ const cut = Math.min(overshoot, reducible);
443
+ content[idx] = (content[idx] ?? 0) - cut;
444
+ sum -= cut;
445
+ }
446
+ return content.map((w) => w + 2);
447
+ }
448
+ function formatCell(value) {
449
+ if (value === void 0 || value === null) return "";
450
+ if (typeof value === "string") return value;
451
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
452
+ return String(value);
453
+ }
454
+ return JSON.stringify(value);
455
+ }
456
+ function definitionList(rows, opts = {}) {
457
+ table(
458
+ rows.map((r) => ({ Field: r.label, Value: formatCell(r.value) })),
459
+ ["Field", "Value"],
460
+ opts
461
+ );
462
+ }
463
+ function spinner(text = "", opts = {}) {
464
+ if (isJsonMode(opts) || isQuietMode(opts) || !isInteractive(opts)) {
465
+ return new NoopSpinner();
466
+ }
467
+ return new OraSpinner(text);
468
+ }
469
+ function fail(exitCode, message, opts = {}) {
470
+ if (isJsonMode(opts)) {
471
+ process.stdout.write(`${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
472
+ `);
473
+ } else if (message.length > 0) {
474
+ process.stderr.write(`${message}
475
+ `);
476
+ }
477
+ throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);
478
+ }
479
+ function requireInteractive(opts, hint) {
480
+ if (!isInteractive(opts)) {
481
+ fail(
482
+ EXIT.USAGE,
483
+ `${hint} needs an interactive terminal, but input is disabled (non-TTY, --no-input, --json, CI, or NO_COLOR). Re-run in a TTY or supply the value as a flag.`,
484
+ opts
485
+ );
486
+ }
487
+ }
488
+ function commanderExitCode(err) {
489
+ switch (err.code) {
490
+ case "commander.helpDisplayed":
491
+ case "commander.versionDisplayed":
492
+ case "commander.version":
493
+ return EXIT.OK;
494
+ case "commander.unknownCommand":
495
+ return EXIT.NOT_FOUND;
496
+ default:
497
+ return EXIT.USAGE;
498
+ }
499
+ }
500
+ function inferExitCode(err) {
501
+ if (err instanceof NoirCliError) return err.exitCode;
502
+ if (err instanceof CommanderError) {
503
+ if (err.code.startsWith("noir.")) return err.exitCode;
504
+ return commanderExitCode(err);
505
+ }
506
+ return EXIT.ERROR;
507
+ }
508
+ function handleError(err) {
509
+ if (err instanceof NoirCliError) {
510
+ if (err.message.length > 0) process.stderr.write(`${err.message}
511
+ `);
512
+ } else if (err instanceof CommanderError) {
513
+ } else {
514
+ process.stderr.write(`noir: ${err instanceof Error ? err.message : String(err)}
515
+ `);
516
+ }
517
+ process.exitCode = inferExitCode(err);
518
+ }
519
+ var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner;
520
+ var init_output = __esm({
521
+ "src/output.ts"() {
522
+ "use strict";
523
+ init_theme();
524
+ EXIT = {
525
+ OK: 0,
526
+ ERROR: 1,
527
+ USAGE: 2,
528
+ NOT_FOUND: 3,
529
+ DAEMON_DOWN: 4,
530
+ CANCELLED: 5
531
+ };
532
+ NoirCliError = class extends Error {
533
+ exitCode;
534
+ constructor(exitCode, message) {
535
+ super(message);
536
+ this.name = "NoirCliError";
537
+ this.exitCode = exitCode;
538
+ }
539
+ };
540
+ NOIR_ERROR_CODE = "noir.error";
541
+ NoopSpinner = class {
542
+ text = "";
543
+ start(_text) {
544
+ return this;
545
+ }
546
+ stop() {
547
+ return this;
548
+ }
549
+ clear() {
550
+ return this;
551
+ }
552
+ succeed(_text) {
553
+ }
554
+ fail(_text) {
555
+ }
556
+ warn(_text) {
557
+ }
558
+ info(_text) {
559
+ }
560
+ };
561
+ OraSpinner = class {
562
+ handle;
563
+ constructor(initialText) {
564
+ this.handle = ora(initialText);
565
+ }
566
+ get text() {
567
+ return this.handle.text;
568
+ }
569
+ set text(value) {
570
+ this.handle.text = value;
571
+ }
572
+ start(text) {
573
+ if (text !== void 0) this.handle.text = text;
574
+ this.handle.start();
575
+ return this;
576
+ }
577
+ stop() {
578
+ this.handle.stop();
579
+ return this;
580
+ }
581
+ clear() {
582
+ this.handle.clear();
583
+ return this;
584
+ }
585
+ succeed(text) {
586
+ this.handle.succeed(text);
587
+ }
588
+ fail(text) {
589
+ this.handle.fail(text);
590
+ }
591
+ warn(text) {
592
+ this.handle.warn(text);
593
+ }
594
+ info(text) {
595
+ this.handle.info(text);
596
+ }
597
+ };
598
+ }
599
+ });
236
600
 
237
601
  // src/commands/context.ts
238
602
  function parseLimit(raw, label, opts) {
@@ -349,8 +713,8 @@ async function contextIndex(opts) {
349
713
  }
350
714
  renderIndex(data, opts.paths, opts);
351
715
  }
352
- function renderIndex(data, paths, opts) {
353
- const scope = paths && paths.length > 0 ? paths.join(", ") : ".";
716
+ function renderIndex(data, paths3, opts) {
717
+ const scope = paths3 && paths3.length > 0 ? paths3.join(", ") : ".";
354
718
  const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
355
719
  log(`context index \u2014 ${scope}${flag}`, opts);
356
720
  definitionList(
@@ -398,6 +762,14 @@ function renderStatus(data, opts) {
398
762
  opts
399
763
  );
400
764
  }
765
+ var init_context = __esm({
766
+ "src/commands/context.ts"() {
767
+ "use strict";
768
+ init_daemon_client();
769
+ init_output();
770
+ init_theme();
771
+ }
772
+ });
401
773
 
402
774
  // src/commands/daemon.ts
403
775
  import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
@@ -408,23 +780,41 @@ import {
408
780
  readDaemonRecord as readDaemonRecord2,
409
781
  spawnDetachedDaemon
410
782
  } from "@noir-ai/daemon";
411
- var MODE = "foreground";
412
783
  function formatUptime(sec) {
413
784
  if (!Number.isFinite(sec) || sec < 0) return "unknown";
414
785
  if (sec < 60) return `${sec}s`;
415
786
  if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
416
787
  return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
417
788
  }
418
- async function isHealthy(port) {
789
+ function resolveCallerProjectId() {
419
790
  try {
420
- const res = await fetch(`http://127.0.0.1:${port}/health`);
421
- return res.status === 200;
791
+ return loadProjectInfo2(process.cwd()).id;
792
+ } catch {
793
+ return void 0;
794
+ }
795
+ }
796
+ async function isHealthy(port, expectedPid, expectedProjectId) {
797
+ try {
798
+ const res = await fetch(`http://127.0.0.1:${port}/health`, {
799
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
800
+ });
801
+ if (res.status !== 200) return false;
802
+ if (expectedPid === void 0) return true;
803
+ const body = await res.json();
804
+ if (body?.ok !== true || body.pid !== expectedPid) return false;
805
+ if (expectedProjectId !== void 0 && body.projectId !== expectedProjectId) return false;
806
+ return true;
422
807
  } catch {
423
808
  return false;
424
809
  }
425
810
  }
426
811
  async function daemonStart(opts) {
427
- const project = loadProjectInfo2(process.cwd());
812
+ let project;
813
+ try {
814
+ project = loadProjectInfo2(process.cwd());
815
+ } catch {
816
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
817
+ }
428
818
  if (opts.detachChild === true) {
429
819
  const child = await ensureDaemonRunning2({
430
820
  project,
@@ -452,7 +842,7 @@ async function daemonStart(opts) {
452
842
  }
453
843
  if (opts.detach === true) {
454
844
  const existing = readDaemonRecord2();
455
- if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port)) {
845
+ if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
456
846
  if (opts.json === true) {
457
847
  process.stdout.write(
458
848
  `${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
@@ -466,7 +856,16 @@ async function daemonStart(opts) {
466
856
  );
467
857
  return;
468
858
  }
469
- const spawned = await spawnDetachedDaemon({ project });
859
+ let spawned;
860
+ try {
861
+ spawned = await spawnDetachedDaemon({ project });
862
+ } catch (err) {
863
+ fail(
864
+ EXIT.ERROR,
865
+ `noir daemon start --detach failed: ${err instanceof Error ? err.message : String(err)}`,
866
+ opts
867
+ );
868
+ }
470
869
  if (opts.json === true) {
471
870
  process.stdout.write(
472
871
  `${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
@@ -478,10 +877,20 @@ async function daemonStart(opts) {
478
877
  return;
479
878
  }
480
879
  const ds = spinner("Starting Noir daemon...", opts).start();
481
- const ensured = await ensureDaemonRunning2({
482
- project,
483
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
484
- });
880
+ let ensured;
881
+ try {
882
+ ensured = await ensureDaemonRunning2({
883
+ project,
884
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec
885
+ });
886
+ } catch (err) {
887
+ ds.fail("Daemon failed to start");
888
+ fail(
889
+ EXIT.ERROR,
890
+ `noir daemon start failed: ${err instanceof Error ? err.message : String(err)}`,
891
+ opts
892
+ );
893
+ }
485
894
  if (ensured.started) {
486
895
  ds.succeed("Daemon started");
487
896
  if (opts.json === true) {
@@ -529,15 +938,19 @@ async function daemonStop(opts) {
529
938
  }
530
939
  const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
531
940
  let signalled = false;
532
- let errMsg;
533
- try {
534
- process.kill(rec.pid, "SIGTERM");
535
- signalled = true;
536
- } catch (err) {
537
- errMsg = err instanceof Error ? err.message : String(err);
538
- } finally {
539
- clearDaemonRecord();
941
+ let errMsg2;
942
+ const callerProject = resolveCallerProjectId();
943
+ const wrongProject = rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject;
944
+ const healthy = !wrongProject && pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
945
+ if (healthy) {
946
+ try {
947
+ process.kill(rec.pid, "SIGTERM");
948
+ signalled = true;
949
+ } catch (err) {
950
+ errMsg2 = err instanceof Error ? err.message : String(err);
951
+ }
540
952
  }
953
+ if (!wrongProject) clearDaemonRecord();
541
954
  if (opts.json === true) {
542
955
  process.stdout.write(
543
956
  `${JSON.stringify({
@@ -546,7 +959,7 @@ async function daemonStop(opts) {
546
959
  running: signalled,
547
960
  stopped: signalled,
548
961
  pid: rec.pid,
549
- ...errMsg !== void 0 ? { error: errMsg } : {}
962
+ ...errMsg2 !== void 0 ? { error: errMsg2 } : {}
550
963
  }
551
964
  })}
552
965
  `
@@ -556,8 +969,9 @@ async function daemonStop(opts) {
556
969
  if (signalled) {
557
970
  ds.succeed(`Stopped Noir daemon (pid ${rec.pid})`);
558
971
  } else {
559
- ds.warn(`Daemon (pid ${rec.pid}) could not be signalled: ${errMsg}`);
560
- info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${errMsg}`, opts);
972
+ const reason = wrongProject ? "the daemon belongs to another project (left running)" : errMsg2 !== void 0 ? errMsg2 : "the recorded pid did not answer as our daemon (stale record cleared)";
973
+ ds.warn(`Daemon (pid ${rec.pid}) could not be signalled: ${reason}`);
974
+ info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${reason}`, opts);
561
975
  }
562
976
  }
563
977
  async function daemonStatus(opts) {
@@ -565,6 +979,14 @@ async function daemonStatus(opts) {
565
979
  if (!rec) {
566
980
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
567
981
  }
982
+ const callerProject = resolveCallerProjectId();
983
+ if (rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject) {
984
+ fail(
985
+ EXIT.DAEMON_DOWN,
986
+ "Noir daemon is not running for this project (a daemon for another project is running).",
987
+ opts
988
+ );
989
+ }
568
990
  if (!pidAlive2(rec.pid)) {
569
991
  clearDaemonRecord();
570
992
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
@@ -572,19 +994,22 @@ async function daemonStatus(opts) {
572
994
  const hs = spinner("Probing daemon health...", opts).start();
573
995
  let health = null;
574
996
  try {
575
- const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
997
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
998
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
999
+ });
576
1000
  if (res.status === 200) {
577
1001
  health = await res.json();
578
1002
  }
579
1003
  } catch {
580
1004
  health = null;
581
1005
  }
582
- if (health?.ok !== true) {
1006
+ const pidOk = typeof health?.pid === "number" && health.pid === rec.pid;
1007
+ if (health?.ok !== true || !pidOk) {
583
1008
  hs.fail("Daemon not responding");
584
1009
  clearDaemonRecord();
585
1010
  fail(
586
1011
  EXIT.DAEMON_DOWN,
587
- "Noir daemon is not running (port unresponsive; stale record removed).",
1012
+ health !== null && !pidOk ? "Noir daemon is not running (pid missing/mismatched \u2014 a foreign process holds the recorded port; stale record removed)." : "Noir daemon is not running (port unresponsive; stale record removed).",
588
1013
  opts
589
1014
  );
590
1015
  }
@@ -614,19 +1039,1140 @@ async function daemonRestart(opts) {
614
1039
  await daemonStop({ ...opts, json: false, quiet: true });
615
1040
  await daemonStart(opts);
616
1041
  }
617
-
618
- // src/commands/handoff.ts
619
- import { mkdirSync, writeFileSync } from "fs";
620
- import { join } from "path";
621
- import {
622
- defaultHandoffBlock,
623
- hostLaunchDirective,
624
- resolveAdapter
625
- } from "@noir-ai/adapters";
626
- import { loadProjectInfo as loadProjectInfo5, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
1042
+ var MODE;
1043
+ var init_daemon = __esm({
1044
+ "src/commands/daemon.ts"() {
1045
+ "use strict";
1046
+ init_daemon_client();
1047
+ init_output();
1048
+ MODE = "foreground";
1049
+ }
1050
+ });
1051
+
1052
+ // src/commands/doctor.ts
1053
+ import { spawnSync } from "child_process";
1054
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
1055
+ import { dirname, join, relative } from "path";
1056
+ import { fileURLToPath } from "url";
1057
+ import { resolveAdapter } from "@noir-ai/adapters";
1058
+ import {
1059
+ detectActiveMethod,
1060
+ latestVersionFromCache,
1061
+ loadProjectInfo as loadProjectInfo3,
1062
+ NOIR_VERSION as NOIR_VERSION2,
1063
+ paths,
1064
+ readInstallRecord,
1065
+ readUpdateCache
1066
+ } from "@noir-ai/core";
1067
+ import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
1068
+ import { pidAlive as pidAlive3, readDaemonRecord as readDaemonRecord3 } from "@noir-ai/daemon";
1069
+ import { resolveModelConfig } from "@noir-ai/model";
1070
+ async function probeOnnx() {
1071
+ try {
1072
+ const spec = "onnxruntime-node";
1073
+ await import(spec);
1074
+ return { ok: true, reason: "loadable" };
1075
+ } catch (e) {
1076
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
1077
+ }
1078
+ }
1079
+ function describeOnnx(o) {
1080
+ if (o.ok) return "loadable";
1081
+ if (/Cannot find (module|package)/i.test(o.reason)) {
1082
+ return "not resolvable from CLI probe (best-effort)";
1083
+ }
1084
+ const r = o.reason.length > 60 ? `${o.reason.slice(0, 59)}\u2026` : o.reason;
1085
+ return `probe failed: ${r}`;
1086
+ }
1087
+ function fmtUptime(sec) {
1088
+ if (!Number.isFinite(sec) || sec < 0) return "unknown";
1089
+ if (sec < 60) return `${sec}s`;
1090
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
1091
+ return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
1092
+ }
1093
+ function msg(e) {
1094
+ return e instanceof Error ? e.message : String(e);
1095
+ }
1096
+ async function checkRuntime(checks) {
1097
+ checks.push({
1098
+ name: "runtime",
1099
+ status: "ok",
1100
+ detail: `noir ${NOIR_VERSION2} \xB7 node ${process.version} \xB7 ${process.platform}`
1101
+ });
1102
+ }
1103
+ function checkConfig(root) {
1104
+ const configPath = paths.config(root);
1105
+ if (!existsSync(configPath)) {
1106
+ return {
1107
+ project: void 0,
1108
+ result: {
1109
+ name: "config",
1110
+ status: "warn",
1111
+ detail: `no .noir/config.yml in ${root} \u2014 run \`noir init\``
1112
+ }
1113
+ };
1114
+ }
1115
+ try {
1116
+ const project = loadProjectInfo3(root);
1117
+ return {
1118
+ project,
1119
+ result: {
1120
+ name: "config",
1121
+ status: "ok",
1122
+ detail: `host=${project.config.host} \xB7 mode=${project.config.mode}`
1123
+ }
1124
+ };
1125
+ } catch (e) {
1126
+ return {
1127
+ project: void 0,
1128
+ result: { name: "config", status: "fail", detail: `config parse error: ${msg(e)}` }
1129
+ };
1130
+ }
1131
+ }
1132
+ async function checkDaemon(checks) {
1133
+ const rec = readDaemonRecord3();
1134
+ if (!rec) {
1135
+ checks.push({
1136
+ name: "daemon",
1137
+ status: "warn",
1138
+ detail: "not running (start with `noir daemon start`)"
1139
+ });
1140
+ return;
1141
+ }
1142
+ if (!pidAlive3(rec.pid)) {
1143
+ checks.push({
1144
+ name: "daemon",
1145
+ status: "warn",
1146
+ detail: `stale record (pid ${rec.pid} not alive)`
1147
+ });
1148
+ return;
1149
+ }
1150
+ try {
1151
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
1152
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
1153
+ });
1154
+ const body = res.status === 200 ? await res.json() : null;
1155
+ const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
1156
+ if (body && body.ok === true && pidOk) {
1157
+ checks.push({
1158
+ name: "daemon",
1159
+ status: "ok",
1160
+ detail: `running (pid ${rec.pid}, port ${rec.port}, up ${fmtUptime(body.uptimeSec ?? 0)})`
1161
+ });
1162
+ return;
1163
+ }
1164
+ checks.push({
1165
+ name: "daemon",
1166
+ status: "warn",
1167
+ detail: `record present but /health unreachable on port ${rec.port}`
1168
+ });
1169
+ } catch {
1170
+ checks.push({
1171
+ name: "daemon",
1172
+ status: "warn",
1173
+ detail: `record present but /health unreachable on port ${rec.port}`
1174
+ });
1175
+ }
1176
+ }
1177
+ async function checkNativeDeps(checks) {
1178
+ const { vecAvailability } = await import("@noir-ai/store");
1179
+ const vec = vecAvailability();
1180
+ const onnx = await probeOnnx();
1181
+ const vecOk = vec.ok === true;
1182
+ const onnxOk = onnx.ok === true;
1183
+ const status2 = vecOk ? onnxOk ? "ok" : "warn" : "fail";
1184
+ checks.push({
1185
+ name: "native deps",
1186
+ status: status2,
1187
+ detail: `better-sqlite3+sqlite-vec: ${vecOk ? "loadable" : vec.ok === false ? vec.reason : "unknown"} \xB7 onnxruntime-node: ${describeOnnx(onnx)}`
1188
+ });
1189
+ return { vecOk };
1190
+ }
1191
+ async function checkStore(checks, project, root) {
1192
+ if (!project) {
1193
+ checks.push({ name: "store", status: "warn", detail: "skipped \u2014 not initialized" });
1194
+ return;
1195
+ }
1196
+ const dbPath = paths.storeDb(root, project.id);
1197
+ if (!existsSync(dbPath)) {
1198
+ checks.push({
1199
+ name: "store",
1200
+ status: "warn",
1201
+ detail: `not created yet \u2014 the store DB is created when the daemon first runs (\`noir daemon start\` or index content); expected at ${dbPath}`
1202
+ });
1203
+ return;
1204
+ }
1205
+ const { openStore: openStore2 } = await import("@noir-ai/store");
1206
+ let store;
1207
+ try {
1208
+ store = await openStore2({ projectId: project.id, root, readonly: true });
1209
+ checks.push({
1210
+ name: "store",
1211
+ status: "ok",
1212
+ detail: `opens readonly (db: ${paths.storeDb(root, project.id)})`
1213
+ });
1214
+ } catch (e) {
1215
+ checks.push({ name: "store", status: "fail", detail: `open failed: ${msg(e)}` });
1216
+ } finally {
1217
+ if (store) {
1218
+ try {
1219
+ await Promise.resolve(store.close());
1220
+ } catch {
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1225
+ function checkNoirEnv(checks, root) {
1226
+ const path = join(paths.noirDir(root), ".env");
1227
+ let st;
1228
+ try {
1229
+ st = statSync(path);
1230
+ } catch {
1231
+ return;
1232
+ }
1233
+ const mode = st.mode & 511;
1234
+ const tooOpen = (st.mode & 63) !== 0;
1235
+ checks.push(
1236
+ tooOpen ? {
1237
+ name: "noir-env",
1238
+ status: "warn",
1239
+ detail: `${path} is ${mode.toString(8)} \u2014 readable by others; run chmod 600`
1240
+ } : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${mode.toString(8)})` }
1241
+ );
1242
+ }
1243
+ function checkEmbedder(checks, project, vecOk) {
1244
+ if (!project) {
1245
+ checks.push({ name: "embedder", status: "warn", detail: "skipped \u2014 not initialized" });
1246
+ return;
1247
+ }
1248
+ const emb = project.config.context.embedder;
1249
+ const kind = emb.kind;
1250
+ const model = typeof emb.model === "string" && emb.model.length > 0 ? emb.model : "<unset>";
1251
+ if (kind === "none") {
1252
+ checks.push({
1253
+ name: "embedder",
1254
+ status: "ok",
1255
+ detail: `kind=none (BM25-only) \xB7 dim=${emb.dim}`
1256
+ });
1257
+ return;
1258
+ }
1259
+ if (!vecOk) {
1260
+ checks.push({
1261
+ name: "embedder",
1262
+ status: "warn",
1263
+ detail: `${kind}/${model} (${emb.dim}-dim) configured but vec native layer unavailable \u2014 search degrades to BM25`
1264
+ });
1265
+ return;
1266
+ }
1267
+ checks.push({
1268
+ name: "embedder",
1269
+ status: "ok",
1270
+ detail: `${kind}/${model} (${emb.dim}-dim) \xB7 vec backend available`
1271
+ });
1272
+ }
1273
+ function checkProvider(checks, project) {
1274
+ if (!project) {
1275
+ checks.push({ name: "provider", status: "warn", detail: "skipped \u2014 not initialized" });
1276
+ return;
1277
+ }
1278
+ const resolved = resolveModelConfig(project.config.model);
1279
+ const names = Object.keys(resolved.providers);
1280
+ if (names.length === 0) {
1281
+ checks.push({
1282
+ name: "provider",
1283
+ status: "ok",
1284
+ detail: "none configured (offline mode; drafting degrades to templates)"
1285
+ });
1286
+ return;
1287
+ }
1288
+ const parts = [];
1289
+ let missing = false;
1290
+ for (const name of names) {
1291
+ const p = resolved.providers[name];
1292
+ if (!p) continue;
1293
+ const key = p.hasKey ? "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
1294
+ if (!p.hasKey && p.apiKeyEnv) missing = true;
1295
+ parts.push(`${name}/${p.model ?? "?"} (${key})`);
1296
+ }
1297
+ checks.push({
1298
+ name: "provider",
1299
+ status: missing ? "warn" : "ok",
1300
+ detail: parts.join(" \xB7 ")
1301
+ });
1302
+ }
1303
+ function buildInstallCheck(opts) {
1304
+ const { method, version, latestKnown } = opts;
1305
+ const parts = [`method=${method}`];
1306
+ if (version) parts.push(`v${version}`);
1307
+ const advisory = [];
1308
+ if (method !== "native") advisory.push("native recommended");
1309
+ if (latestKnown && version && latestKnown !== version) advisory.push("update available");
1310
+ const status2 = advisory.length > 0 ? "warn" : "ok";
1311
+ if (advisory.length > 0) parts.push(advisory.join(" + "));
1312
+ return {
1313
+ name: "install",
1314
+ status: status2,
1315
+ detail: parts.join(" \xB7 "),
1316
+ method,
1317
+ installedVersion: version,
1318
+ latestKnown
1319
+ };
1320
+ }
1321
+ function checkScaffoldVersion(checks, root) {
1322
+ const onDisk = readScaffoldVersion(root);
1323
+ const current = CURRENT_SCAFFOLD_VERSION;
1324
+ const drift = onDisk !== null && onDisk !== current;
1325
+ const status2 = onDisk === null || drift ? "warn" : "ok";
1326
+ const detail = onDisk === null ? "no .noir/scaffold-version stamp (run `noir init`)" : drift ? `on-disk ${onDisk} \u2260 current ${current} (run \`noir init --upgrade\`)` : `${onDisk} (up to date)`;
1327
+ checks.push({ name: "scaffold version", status: status2, detail });
1328
+ return { onDisk, current, drift };
1329
+ }
1330
+ function checkRulesMdBudget(checks, root, project) {
1331
+ if (!project) {
1332
+ checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
1333
+ return null;
1334
+ }
1335
+ const rulesPath = paths.rulesMd(root);
1336
+ if (!existsSync(rulesPath)) {
1337
+ checks.push({
1338
+ name: "rules budget",
1339
+ status: "ok",
1340
+ detail: "no .noir/rules/RULES.md"
1341
+ });
1342
+ return null;
1343
+ }
1344
+ let content;
1345
+ try {
1346
+ content = readFileSync(rulesPath, "utf8");
1347
+ } catch (e) {
1348
+ checks.push({
1349
+ name: "rules budget",
1350
+ status: "warn",
1351
+ detail: `RULES.md unreadable: ${msg(e)}`
1352
+ });
1353
+ return null;
1354
+ }
1355
+ const bytes = Buffer.byteLength(content, "utf8");
1356
+ const lineCount = content.length === 0 ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
1357
+ const lengthBudgetKb = project.config.rules.lengthBudgetKb;
1358
+ const budgetBytes = lengthBudgetKb * 1024;
1359
+ const maxLines = 150;
1360
+ const overBytes = bytes > budgetBytes;
1361
+ const overLines = lineCount > maxLines;
1362
+ const over = overBytes || overLines;
1363
+ const status2 = over ? "warn" : "ok";
1364
+ const kb = (n) => (n / 1024).toFixed(1);
1365
+ const detail = over ? `OVER: ${kb(bytes)} KB / ${lengthBudgetKb} KB \xB7 ${lineCount}/${maxLines} lines \u2014 trim RULES.md (failure-backed clauses only) or raise rules.lengthBudgetKb` : `${kb(bytes)} KB / ${lengthBudgetKb} KB \xB7 ${lineCount}/${maxLines} lines (within budget)`;
1366
+ checks.push({ name: "rules budget", status: status2, detail });
1367
+ return {
1368
+ onDisk: { bytes, lines: lineCount },
1369
+ budget: { kb: lengthBudgetKb, maxLines },
1370
+ over
1371
+ };
1372
+ }
1373
+ function summarize(checks) {
1374
+ let ok = 0;
1375
+ let warnN = 0;
1376
+ let fail2 = 0;
1377
+ for (const c2 of checks) {
1378
+ if (c2.status === "ok") ok++;
1379
+ else if (c2.status === "warn") warnN++;
1380
+ else fail2++;
1381
+ }
1382
+ return { ok, warn: warnN, fail: fail2 };
1383
+ }
1384
+ function checkHostArtifacts(checks, root, project) {
1385
+ if (!project) {
1386
+ checks.push({ name: "host artifacts", status: "warn", detail: "skipped \u2014 not initialized" });
1387
+ return null;
1388
+ }
1389
+ const host = project.config.host;
1390
+ const adapter = resolveAdapter(host);
1391
+ const ectx = { root };
1392
+ const expected = [];
1393
+ const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
1394
+ if (emitsAgentsMd) {
1395
+ expected.push(relOr(adapter.agentsMdPath?.(ectx) ?? "AGENTS.md", root));
1396
+ }
1397
+ if (host === "claude") expected.push("CLAUDE.md");
1398
+ else if (host === "gemini") expected.push("GEMINI.md");
1399
+ expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
1400
+ const missing = expected.filter((p) => !existsSync(joinRoot(root, p)));
1401
+ const status2 = missing.length === 0 ? "ok" : "warn";
1402
+ const detail = missing.length === 0 ? `host=${host} \xB7 all ${expected.length} primary artifacts present` : `host=${host} \xB7 missing ${missing.join(", ")} (run \`noir sync\` to restore)`;
1403
+ checks.push({ name: "host artifacts", status: status2, detail });
1404
+ return { active: host, expected, missing };
1405
+ }
1406
+ function checkNestedNoir(checks, root) {
1407
+ const candidates = [
1408
+ ".noir/.noir",
1409
+ ".noir/CLAUDE.md",
1410
+ ".noir/.mcp.json",
1411
+ ".noir/.claude",
1412
+ ".noir/.gitignore",
1413
+ ".noir/.dockerignore",
1414
+ ".noir/.npmignore",
1415
+ ".noir/.prettierignore"
1416
+ ];
1417
+ const found = candidates.filter((rel) => existsSync(join(root, rel)));
1418
+ const detected = found.length > 0;
1419
+ checks.push({
1420
+ name: "nested .noir",
1421
+ status: detected ? "warn" : "ok",
1422
+ detail: detected ? `nested Noir artifacts inside .noir/ (${found.join(", ")}) \u2014 likely from running \`noir init\` inside .noir/. Remove them; the real project is at ${root}.` : "no nested .noir/ artifacts"
1423
+ });
1424
+ return { detected, paths: found };
1425
+ }
1426
+ function collectDedupCandidates(root) {
1427
+ const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
1428
+ const out = [];
1429
+ for (const rel of rels) {
1430
+ const abs = join(root, rel);
1431
+ if (!existsSync(abs)) continue;
1432
+ let text;
1433
+ try {
1434
+ text = readFileSync(abs, "utf8");
1435
+ } catch {
1436
+ continue;
1437
+ }
1438
+ if (text.trim().length === 0) continue;
1439
+ out.push({ path: rel, text });
1440
+ }
1441
+ return out;
1442
+ }
1443
+ async function checkSemanticDupDoctor(checks, root, project, opts = {}) {
1444
+ if (!project) {
1445
+ checks.push({ name: "semantic dup", status: "warn", detail: "skipped \u2014 not initialized" });
1446
+ return;
1447
+ }
1448
+ const candidates = collectDedupCandidates(root);
1449
+ if (candidates.length < 2) {
1450
+ checks.push({
1451
+ name: "semantic dup",
1452
+ status: "ok",
1453
+ detail: `${candidates.length} candidate file(s); nothing to compare`
1454
+ });
1455
+ return;
1456
+ }
1457
+ const ctx = await import("@noir-ai/context");
1458
+ let embed = opts.embed;
1459
+ if (!embed) {
1460
+ const cfg = ctx.resolveEmbedderConfig(project.config.context);
1461
+ if (cfg.kind === "none") {
1462
+ checks.push({
1463
+ name: "semantic dup",
1464
+ status: "warn",
1465
+ detail: "embedder kind=none \u2014 set context.embedder.kind=local to enable semantic dedup"
1466
+ });
1467
+ return;
1468
+ }
1469
+ try {
1470
+ embed = ctx.createEmbedFn(cfg).embed;
1471
+ } catch (e) {
1472
+ checks.push({
1473
+ name: "semantic dup",
1474
+ status: "warn",
1475
+ detail: `embedder unavailable \u2014 skipped (${msg(e)})`
1476
+ });
1477
+ return;
1478
+ }
1479
+ }
1480
+ if (!embed) return;
1481
+ const pairs = await ctx.findSemanticDuplicates(candidates, embed, ctx.DEFAULT_DUP_THRESHOLD);
1482
+ checks.push({
1483
+ name: "semantic dup",
1484
+ status: pairs.length > 0 ? "warn" : "ok",
1485
+ detail: pairs.length === 0 ? `no near-duplicates across ${candidates.length} file(s)` : `${pairs.length} near-duplicate pair${pairs.length === 1 ? "" : "s"}: ${pairs.map((p) => `${p.a}\u2248${p.b} (${p.similarity.toFixed(2)})`).join("; ")}`
1486
+ });
1487
+ }
1488
+ function resolveWorkspacePackagesDir() {
1489
+ let here = dirname(fileURLToPath(import.meta.url));
1490
+ for (let i = 0; i < 8; i++) {
1491
+ const pkgPath = join(here, "package.json");
1492
+ if (existsSync(pkgPath)) {
1493
+ try {
1494
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1495
+ if (pkg.name === "@noir-ai/cli") {
1496
+ const packagesDir = dirname(here);
1497
+ if (existsSync(join(dirname(packagesDir), "pnpm-workspace.yaml"))) {
1498
+ return packagesDir;
1499
+ }
1500
+ return null;
1501
+ }
1502
+ } catch {
1503
+ }
1504
+ }
1505
+ const parent = dirname(here);
1506
+ if (parent === here) break;
1507
+ here = parent;
1508
+ }
1509
+ return null;
1510
+ }
1511
+ function npmPackDryRun(cliPkgDir) {
1512
+ const probe = spawnSync("npm", ["--version"], { encoding: "utf8", shell: true });
1513
+ if (probe.status !== 0 || probe.error) {
1514
+ return { error: probe.error ? probe.error.message : "npm not on PATH" };
1515
+ }
1516
+ const res = spawnSync("npm", ["pack", "--dry-run"], {
1517
+ cwd: cliPkgDir,
1518
+ encoding: "utf8",
1519
+ shell: true
1520
+ });
1521
+ if (res.status !== 0 || res.error) {
1522
+ return { error: res.error ? res.error.message : `npm pack exited ${res.status ?? "?"}` };
1523
+ }
1524
+ const out = `${res.stdout ?? ""}
1525
+ ${res.stderr ?? ""}`;
1526
+ const totalMatch = out.match(/total files:\s*(\d+)/i);
1527
+ const fileCount = totalMatch?.[1] ? Number.parseInt(totalMatch[1], 10) : null;
1528
+ const distIncluded = /\bdist\//.test(out);
1529
+ return { fileCount, distIncluded };
1530
+ }
1531
+ function checkPublish(checks, packagesDir, opts = {}) {
1532
+ if (packagesDir === null) {
1533
+ checks.push({
1534
+ name: "publish",
1535
+ status: "ok",
1536
+ detail: "skipped \u2014 not a monorepo checkout (no packages/* workspace)"
1537
+ });
1538
+ return null;
1539
+ }
1540
+ let entries = [];
1541
+ try {
1542
+ entries = readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1543
+ } catch (e) {
1544
+ checks.push({
1545
+ name: "publish",
1546
+ status: "warn",
1547
+ detail: `could not read ${packagesDir}: ${msg(e)}`
1548
+ });
1549
+ return { checked: 0, issues: [`unreadable packages dir: ${msg(e)}`] };
1550
+ }
1551
+ const issues = [];
1552
+ let checked = 0;
1553
+ let cliDir = null;
1554
+ for (const name of entries) {
1555
+ const pkgPath = join(packagesDir, name, "package.json");
1556
+ if (!existsSync(pkgPath)) continue;
1557
+ let pkg;
1558
+ try {
1559
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1560
+ } catch (e) {
1561
+ issues.push(`${name}/package.json: unparseable (${msg(e)})`);
1562
+ checked++;
1563
+ continue;
1564
+ }
1565
+ checked++;
1566
+ const pkgName = typeof pkg.name === "string" ? pkg.name : "";
1567
+ if (!pkgName.startsWith("@noir-ai/")) {
1568
+ issues.push(`${name}/package.json: name '${pkgName}' is not @noir-ai/*`);
1569
+ }
1570
+ const version = typeof pkg.version === "string" ? pkg.version : "";
1571
+ if (!SEMVER_ISH.test(version)) {
1572
+ issues.push(`${name}/package.json: version '${version}' is not semver`);
1573
+ }
1574
+ if (!Array.isArray(pkg.files) || pkg.files.length === 0) {
1575
+ issues.push(`${name}/package.json: files missing or empty`);
1576
+ }
1577
+ if (pkgName === "@noir-ai/cli") {
1578
+ if (pkg.bin == null || typeof pkg.bin === "object" && Object.keys(pkg.bin).length === 0) {
1579
+ issues.push(`${name}/package.json: bin missing (cli package must declare a bin)`);
1580
+ }
1581
+ cliDir = join(packagesDir, name);
1582
+ }
1583
+ }
1584
+ let packNote = "";
1585
+ if (cliDir && opts.skipNpmPack !== true) {
1586
+ const pack = npmPackDryRun(cliDir);
1587
+ if ("error" in pack) {
1588
+ packNote = ` \xB7 npm pack skipped (${pack.error})`;
1589
+ } else {
1590
+ const count = pack.fileCount == null ? "?" : String(pack.fileCount);
1591
+ const dist = pack.distIncluded ? "dist/ included" : "dist/ NOT in tarball";
1592
+ packNote = ` \xB7 npm pack: ${count} files, ${dist}`;
1593
+ if (pack.distIncluded === false) {
1594
+ issues.push("cli npm pack --dry-run: dist/ absent from the tarball");
1595
+ }
1596
+ }
1597
+ }
1598
+ const status2 = issues.length > 0 ? "warn" : "ok";
1599
+ const detail = issues.length === 0 ? `${checked}/${entries.length} packages publish-ready${packNote}` : `${issues.length} issue${issues.length === 1 ? "" : "s"} across ${checked} package${checked === 1 ? "" : "s"}${packNote}`;
1600
+ checks.push({ name: "publish", status: status2, detail });
1601
+ return { checked, issues };
1602
+ }
1603
+ function relOr(abs, root) {
1604
+ const posix = abs.replace(/\\/g, "/");
1605
+ if (posix.startsWith("/")) {
1606
+ const rel = relative(root, abs).replace(/\\/g, "/");
1607
+ if (rel.length === 0 || rel.startsWith("..") || rel.startsWith("/")) {
1608
+ throw new Error(`doctor: host path '${abs}' is not under root '${root}'`);
1609
+ }
1610
+ return rel;
1611
+ }
1612
+ if (posix.split("/").some((seg) => seg === "..")) {
1613
+ throw new Error(`doctor: host path '${abs}' escapes root '${root}'`);
1614
+ }
1615
+ return posix;
1616
+ }
1617
+ function joinRoot(root, relPath) {
1618
+ return `${root}/${relPath}`;
1619
+ }
1620
+ function severityBadge(s) {
1621
+ const state = s === "fail" ? "error" : s;
1622
+ return badge(state, s.toUpperCase());
1623
+ }
1624
+ function renderHuman(payload, opts) {
1625
+ log(
1626
+ `noir doctor \u2014 ${payload.checks.length} check${payload.checks.length === 1 ? "" : "s"}`,
1627
+ opts
1628
+ );
1629
+ table(
1630
+ payload.checks.map((c2) => ({
1631
+ Check: c2.name,
1632
+ Status: severityBadge(c2.status),
1633
+ Detail: c2.detail
1634
+ })),
1635
+ ["Check", "Status", "Detail"],
1636
+ opts
1637
+ );
1638
+ const { ok, warn: warnN, fail: fail2 } = payload.summary;
1639
+ if (fail2 > 0) {
1640
+ error(
1641
+ `${fail2} critical check${fail2 === 1 ? "" : "s"} failed (${warnN} warning${warnN === 1 ? "" : "s"}, ${ok} ok)`,
1642
+ opts
1643
+ );
1644
+ } else if (warnN > 0) {
1645
+ warn(`all critical checks passed (${warnN} warning${warnN === 1 ? "" : "s"}, ${ok} ok)`, opts);
1646
+ } else {
1647
+ success(`all ${ok} check${ok === 1 ? "" : "s"} passed`, opts);
1648
+ }
1649
+ }
1650
+ async function doctor(opts = {}) {
1651
+ const root = process.cwd();
1652
+ const checks = [];
1653
+ await checkRuntime(checks);
1654
+ const { project, result: configResult } = checkConfig(root);
1655
+ checks.push(configResult);
1656
+ await checkDaemon(checks);
1657
+ const { vecOk } = await checkNativeDeps(checks);
1658
+ await checkStore(checks, project, root);
1659
+ checkEmbedder(checks, project, vecOk);
1660
+ checkProvider(checks, project);
1661
+ checkNoirEnv(checks, root);
1662
+ const scaffold4 = checkScaffoldVersion(checks, root);
1663
+ const rules = checkRulesMdBudget(checks, root, project);
1664
+ const host = checkHostArtifacts(checks, root, project);
1665
+ checkNestedNoir(checks, root);
1666
+ if (opts.dedup === true) {
1667
+ await checkSemanticDupDoctor(checks, root, project);
1668
+ }
1669
+ const publish = checkPublish(checks, resolveWorkspacePackagesDir());
1670
+ const installCheck = buildInstallCheck({
1671
+ method: detectActiveMethod(),
1672
+ version: readInstallRecord()?.version ?? null,
1673
+ latestKnown: latestVersionFromCache(readUpdateCache(), "latest")
1674
+ });
1675
+ checks.push(installCheck);
1676
+ const summary = summarize(checks);
1677
+ const payload = {
1678
+ noir: NOIR_VERSION2,
1679
+ checks,
1680
+ summary,
1681
+ scaffold: scaffold4,
1682
+ rules,
1683
+ host,
1684
+ publish,
1685
+ install: {
1686
+ method: installCheck.method,
1687
+ installedVersion: installCheck.installedVersion,
1688
+ latestKnown: installCheck.latestKnown
1689
+ }
1690
+ };
1691
+ if (opts.json === true) {
1692
+ process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
1693
+ `);
1694
+ } else {
1695
+ renderHuman(payload, opts);
1696
+ }
1697
+ if (summary.fail > 0) {
1698
+ throw new NoirCliError(
1699
+ EXIT.ERROR,
1700
+ opts.json === true ? "" : `noir doctor: ${summary.fail} critical check(s) failed`
1701
+ );
1702
+ }
1703
+ }
1704
+ var SEMVER_ISH;
1705
+ var init_doctor = __esm({
1706
+ "src/commands/doctor.ts"() {
1707
+ "use strict";
1708
+ init_daemon_client();
1709
+ init_output();
1710
+ init_theme();
1711
+ SEMVER_ISH = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
1712
+ }
1713
+ });
1714
+
1715
+ // src/commands/install.ts
1716
+ var install_exports = {};
1717
+ __export(install_exports, {
1718
+ buildMigrationPlan: () => buildMigrationPlan,
1719
+ install: () => install,
1720
+ installManagedNode: () => installManagedNode
1721
+ });
1722
+ import { chmodSync, mkdirSync } from "fs";
1723
+ import { join as join2 } from "path";
1724
+ import {
1725
+ atomicWriteFile,
1726
+ detectActiveMethod as detectActiveMethod2,
1727
+ detectInstallMethods,
1728
+ ensureShimExecutable,
1729
+ loadProjectInfo as loadProjectInfo4,
1730
+ MANAGED_NODE_VERSION,
1731
+ NOIR_VERSION as NOIR_VERSION3,
1732
+ noirHome,
1733
+ provisionManagedNode,
1734
+ readInstallRecord as readInstallRecord2,
1735
+ runManagerCmd,
1736
+ writeInstallRecord
1737
+ } from "@noir-ai/core";
1738
+ function semverLt(a, b) {
1739
+ const seg = (s) => Number(s.replace(/^v/, "")) || 0;
1740
+ const pa = a.split(".").map(seg);
1741
+ const pb = b.split(".").map(seg);
1742
+ for (let i = 0; i < 3; i++) {
1743
+ const x = pa[i] ?? 0;
1744
+ const y = pb[i] ?? 0;
1745
+ if (x !== y) return x < y;
1746
+ }
1747
+ return false;
1748
+ }
1749
+ function buildMigrationPlan(opts) {
1750
+ const detected = opts.detected;
1751
+ const shouldMigrate = detected.length > 0 && opts.currentMethod !== "native";
1752
+ const nativeVersion = opts.targetSpec;
1753
+ let isDowngrade = false;
1754
+ if (opts.installedVersion && nativeVersion !== "latest" && nativeVersion !== "beta") {
1755
+ isDowngrade = semverLt(nativeVersion, opts.installedVersion);
1756
+ }
1757
+ const floor = opts.minVersion ?? "1.6.0";
1758
+ const belowMinVersion = nativeVersion !== "latest" && nativeVersion !== "beta" && semverLt(nativeVersion, floor);
1759
+ return {
1760
+ detected,
1761
+ currentMethod: opts.currentMethod,
1762
+ targetSpec: opts.targetSpec,
1763
+ installedVersion: opts.installedVersion,
1764
+ shouldMigrate,
1765
+ isDowngrade,
1766
+ belowMinVersion,
1767
+ nativeVersion,
1768
+ prevUninstallCmd: detected[0]?.uninstallCmd ?? null,
1769
+ autoUninstall: false
1770
+ };
1771
+ }
1772
+ async function installManagedNode(opts) {
1773
+ const spec = opts.version ?? opts.channel ?? "latest";
1774
+ const home2 = noirHome();
1775
+ const cliDir = join2(home2, "cli");
1776
+ const binDir = join2(home2, "bin");
1777
+ mkdirSync(cliDir, { recursive: true });
1778
+ mkdirSync(binDir, { recursive: true });
1779
+ let runtime;
1780
+ try {
1781
+ runtime = await provisionManagedNode({
1782
+ ...opts.provision ?? {},
1783
+ env: opts.env
1784
+ });
1785
+ } catch (err) {
1786
+ const reason = err instanceof Error ? err.message : String(err);
1787
+ return { ok: false, version: null, error: `managed-Node provision failed: ${reason}` };
1788
+ }
1789
+ if (runtime.source === "system") {
1790
+ warn(
1791
+ `managed-Node download failed; using system Node ${runtime.version} at ${runtime.nodeBin}.`
1792
+ );
1793
+ }
1794
+ const { nodeBin, npmBin } = runtime;
1795
+ const { code, stderr } = await runManagerCmd(
1796
+ [npmBin, "install", "-g", `@noir-ai/cli@${spec}`, `--prefix=${cliDir}`],
1797
+ { env: opts.env, timeoutMs: 12e4 }
1798
+ );
1799
+ if (code !== 0) {
1800
+ return { ok: false, version: null, error: `npm install failed: ${stderr.slice(0, 300)}` };
1801
+ }
1802
+ const shim = join2(binDir, "noir");
1803
+ const shimBody = `#!/usr/bin/env bash
1804
+ "${nodeBin}" "${join2(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js")}" "$@"
1805
+ `;
1806
+ atomicWriteFile(shim, shimBody);
1807
+ chmodSync(shim, 493);
1808
+ ensureShimExecutable();
1809
+ const ver = await runManagerCmd(
1810
+ [
1811
+ nodeBin,
1812
+ join2(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js"),
1813
+ "--version"
1814
+ ],
1815
+ { env: opts.env, timeoutMs: 3e4 }
1816
+ );
1817
+ const version = ver.code === 0 ? ver.stdout.trim() : null;
1818
+ writeInstallRecord({
1819
+ method: "native",
1820
+ version: version ?? "0.0.0",
1821
+ channel: opts.channel ?? "latest",
1822
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1823
+ // Reflect which runtime backs this install: the pinned managed version
1824
+ // (e.g. '22.23.2') for managed, or 'system-<v>' for the fallback so doctor
1825
+ // can distinguish a fallback install from a real managed one.
1826
+ managedRuntimeVersion: runtime.source === "managed" ? MANAGED_NODE_VERSION : `system-${runtime.version}`
1827
+ });
1828
+ return { ok: true, version, runtimeSource: runtime.source };
1829
+ }
1830
+ async function install(opts = {}) {
1831
+ let dismissOutcome;
1832
+ if (opts.dismiss === true) {
1833
+ const rec = readInstallRecord2();
1834
+ if (!rec) {
1835
+ dismissOutcome = { dismissed: false, version: null };
1836
+ if (!opts.json) info("No install record found; nothing to dismiss.");
1837
+ } else {
1838
+ const dismissed = new Set(rec.dismissedVersions ?? []);
1839
+ if (!dismissed.has(NOIR_VERSION3)) dismissed.add(NOIR_VERSION3);
1840
+ writeInstallRecord({ ...rec, dismissedVersions: [...dismissed] });
1841
+ dismissOutcome = { dismissed: true, version: NOIR_VERSION3 };
1842
+ if (!opts.json) success(`Migration banner dismissed for ${NOIR_VERSION3}.`);
1843
+ }
1844
+ }
1845
+ if (opts.dismiss === true && opts.list !== true) {
1846
+ if (opts.json) process.stdout.write(`${JSON.stringify({ ok: true, data: dismissOutcome })}
1847
+ `);
1848
+ return;
1849
+ }
1850
+ if (opts.list === true) {
1851
+ const detected2 = await detectInstallMethods(process.env);
1852
+ if (opts.json) {
1853
+ process.stdout.write(
1854
+ `${JSON.stringify({ ok: true, data: { ...dismissOutcome ?? {}, detected: detected2 } })}
1855
+ `
1856
+ );
1857
+ return;
1858
+ }
1859
+ info("Detected installs:");
1860
+ for (const d of detected2) info(` ${d.method} (${d.version ?? "unknown"})`);
1861
+ return;
1862
+ }
1863
+ const currentMethod = detectActiveMethod2();
1864
+ const detected = await detectInstallMethods(process.env);
1865
+ const installedRecord = readInstallRecord2();
1866
+ let minVersion = "1.6.0";
1867
+ try {
1868
+ minVersion = loadProjectInfo4(process.cwd()).config.update.minVersion;
1869
+ } catch {
1870
+ }
1871
+ const plan = buildMigrationPlan({
1872
+ detected,
1873
+ currentMethod,
1874
+ targetSpec: opts.spec ?? "latest",
1875
+ installedVersion: installedRecord?.version ?? null,
1876
+ minVersion
1877
+ });
1878
+ if (plan.belowMinVersion) {
1879
+ fail(
1880
+ EXIT.USAGE,
1881
+ `refusing install of ${plan.nativeVersion} (below minVersion ${minVersion})`,
1882
+ opts
1883
+ );
1884
+ }
1885
+ if (plan.isDowngrade) {
1886
+ fail(
1887
+ EXIT.USAGE,
1888
+ `refusing downgrade to ${plan.nativeVersion} (installed ${plan.installedVersion})`,
1889
+ opts
1890
+ );
1891
+ }
1892
+ const installSpin = spinner("Installing Noir via native installer...", opts).start();
1893
+ const result = await installManagedNode({
1894
+ channel: plan.nativeVersion === "beta" ? "beta" : void 0,
1895
+ version: plan.nativeVersion.startsWith("v") ? plan.nativeVersion : plan.nativeVersion === "latest" || plan.nativeVersion === "beta" ? void 0 : plan.nativeVersion,
1896
+ env: process.env
1897
+ });
1898
+ if (!result.ok) {
1899
+ installSpin.fail("Native install failed");
1900
+ fail(EXIT.ERROR, result.error ?? "native install failed", opts);
1901
+ }
1902
+ installSpin.succeed(`Installed noir ${result.version}`);
1903
+ if (plan.prevUninstallCmd && opts.uninstallPrev !== true) {
1904
+ warn(`To finish the migration, uninstall the previous install: ${plan.prevUninstallCmd}`);
1905
+ warn(
1906
+ " Or re-run with --uninstall-prev to do it now. Rollback: reinstall via the previous manager."
1907
+ );
1908
+ } else if (plan.prevUninstallCmd && opts.uninstallPrev === true) {
1909
+ const { code } = await runManagerCmd(plan.prevUninstallCmd.split(" "), { env: process.env });
1910
+ if (code === 0) success("Previous install removed.");
1911
+ else warn("Previous install NOT removed (non-zero exit). You can remove it manually.");
1912
+ }
1913
+ if (opts.json === true) {
1914
+ process.stdout.write(
1915
+ `${JSON.stringify({ ok: true, data: { version: result.version, runtimeSource: result.runtimeSource } })}
1916
+ `
1917
+ );
1918
+ return;
1919
+ }
1920
+ info(`Installed native: ${result.version}.`);
1921
+ info("If `noir` still points at the old install, run: hash -r && which -a noir");
1922
+ success(`noir ${result.version} installed via native. Run \`noir doctor\` to verify.`);
1923
+ }
1924
+ var init_install = __esm({
1925
+ "src/commands/install.ts"() {
1926
+ "use strict";
1927
+ init_output();
1928
+ }
1929
+ });
1930
+
1931
+ // src/commands/update.ts
1932
+ var update_exports = {};
1933
+ __export(update_exports, {
1934
+ DEFAULT_UPDATE_CONFIG: () => DEFAULT_UPDATE_CONFIG,
1935
+ buildUpdateNotice: () => buildUpdateNotice,
1936
+ buildUpdateTarget: () => buildUpdateTarget,
1937
+ runAsyncUpdateCheck: () => runAsyncUpdateCheck,
1938
+ update: () => update,
1939
+ updateAdvice: () => updateAdvice
1940
+ });
1941
+ import {
1942
+ detectActiveMethod as detectActiveMethod3,
1943
+ ensureShimExecutable as ensureShimExecutable2,
1944
+ fetchLatestVersion,
1945
+ loadProjectInfo as loadProjectInfo5,
1946
+ readInstallRecord as readInstallRecord3,
1947
+ readUpdateCache as readUpdateCache2,
1948
+ runManagerCmd as runManagerCmd2,
1949
+ shouldCheckForUpdate,
1950
+ writeUpdateCache
1951
+ } from "@noir-ai/core";
1952
+ function buildUpdateTarget(opts) {
1953
+ const targetSpec = opts.spec ?? opts.channel;
1954
+ const concrete = opts.spec != null && opts.spec !== "latest" && opts.spec !== "beta" ? opts.spec : opts.latestKnown;
1955
+ const isUpgrade = concrete != null && opts.currentVersion != null && semverGt(concrete, opts.currentVersion);
1956
+ const floor = opts.minVersion ?? "1.6.0";
1957
+ const belowMinVersion = concrete != null && semverLt2(concrete, floor);
1958
+ return {
1959
+ method: opts.method,
1960
+ targetSpec,
1961
+ currentVersion: opts.currentVersion,
1962
+ latestKnown: opts.latestKnown,
1963
+ isUpgrade,
1964
+ belowMinVersion
1965
+ };
1966
+ }
1967
+ function semverGt(a, b) {
1968
+ const seg = (s) => Number(s.replace(/^v/, "")) || 0;
1969
+ const pa = a.split(".").map(seg);
1970
+ const pb = b.split(".").map(seg);
1971
+ for (let i = 0; i < 3; i++) {
1972
+ const x = pa[i] ?? 0;
1973
+ const y = pb[i] ?? 0;
1974
+ if (x !== y) return x > y;
1975
+ }
1976
+ return false;
1977
+ }
1978
+ function semverLt2(a, b) {
1979
+ const seg = (s) => Number(s.replace(/^v/, "")) || 0;
1980
+ const pa = a.split(".").map(seg);
1981
+ const pb = b.split(".").map(seg);
1982
+ for (let i = 0; i < 3; i++) {
1983
+ const x = pa[i] ?? 0;
1984
+ const y = pb[i] ?? 0;
1985
+ if (x !== y) return x < y;
1986
+ }
1987
+ return false;
1988
+ }
1989
+ async function update(opts = {}) {
1990
+ if (process.env.NOIR_DISABLE_UPDATES !== void 0) {
1991
+ fail(
1992
+ EXIT.USAGE,
1993
+ "self-update is disabled (NOIR_DISABLE_UPDATES is set); update via your package manager or image rebuild.",
1994
+ opts
1995
+ );
1996
+ return;
1997
+ }
1998
+ const method = detectActiveMethod3();
1999
+ const rec = readInstallRecord3();
2000
+ const currentVersion = rec?.version ?? null;
2001
+ if (opts.check === true) {
2002
+ const checkSpin = spinner("Checking for updates...", opts).start();
2003
+ const latest2 = await fetchLatestVersion("latest");
2004
+ if (latest2) {
2005
+ checkSpin.succeed(`Latest: ${latest2} (you have ${currentVersion ?? "unknown"})`);
2006
+ if (opts.json === true) {
2007
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { latest: latest2, currentVersion } })}
2008
+ `);
2009
+ }
2010
+ } else {
2011
+ checkSpin.fail("Could not reach the registry.");
2012
+ fail(EXIT.ERROR, "Could not reach the registry.", opts);
2013
+ }
2014
+ return;
2015
+ }
2016
+ const fetchSpin = spinner("Checking for updates...", opts).start();
2017
+ const latest = await fetchLatestVersion("latest");
2018
+ if (latest === null) {
2019
+ fetchSpin.fail("Could not reach the registry.");
2020
+ fail(EXIT.ERROR, "Could not reach the registry.", opts);
2021
+ }
2022
+ fetchSpin.succeed(`Latest: ${latest}`);
2023
+ let minVersion = DEFAULT_UPDATE_CONFIG.minVersion;
2024
+ try {
2025
+ minVersion = loadProjectInfo5(process.cwd()).config.update.minVersion;
2026
+ } catch {
2027
+ }
2028
+ const target = buildUpdateTarget({
2029
+ method,
2030
+ channel: "latest",
2031
+ spec: opts.spec,
2032
+ currentVersion,
2033
+ latestKnown: latest,
2034
+ minVersion
2035
+ });
2036
+ if (target.belowMinVersion) {
2037
+ warn(`Target ${target.targetSpec} is below the minVersion floor (${minVersion}).`);
2038
+ fail(
2039
+ EXIT.USAGE,
2040
+ `refusing update to ${target.targetSpec} (below minVersion ${minVersion})`,
2041
+ opts
2042
+ );
2043
+ }
2044
+ if (!target.isUpgrade) {
2045
+ fetchSpin.succeed(`noir ${currentVersion} is up to date`);
2046
+ if (opts.json === true) {
2047
+ process.stdout.write(
2048
+ `${JSON.stringify({ ok: true, data: { version: currentVersion, upToDate: true } })}
2049
+ `
2050
+ );
2051
+ return;
2052
+ }
2053
+ info(`noir ${currentVersion} is up to date.`, opts);
2054
+ return;
2055
+ }
2056
+ if (method === "native") {
2057
+ const updateSpin = spinner(
2058
+ `Updating to ${target.latestKnown} via native installer...`,
2059
+ opts
2060
+ ).start();
2061
+ const res = await installManagedNode({
2062
+ version: target.targetSpec === "latest" ? void 0 : target.targetSpec,
2063
+ env: process.env
2064
+ });
2065
+ if (!res.ok) {
2066
+ updateSpin.fail("Update failed");
2067
+ fail(EXIT.ERROR, res.error ?? "update failed", opts);
2068
+ }
2069
+ ensureShimExecutable2();
2070
+ updateSpin.succeed(`Updated to noir ${res.version}`);
2071
+ if (opts.json === true) {
2072
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { version: res.version } })}
2073
+ `);
2074
+ return;
2075
+ }
2076
+ success(`Updated to ${res.version}.`);
2077
+ return;
2078
+ }
2079
+ const cmd = updateCmdFor(method, target.targetSpec);
2080
+ if (!cmd)
2081
+ fail(EXIT.USAGE, `cannot auto-update a ${method} install; use the manager directly`, opts);
2082
+ const mgrSpin = spinner(`Updating via ${method}...`, opts).start();
2083
+ const { code, stderr } = await runManagerCmd2(cmd, { env: process.env });
2084
+ if (code !== 0) {
2085
+ mgrSpin.fail(`Update failed`);
2086
+ fail(EXIT.ERROR, `update failed: ${stderr.slice(0, 300)}`, opts);
2087
+ }
2088
+ mgrSpin.succeed("Updated");
2089
+ if (opts.json === true) {
2090
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { version: target.targetSpec } })}
2091
+ `);
2092
+ return;
2093
+ }
2094
+ success("Updated.");
2095
+ }
2096
+ function updateCmdFor(method, spec) {
2097
+ switch (method) {
2098
+ case "npm":
2099
+ return ["npm", "install", "-g", `@noir-ai/cli@${spec}`];
2100
+ case "pnpm":
2101
+ return ["pnpm", "add", "-g", `@noir-ai/cli@${spec}`];
2102
+ case "yarn":
2103
+ return ["yarn", "global", "add", `@noir-ai/cli@${spec}`];
2104
+ case "bun":
2105
+ return ["bun", "add", "-g", `@noir-ai/cli@${spec}`];
2106
+ case "homebrew":
2107
+ return ["brew", "upgrade", "noir"];
2108
+ case "scoop":
2109
+ return ["scoop", "update", "noir"];
2110
+ default:
2111
+ return null;
2112
+ }
2113
+ }
2114
+ function updateAdvice(method) {
2115
+ switch (method) {
2116
+ case "npm":
2117
+ return "run `npm install -g @noir-ai/cli@latest`";
2118
+ case "pnpm":
2119
+ return "run `pnpm add -g @noir-ai/cli@latest`";
2120
+ case "yarn":
2121
+ return "run `yarn global add @noir-ai/cli@latest`";
2122
+ case "bun":
2123
+ return "run `bun add -g @noir-ai/cli@latest`";
2124
+ case "homebrew":
2125
+ return "run `brew upgrade noir`";
2126
+ case "scoop":
2127
+ return "run `scoop update noir`";
2128
+ case "native":
2129
+ return "run `noir update`";
2130
+ default:
2131
+ return "run `noir update`";
2132
+ }
2133
+ }
2134
+ function buildUpdateNotice(opts) {
2135
+ const { method, currentVersion, latestKnown } = opts;
2136
+ if (currentVersion == null || latestKnown == null) return null;
2137
+ if (!semverGt(latestKnown, currentVersion)) return null;
2138
+ return { latestVersion: latestKnown, currentVersion, method, advice: updateAdvice(method) };
2139
+ }
2140
+ async function runAsyncUpdateCheck(opts) {
2141
+ try {
2142
+ const cache = readUpdateCache2();
2143
+ if (!shouldCheckForUpdate({ env: opts.env, configUpdate: opts.configUpdate, cache })) return;
2144
+ const controller = new AbortController();
2145
+ const timer = setTimeout(() => controller.abort(), 2e3);
2146
+ const latest = await fetchLatestVersion(opts.configUpdate.channel, controller.signal);
2147
+ clearTimeout(timer);
2148
+ if (latest != null) {
2149
+ writeUpdateCache({
2150
+ lastCheckAt: (/* @__PURE__ */ new Date()).toISOString(),
2151
+ latestVersion: latest,
2152
+ channel: opts.configUpdate.channel
2153
+ });
2154
+ }
2155
+ } catch {
2156
+ }
2157
+ }
2158
+ var DEFAULT_UPDATE_CONFIG;
2159
+ var init_update = __esm({
2160
+ "src/commands/update.ts"() {
2161
+ "use strict";
2162
+ init_output();
2163
+ init_install();
2164
+ DEFAULT_UPDATE_CONFIG = {
2165
+ checkEnabled: true,
2166
+ checkIntervalHours: 24,
2167
+ channel: "latest",
2168
+ minVersion: "1.6.0",
2169
+ display: "notice"
2170
+ };
2171
+ }
2172
+ });
627
2173
 
628
2174
  // src/commands/status.ts
629
- import { loadProjectInfo as loadProjectInfo3, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
2175
+ import { loadProjectInfo as loadProjectInfo6, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
630
2176
  async function tryTool(caller, name, args) {
631
2177
  try {
632
2178
  return await caller.callTool(name, args);
@@ -677,7 +2223,7 @@ function pickMemory(m) {
677
2223
  }
678
2224
  function buildPayload(project, probe, host, store, context, workflow, memory) {
679
2225
  return {
680
- noir: NOIR_VERSION2,
2226
+ noir: NOIR_VERSION4,
681
2227
  project: { id: project.id, name: project.name },
682
2228
  host: project.config.host,
683
2229
  daemon: {
@@ -725,7 +2271,7 @@ function describeMemory(m) {
725
2271
  if (!m) return "unavailable (memory engine not wired)";
726
2272
  return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
727
2273
  }
728
- function renderHuman(p, opts) {
2274
+ function renderHuman2(p, opts) {
729
2275
  log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
730
2276
  definitionList(
731
2277
  [
@@ -745,7 +2291,12 @@ function renderHuman(p, opts) {
745
2291
  }
746
2292
  }
747
2293
  async function gatherStatusPayload(opts) {
748
- const project = loadProjectInfo3(process.cwd());
2294
+ let project;
2295
+ try {
2296
+ project = loadProjectInfo6(process.cwd());
2297
+ } catch {
2298
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2299
+ }
749
2300
  const probe = await probeDaemon(opts);
750
2301
  let host = null;
751
2302
  let store = null;
@@ -772,7 +2323,7 @@ async function status(opts) {
772
2323
  env: process.env,
773
2324
  configUpdate: (() => {
774
2325
  try {
775
- const info2 = loadProjectInfo3(process.cwd());
2326
+ const info2 = loadProjectInfo6(process.cwd());
776
2327
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
777
2328
  } catch {
778
2329
  return DEFAULT_UPDATE_CONFIG;
@@ -786,37 +2337,35 @@ async function status(opts) {
786
2337
  `);
787
2338
  return;
788
2339
  }
789
- renderHuman(payload, opts);
2340
+ renderHuman2(payload, opts);
790
2341
  }
2342
+ var init_status = __esm({
2343
+ "src/commands/status.ts"() {
2344
+ "use strict";
2345
+ init_daemon_client();
2346
+ init_output();
2347
+ init_theme();
2348
+ init_update();
2349
+ }
2350
+ });
791
2351
 
792
2352
  // src/commands/task.ts
793
- import { spawnSync } from "child_process";
794
- import { createHash } from "crypto";
795
- import { loadProjectInfo as loadProjectInfo4, nextArtifactSequence } from "@noir-ai/core";
2353
+ import { spawnSync as spawnSync2 } from "child_process";
2354
+ import { writeFileSync } from "fs";
2355
+ import { loadProjectInfo as loadProjectInfo7, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
796
2356
  import {
797
2357
  PHASES,
798
2358
  TASK_CLASSES,
799
2359
  writeChangelogStub,
800
2360
  writeDecisionStub
801
2361
  } from "@noir-ai/workflow";
802
- var PHASE_SKILL = {
803
- // C3 curation (2026-08-10): intake+clarify merged into brainstorming;
804
- // execute → executing-plans; verify → verifying; document → wrap.
805
- intake: "noir-brainstorming",
806
- clarify: "noir-brainstorming",
807
- spec: "noir-spec",
808
- plan: "noir-planning",
809
- execute: "noir-executing-plans",
810
- verify: "noir-verifying",
811
- document: "noir-wrap"
812
- };
813
2362
  function skillFor(phase) {
814
2363
  if (typeof phase !== "string") return null;
815
2364
  return PHASE_SKILL[phase] ?? null;
816
2365
  }
817
2366
  function writeDoneArtifacts(opts, taskId) {
818
2367
  try {
819
- const project = loadProjectInfo4(process.cwd());
2368
+ const project = loadProjectInfo7(process.cwd());
820
2369
  const conflict = { conflictPolicy: "preserve", interactive: false };
821
2370
  writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
822
2371
  const nextN = nextArtifactSequence(project.root, "adr");
@@ -861,8 +2410,8 @@ function renderStatusRow(s, opts) {
861
2410
  );
862
2411
  definitionList(rows, opts);
863
2412
  }
864
- var GATE_PHASES = ["spec", "plan", "verify"];
865
2413
  function nextGateAfter(phase) {
2414
+ if (phase === "verify") return "verify";
866
2415
  const cur = PHASES.indexOf(phase);
867
2416
  for (const p of GATE_PHASES) {
868
2417
  if (PHASES.indexOf(p) > cur) return p;
@@ -995,6 +2544,9 @@ async function taskAdvance(opts) {
995
2544
  if (typeof opts.force === "string" && opts.force.length > 0) {
996
2545
  args.force = { reason: opts.force };
997
2546
  }
2547
+ if (opts.skip === true) {
2548
+ args.skip = true;
2549
+ }
998
2550
  const res = await callDaemonTool(
999
2551
  opts,
1000
2552
  "workflow_advance",
@@ -1048,11 +2600,6 @@ async function taskResume(opts) {
1048
2600
  const res = await callDaemonTool(opts, "workflow_resume", args);
1049
2601
  if (res.resumable !== true) {
1050
2602
  const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "nothing to resume";
1051
- if (opts.json === true) {
1052
- process.stdout.write(`${JSON.stringify({ ok: false, resumable: false, error: detail })}
1053
- `);
1054
- return;
1055
- }
1056
2603
  fail(EXIT.ERROR, `task resume: ${detail}`, opts);
1057
2604
  }
1058
2605
  if (typeof opts.prompt === "string" && opts.prompt.length > 0) {
@@ -1122,7 +2669,7 @@ async function taskAbandon(opts) {
1122
2669
  }
1123
2670
  function runCheck(cmd) {
1124
2671
  try {
1125
- const buf = spawnSync("sh", ["-c", cmd], {
2672
+ const buf = spawnSync2("sh", ["-c", cmd], {
1126
2673
  stdio: ["ignore", "pipe", "pipe"],
1127
2674
  encoding: "utf8"
1128
2675
  });
@@ -1132,14 +2679,19 @@ function runCheck(cmd) {
1132
2679
  const status2 = buf.status;
1133
2680
  return {
1134
2681
  exitCode: typeof status2 === "number" ? status2 : 1,
1135
- digest: createHash("sha256").update(out).digest("hex")
2682
+ digest: sha256Hex(out)
1136
2683
  };
1137
2684
  } catch {
1138
- return { exitCode: 1, digest: createHash("sha256").update("").digest("hex") };
2685
+ return { exitCode: 1, digest: sha256Hex("") };
1139
2686
  }
1140
2687
  }
1141
2688
  async function taskVerify(opts) {
1142
- const project = loadProjectInfo4(process.cwd());
2689
+ let project;
2690
+ try {
2691
+ project = loadProjectInfo7(process.cwd());
2692
+ } catch {
2693
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2694
+ }
1143
2695
  const cfgChecks = project.config.workflow?.gate?.verify?.checks ?? [];
1144
2696
  const all = cfgChecks.map((c2) => ({
1145
2697
  name: c2.name,
@@ -1161,7 +2713,7 @@ async function taskVerify(opts) {
1161
2713
  if (checks.length === 0) {
1162
2714
  fail(
1163
2715
  EXIT.USAGE,
1164
- "task verify: no verify checks configured (set workflow.gate.verify.checks in noir.config)",
2716
+ "task verify: no verify checks configured (set workflow.gate.verify.checks in .noir/config.yml)",
1165
2717
  opts
1166
2718
  );
1167
2719
  }
@@ -1186,17 +2738,40 @@ async function taskVerify(opts) {
1186
2738
  const res = await callDaemonTool(opts, "workflow_advance", { evidence });
1187
2739
  if (res.ok === true) {
1188
2740
  if (opts.json === true) {
1189
- process.stdout.write(`${JSON.stringify({ ok: true, data: res, evidence })}
2741
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { ...res, evidence } })}
1190
2742
  `);
1191
2743
  return;
1192
2744
  }
1193
- success(`verify \u2014 ${evidence.summary} \u2192 gate approved`, opts);
2745
+ const landed = res.state;
2746
+ if (landed === "done") {
2747
+ success(`verify \u2014 ${evidence.summary} \u2192 gate approved`, opts);
2748
+ } else {
2749
+ info(
2750
+ `verify \u2014 ${evidence.summary} \u2192 advanced to ${landed} (the verify gate fires at \`done\`; run \`noir task verify\` again when there)`,
2751
+ opts
2752
+ );
2753
+ }
1194
2754
  renderStatusRow(res, opts);
1195
2755
  return;
1196
2756
  }
2757
+ const detail = res.error ?? res.pendingGate?.reason ?? "verify failed";
1197
2758
  if (opts.json === true) {
1198
- process.stdout.write(`${JSON.stringify({ evidence, ...res })}
1199
- `);
2759
+ process.stdout.write(
2760
+ `${JSON.stringify({
2761
+ ok: false,
2762
+ error: { code: EXIT.ERROR, message: `task verify: ${detail}` },
2763
+ // S9 `{ok:false, error}` + a payload — recovery context + evidence nest
2764
+ // under `data`, not as top-level siblings (same envelope shape as the
2765
+ // success path; consumers read `data` for the full picture).
2766
+ data: {
2767
+ ...res.pendingGate !== void 0 ? { pendingGate: res.pendingGate } : {},
2768
+ ...res.recovery !== void 0 ? { recovery: res.recovery } : {},
2769
+ evidence
2770
+ }
2771
+ })}
2772
+ `
2773
+ );
2774
+ process.exitCode = EXIT.ERROR;
1200
2775
  return;
1201
2776
  }
1202
2777
  if (res.pendingGate) {
@@ -1208,9 +2783,8 @@ async function taskVerify(opts) {
1208
2783
  ).join(" | ")}`,
1209
2784
  opts
1210
2785
  );
1211
- } else {
1212
- fail(EXIT.ERROR, `task verify: ${res.error ?? "verify failed"}`, opts);
1213
2786
  }
2787
+ fail(EXIT.ERROR, `task verify: ${detail}`, opts);
1214
2788
  }
1215
2789
  async function taskResearch(opts) {
1216
2790
  const s = await fetchStatus(opts, opts.id);
@@ -1254,7 +2828,7 @@ async function taskDecompose(opts) {
1254
2828
  if (cap.length === 0) fail(EXIT.USAGE, "task decompose: <capability-id> is required", opts);
1255
2829
  const plan = {
1256
2830
  capabilityId: cap,
1257
- intent: `Capability ${cap} \u2014 details TBD (run with a provider for an LLM-drafted plan)`,
2831
+ intent: `Capability ${cap} \u2014 offline template (run with a provider for an LLM-drafted plan)`,
1258
2832
  slices: [
1259
2833
  {
1260
2834
  id: "s1-walking-skeleton",
@@ -1272,6 +2846,19 @@ async function taskDecompose(opts) {
1272
2846
  ],
1273
2847
  status: { "s1-walking-skeleton": "planned" }
1274
2848
  };
2849
+ if (typeof opts.out === "string" && opts.out.length > 0) {
2850
+ try {
2851
+ writeFileSync(opts.out, `${JSON.stringify(plan, null, 2)}
2852
+ `, "utf8");
2853
+ } catch (err) {
2854
+ fail(
2855
+ EXIT.ERROR,
2856
+ `task decompose: could not write ${opts.out}: ${err instanceof Error ? err.message : String(err)}`,
2857
+ opts
2858
+ );
2859
+ }
2860
+ if (opts.json !== true) info(`wrote plan to ${opts.out}`, opts);
2861
+ }
1275
2862
  if (opts.json === true) {
1276
2863
  process.stdout.write(`${JSON.stringify({ ok: true, data: plan })}
1277
2864
  `);
@@ -1283,9 +2870,37 @@ async function taskDecompose(opts) {
1283
2870
  }
1284
2871
  tip("each piece enters the existing clarify\u2192spec\u2192plan\u2192execute\u2192verify\u2192document workflow", opts);
1285
2872
  }
2873
+ var PHASE_SKILL, GATE_PHASES;
2874
+ var init_task = __esm({
2875
+ "src/commands/task.ts"() {
2876
+ "use strict";
2877
+ init_daemon_client();
2878
+ init_output();
2879
+ init_theme();
2880
+ PHASE_SKILL = {
2881
+ // C3 curation (2026-08-10): intake+clarify merged into brainstorming;
2882
+ // execute → executing-plans; verify → verifying; document → wrap.
2883
+ intake: "noir-brainstorming",
2884
+ clarify: "noir-brainstorming",
2885
+ spec: "noir-spec",
2886
+ plan: "noir-planning",
2887
+ execute: "noir-executing-plans",
2888
+ verify: "noir-verifying",
2889
+ document: "noir-wrap"
2890
+ };
2891
+ GATE_PHASES = ["spec", "plan", "verify"];
2892
+ }
2893
+ });
1286
2894
 
1287
2895
  // src/commands/handoff.ts
1288
- var SEED_LIMIT = 5;
2896
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2897
+ import { join as join3 } from "path";
2898
+ import {
2899
+ defaultHandoffBlock,
2900
+ hostLaunchDirective,
2901
+ resolveAdapter as resolveAdapter2
2902
+ } from "@noir-ai/adapters";
2903
+ import { loadProjectInfo as loadProjectInfo8, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
1289
2904
  function heading(name, id) {
1290
2905
  return `# Noir handoff \u2014 ${name} (${id})`;
1291
2906
  }
@@ -1381,7 +2996,7 @@ function toHostPayload(p) {
1381
2996
  }
1382
2997
  function resolveDirective(root, p) {
1383
2998
  const ctx = { root };
1384
- const adapter = resolveAdapter(p.host);
2999
+ const adapter = resolveAdapter2(p.host);
1385
3000
  const hostPayload = toHostPayload(p);
1386
3001
  if (typeof adapter.emitHandoff === "function") {
1387
3002
  return adapter.emitHandoff(ctx, hostPayload);
@@ -1422,7 +3037,12 @@ async function probeOnly(opts) {
1422
3037
  return probeDaemon(opts);
1423
3038
  }
1424
3039
  async function handoff(opts) {
1425
- const project = loadProjectInfo5(process.cwd());
3040
+ let project;
3041
+ try {
3042
+ project = loadProjectInfo8(process.cwd());
3043
+ } catch {
3044
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
3045
+ }
1426
3046
  const host = project.config.host;
1427
3047
  const snapshot = await gatherStatusPayload(opts);
1428
3048
  const probe = await probeOnly(opts);
@@ -1448,45 +3068,34 @@ async function handoff(opts) {
1448
3068
  }
1449
3069
  };
1450
3070
  full.directive = resolveDirective(project.root, full);
1451
- if (opts.json === true) {
1452
- process.stdout.write(`${JSON.stringify({ ok: true, data: full })}
1453
- `);
1454
- return;
1455
- }
1456
- const md = renderMarkdown(full);
1457
3071
  if (opts.write === true) {
3072
+ const md = renderMarkdown(full);
1458
3073
  const path = handoffFilePath(project.root, task?.taskId, project.id);
1459
- mkdirSync(join(project.root, NOIR_DIR, "handoff"), { recursive: true });
1460
- writeFileSync(path, md, "utf8");
3074
+ mkdirSync2(join3(project.root, NOIR_DIR, "handoff"), { recursive: true });
3075
+ writeFileSync2(path, md, "utf8");
1461
3076
  info(`handoff written to ${path}`, opts);
3077
+ }
3078
+ if (opts.json === true) {
3079
+ process.stdout.write(`${JSON.stringify({ ok: true, data: full })}
3080
+ `);
1462
3081
  return;
1463
3082
  }
1464
- process.stdout.write(md);
3083
+ if (opts.write !== true) process.stdout.write(renderMarkdown(full));
1465
3084
  }
1466
-
1467
- // src/commands/home.ts
1468
- import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
1469
- import {
1470
- detectActiveMethod,
1471
- latestVersionFromCache,
1472
- loadProjectInfo as loadProjectInfo6,
1473
- NOIR_VERSION as NOIR_VERSION3,
1474
- readInstallRecord,
1475
- readUpdateCache
1476
- } from "@noir-ai/core";
3085
+ var SEED_LIMIT;
3086
+ var init_handoff = __esm({
3087
+ "src/commands/handoff.ts"() {
3088
+ "use strict";
3089
+ init_daemon_client();
3090
+ init_output();
3091
+ init_status();
3092
+ init_task();
3093
+ SEED_LIMIT = 5;
3094
+ }
3095
+ });
1477
3096
 
1478
3097
  // src/banner.ts
1479
3098
  import gradient from "gradient-string";
1480
- var NOIR_BLOCK = [
1481
- "\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
1482
- "\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
1483
- "\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
1484
- "\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551",
1485
- "\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
1486
- "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
1487
- ];
1488
- var NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
1489
- var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
1490
3099
  function renderBanner(opts = {}) {
1491
3100
  const width = opts.width ?? process.stdout.columns ?? 80;
1492
3101
  const colorOn = opts.color !== false && useColor();
@@ -1506,26 +3115,25 @@ function shouldShowBanner(opts) {
1506
3115
  if (v !== void 0 && v !== "") return false;
1507
3116
  return true;
1508
3117
  }
3118
+ var NOIR_BLOCK, NOIR_GRADIENT, NOIR_TAGLINE;
3119
+ var init_banner = __esm({
3120
+ "src/banner.ts"() {
3121
+ "use strict";
3122
+ init_theme();
3123
+ NOIR_BLOCK = [
3124
+ "\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
3125
+ "\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
3126
+ "\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
3127
+ "\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551",
3128
+ "\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
3129
+ "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
3130
+ ];
3131
+ NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
3132
+ NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
3133
+ }
3134
+ });
1509
3135
 
1510
3136
  // src/tui/commands/registry.ts
1511
- var DESTRUCTIVE_PREFIXES = [
1512
- ["context", "index"],
1513
- ["memory", "save"],
1514
- ["memory", "forget"],
1515
- ["memory", "consolidate"],
1516
- ["task", "new"],
1517
- ["task", "advance"],
1518
- ["skills", "sync"],
1519
- ["daemon", "start"],
1520
- ["daemon", "stop"],
1521
- ["daemon", "restart"],
1522
- ["init"],
1523
- ["create"],
1524
- ["sync"],
1525
- ["install"],
1526
- ["update"],
1527
- ["run"]
1528
- ];
1529
3137
  function isDestructive(argv) {
1530
3138
  return DESTRUCTIVE_PREFIXES.some((prefix) => {
1531
3139
  if (argv.length < prefix.length) return false;
@@ -1566,180 +3174,32 @@ function buildPaletteCommands(program2) {
1566
3174
  collectLeaves(program2, [], out);
1567
3175
  return out;
1568
3176
  }
3177
+ var DESTRUCTIVE_PREFIXES;
3178
+ var init_registry = __esm({
3179
+ "src/tui/commands/registry.ts"() {
3180
+ "use strict";
3181
+ DESTRUCTIVE_PREFIXES = [
3182
+ ["context", "index"],
3183
+ ["memory", "save"],
3184
+ ["memory", "forget"],
3185
+ ["memory", "consolidate"],
3186
+ ["task", "new"],
3187
+ ["task", "advance"],
3188
+ ["skills", "sync"],
3189
+ ["daemon", "start"],
3190
+ ["daemon", "stop"],
3191
+ ["daemon", "restart"],
3192
+ ["init"],
3193
+ ["create"],
3194
+ ["sync"],
3195
+ ["install"],
3196
+ ["update"],
3197
+ ["run"]
3198
+ ];
3199
+ }
3200
+ });
1569
3201
 
1570
3202
  // src/tui/commands/sections.ts
1571
- var HOME_SECTIONS = [
1572
- {
1573
- id: "status",
1574
- label: "Status & context",
1575
- hint: "snapshot \xB7 index \xB7 search \xB7 doctor",
1576
- items: [
1577
- {
1578
- id: "status",
1579
- label: "Status",
1580
- hint: "project + daemon + workflow + store snapshot (noir status)"
1581
- },
1582
- {
1583
- id: "context index",
1584
- label: "Index project",
1585
- hint: "(re)index files into context \xB7 \u26A0 destructive",
1586
- destructive: true
1587
- },
1588
- {
1589
- id: "context search",
1590
- label: "Search context",
1591
- hint: "hybrid search over indexed files",
1592
- needsArg: { prompt: "Context search query:", placeholder: "e.g. auth flow, deploy steps" }
1593
- },
1594
- {
1595
- id: "doctor",
1596
- label: "Doctor",
1597
- hint: "environment + project health checks"
1598
- },
1599
- {
1600
- id: "context status",
1601
- label: "Context status",
1602
- hint: "index freshness + counts"
1603
- }
1604
- ]
1605
- },
1606
- {
1607
- id: "memory",
1608
- label: "Memory",
1609
- hint: "recall \xB7 save \xB7 sessions \xB7 forget \xB7 consolidate",
1610
- items: [
1611
- {
1612
- id: "memory recall",
1613
- label: "Recall memory",
1614
- hint: "search cross-session memory",
1615
- needsArg: { prompt: "Recall query:", placeholder: "e.g. auth flow, deploy steps" }
1616
- },
1617
- {
1618
- id: "memory save",
1619
- label: "Save memory",
1620
- hint: "save an observation \xB7 \u26A0 destructive",
1621
- destructive: true,
1622
- needsArg: { prompt: "Memory content:", placeholder: "what you want to remember" }
1623
- },
1624
- {
1625
- id: "memory sessions",
1626
- label: "Sessions",
1627
- hint: "list recent memory sessions"
1628
- },
1629
- {
1630
- id: "memory forget",
1631
- label: "Forget memory",
1632
- hint: "forget observations by id \xB7 \u26A0 destructive",
1633
- destructive: true
1634
- },
1635
- {
1636
- id: "memory consolidate",
1637
- label: "Consolidate",
1638
- hint: "fold observations into lessons \xB7 \u26A0 destructive",
1639
- destructive: true
1640
- }
1641
- ]
1642
- },
1643
- {
1644
- id: "workflow",
1645
- label: "Workflow",
1646
- hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
1647
- items: [
1648
- {
1649
- id: "task next",
1650
- label: "Next task",
1651
- hint: "suggest next phase + applicable skill"
1652
- },
1653
- {
1654
- id: "task status",
1655
- label: "Task status",
1656
- hint: "active task status"
1657
- },
1658
- {
1659
- id: "task advance",
1660
- label: "Advance task",
1661
- hint: "advance to next phase \xB7 \u26A0 destructive",
1662
- destructive: true
1663
- },
1664
- {
1665
- id: "handoff",
1666
- label: "Handoff",
1667
- hint: "ready-to-paste host prompt"
1668
- },
1669
- {
1670
- id: "wrap",
1671
- label: "Wrap (end session)",
1672
- hint: "session-end handoff",
1673
- dispatch: ["wrap", "--write"]
1674
- }
1675
- ]
1676
- },
1677
- {
1678
- id: "setup",
1679
- label: "Setup & maintenance",
1680
- hint: "init \xB7 create \xB7 sync \xB7 skills \xB7 install \xB7 update",
1681
- items: [
1682
- {
1683
- id: "init",
1684
- label: "Init",
1685
- hint: "scaffold Noir in this project \xB7 \u26A0 destructive",
1686
- destructive: true
1687
- },
1688
- {
1689
- id: "create",
1690
- label: "Create",
1691
- hint: "bootstrap in a new/empty dir \xB7 \u26A0 destructive",
1692
- destructive: true
1693
- },
1694
- {
1695
- id: "sync",
1696
- label: "Sync",
1697
- hint: "re-emit managed files + skills \xB7 \u26A0 destructive",
1698
- destructive: true
1699
- },
1700
- {
1701
- id: "skills list",
1702
- label: "Skills list",
1703
- hint: "list installed skills"
1704
- },
1705
- {
1706
- id: "skills sync",
1707
- label: "Sync skills",
1708
- hint: "re-emit builtin skills \xB7 \u26A0 destructive",
1709
- destructive: true
1710
- },
1711
- {
1712
- id: "update",
1713
- label: "Update",
1714
- hint: "check for a new version \xB7 \u26A0 destructive",
1715
- destructive: true
1716
- },
1717
- {
1718
- id: "install",
1719
- label: "Install / migrate",
1720
- hint: "native install / migrate \xB7 \u26A0 destructive",
1721
- destructive: true
1722
- }
1723
- ]
1724
- },
1725
- {
1726
- id: "dashboard",
1727
- label: "Dashboard (full-screen)",
1728
- hint: "noir tui \xB7 noir palette \xB7 home",
1729
- items: [
1730
- {
1731
- id: "tui",
1732
- label: "Dashboard (full-screen)",
1733
- hint: "live status \xB7 /command \xB7 Ctrl+K palette (noir tui)"
1734
- },
1735
- {
1736
- id: "palette",
1737
- label: "All commands (fuzzy palette)",
1738
- hint: "fuzzy search over every command (noir palette)"
1739
- }
1740
- ]
1741
- }
1742
- ];
1743
3203
  async function resolveSections(commands) {
1744
3204
  const byId = new Map(commands.map((c2) => [c2.id, c2]));
1745
3205
  const out = [];
@@ -1759,11 +3219,198 @@ async function resolveSections(commands) {
1759
3219
  }
1760
3220
  return out;
1761
3221
  }
3222
+ var HOME_SECTIONS;
3223
+ var init_sections = __esm({
3224
+ "src/tui/commands/sections.ts"() {
3225
+ "use strict";
3226
+ HOME_SECTIONS = [
3227
+ {
3228
+ id: "status",
3229
+ label: "Status & context",
3230
+ hint: "snapshot \xB7 index \xB7 search \xB7 doctor",
3231
+ items: [
3232
+ {
3233
+ id: "status",
3234
+ label: "Status",
3235
+ hint: "project + daemon + workflow + store snapshot (noir status)"
3236
+ },
3237
+ {
3238
+ id: "context index",
3239
+ label: "Index project",
3240
+ hint: "(re)index files into context \xB7 \u26A0 destructive",
3241
+ destructive: true
3242
+ },
3243
+ {
3244
+ id: "context search",
3245
+ label: "Search context",
3246
+ hint: "hybrid search over indexed files",
3247
+ needsArg: { prompt: "Context search query:", placeholder: "e.g. auth flow, deploy steps" }
3248
+ },
3249
+ {
3250
+ id: "doctor",
3251
+ label: "Doctor",
3252
+ hint: "environment + project health checks"
3253
+ },
3254
+ {
3255
+ id: "context status",
3256
+ label: "Context status",
3257
+ hint: "index freshness + counts"
3258
+ }
3259
+ ]
3260
+ },
3261
+ {
3262
+ id: "memory",
3263
+ label: "Memory",
3264
+ hint: "recall \xB7 save \xB7 sessions \xB7 forget \xB7 consolidate",
3265
+ items: [
3266
+ {
3267
+ id: "memory recall",
3268
+ label: "Recall memory",
3269
+ hint: "search cross-session memory",
3270
+ needsArg: { prompt: "Recall query:", placeholder: "e.g. auth flow, deploy steps" }
3271
+ },
3272
+ {
3273
+ id: "memory save",
3274
+ label: "Save memory",
3275
+ hint: "save an observation \xB7 \u26A0 destructive",
3276
+ destructive: true,
3277
+ needsArg: { prompt: "Memory content:", placeholder: "what you want to remember" }
3278
+ },
3279
+ {
3280
+ id: "memory sessions",
3281
+ label: "Sessions",
3282
+ hint: "list recent memory sessions"
3283
+ },
3284
+ {
3285
+ id: "memory forget",
3286
+ label: "Forget memory",
3287
+ hint: "forget observations by id \xB7 \u26A0 destructive",
3288
+ destructive: true
3289
+ },
3290
+ {
3291
+ id: "memory consolidate",
3292
+ label: "Consolidate",
3293
+ hint: "fold observations into lessons \xB7 \u26A0 destructive",
3294
+ destructive: true
3295
+ }
3296
+ ]
3297
+ },
3298
+ {
3299
+ id: "workflow",
3300
+ label: "Workflow",
3301
+ hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
3302
+ items: [
3303
+ {
3304
+ id: "task next",
3305
+ label: "Next task",
3306
+ hint: "suggest next phase + applicable skill"
3307
+ },
3308
+ {
3309
+ id: "task status",
3310
+ label: "Task status",
3311
+ hint: "active task status"
3312
+ },
3313
+ {
3314
+ id: "task advance",
3315
+ label: "Advance task",
3316
+ hint: "advance to next phase \xB7 \u26A0 destructive",
3317
+ destructive: true
3318
+ },
3319
+ {
3320
+ id: "handoff",
3321
+ label: "Handoff",
3322
+ hint: "ready-to-paste host prompt"
3323
+ },
3324
+ {
3325
+ id: "wrap",
3326
+ label: "Wrap (end session)",
3327
+ hint: "session-end handoff",
3328
+ dispatch: ["wrap", "--write"]
3329
+ }
3330
+ ]
3331
+ },
3332
+ {
3333
+ id: "setup",
3334
+ label: "Setup & maintenance",
3335
+ hint: "init \xB7 create \xB7 sync \xB7 skills \xB7 install \xB7 update",
3336
+ items: [
3337
+ {
3338
+ id: "init",
3339
+ label: "Init",
3340
+ hint: "scaffold Noir in this project \xB7 \u26A0 destructive",
3341
+ destructive: true
3342
+ },
3343
+ {
3344
+ id: "create",
3345
+ label: "Create",
3346
+ hint: "bootstrap in a new/empty dir \xB7 \u26A0 destructive",
3347
+ destructive: true
3348
+ },
3349
+ {
3350
+ id: "sync",
3351
+ label: "Sync",
3352
+ hint: "re-emit managed files + skills \xB7 \u26A0 destructive",
3353
+ destructive: true
3354
+ },
3355
+ {
3356
+ id: "skills list",
3357
+ label: "Skills list",
3358
+ hint: "list installed skills"
3359
+ },
3360
+ {
3361
+ id: "skills sync",
3362
+ label: "Sync skills",
3363
+ hint: "re-emit builtin skills \xB7 \u26A0 destructive",
3364
+ destructive: true
3365
+ },
3366
+ {
3367
+ id: "update",
3368
+ label: "Update",
3369
+ hint: "check for a new version \xB7 \u26A0 destructive",
3370
+ destructive: true
3371
+ },
3372
+ {
3373
+ id: "install",
3374
+ label: "Install / migrate",
3375
+ hint: "native install / migrate \xB7 \u26A0 destructive",
3376
+ destructive: true
3377
+ }
3378
+ ]
3379
+ },
3380
+ {
3381
+ id: "dashboard",
3382
+ label: "Dashboard (full-screen)",
3383
+ hint: "noir tui \xB7 noir palette \xB7 home",
3384
+ items: [
3385
+ {
3386
+ id: "tui",
3387
+ label: "Dashboard (full-screen)",
3388
+ hint: "live status \xB7 /command \xB7 Ctrl+K palette (noir tui)"
3389
+ },
3390
+ {
3391
+ id: "palette",
3392
+ label: "All commands (fuzzy palette)",
3393
+ hint: "fuzzy search over every command (noir palette)"
3394
+ }
3395
+ ]
3396
+ }
3397
+ ];
3398
+ }
3399
+ });
1762
3400
 
1763
3401
  // src/commands/home.ts
3402
+ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
3403
+ import {
3404
+ detectActiveMethod as detectActiveMethod4,
3405
+ latestVersionFromCache as latestVersionFromCache2,
3406
+ loadProjectInfo as loadProjectInfo9,
3407
+ NOIR_VERSION as NOIR_VERSION5,
3408
+ readInstallRecord as readInstallRecord4,
3409
+ readUpdateCache as readUpdateCache3
3410
+ } from "@noir-ai/core";
1764
3411
  function tryProject() {
1765
3412
  try {
1766
- const info2 = loadProjectInfo6(process.cwd());
3413
+ const info2 = loadProjectInfo9(process.cwd());
1767
3414
  return { id: info2.id, name: info2.name, host: info2.config.host };
1768
3415
  } catch {
1769
3416
  return null;
@@ -1774,7 +3421,6 @@ function shouldShowMigrationBanner(rec, currentVersion) {
1774
3421
  if (rec.dismissedVersions?.includes(currentVersion)) return false;
1775
3422
  return true;
1776
3423
  }
1777
- var COMMANDS_HINT = "Commands: status \xB7 context \xB7 memory \xB7 task \xB7 skills \xB7 daemon \xB7 doctor \xB7 install \xB7 update \xB7 tui \xB7 palette";
1778
3424
  async function home(opts, deps) {
1779
3425
  if (isInteractive(opts) && opts.tui !== false) {
1780
3426
  await runMenu(opts, deps);
@@ -1927,7 +3573,7 @@ async function runMenu(opts, deps) {
1927
3573
  env: process.env,
1928
3574
  configUpdate: (() => {
1929
3575
  try {
1930
- const info2 = loadProjectInfo6(process.cwd());
3576
+ const info2 = loadProjectInfo9(process.cwd());
1931
3577
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
1932
3578
  } catch {
1933
3579
  return DEFAULT_UPDATE_CONFIG;
@@ -1948,8 +3594,8 @@ ${COMMANDS_HINT}
1948
3594
  `
1949
3595
  );
1950
3596
  }
1951
- const rec = readInstallRecord();
1952
- if (rec && shouldShowMigrationBanner(rec, NOIR_VERSION3)) {
3597
+ const rec = readInstallRecord4();
3598
+ if (rec && shouldShowMigrationBanner(rec, NOIR_VERSION5)) {
1953
3599
  process.stderr.write(
1954
3600
  `
1955
3601
  ${c.warn(`noir installed via ${rec.method}`)} \u2014 consider \`noir install\` for the native path (auto-update, no npm prefix/PATH issues). Dismiss with: \`noir install --dismiss\` (persisted per version).
@@ -1958,9 +3604,9 @@ ${COMMANDS_HINT}
1958
3604
  );
1959
3605
  }
1960
3606
  const updateNotice = buildUpdateNotice({
1961
- method: detectActiveMethod(),
1962
- currentVersion: NOIR_VERSION3,
1963
- latestKnown: latestVersionFromCache(readUpdateCache(), "latest")
3607
+ method: detectActiveMethod4(),
3608
+ currentVersion: NOIR_VERSION5,
3609
+ latestKnown: latestVersionFromCache2(readUpdateCache3(), "latest")
1964
3610
  });
1965
3611
  if (updateNotice) {
1966
3612
  process.stderr.write(
@@ -1974,6 +3620,19 @@ ${COMMANDS_HINT}
1974
3620
  const sections = await resolveSections(deps.commands ?? []);
1975
3621
  await runGroupedMenu(clack, opts, deps, sections);
1976
3622
  }
3623
+ var COMMANDS_HINT;
3624
+ var init_home = __esm({
3625
+ "src/commands/home.ts"() {
3626
+ "use strict";
3627
+ init_banner();
3628
+ init_output();
3629
+ init_theme();
3630
+ init_registry();
3631
+ init_sections();
3632
+ init_update();
3633
+ COMMANDS_HINT = "Commands: status \xB7 context \xB7 memory \xB7 task \xB7 skills \xB7 daemon \xB7 doctor \xB7 install \xB7 update \xB7 tui \xB7 palette";
3634
+ }
3635
+ });
1977
3636
 
1978
3637
  // src/commands/memory.ts
1979
3638
  function parseLimit2(raw, label, opts) {
@@ -2022,16 +3681,16 @@ async function memoryRecall(opts) {
2022
3681
  }
2023
3682
  if (!probe.running) {
2024
3683
  await withInProcessRead(opts, async (engines) => {
2025
- const result = await engines.memory.recall(opts.query, {
3684
+ const { hits: hits2, degraded, mode } = await engines.memory.recallWithMeta(opts.query, {
2026
3685
  ...limit === void 0 ? {} : { limit }
2027
3686
  });
2028
- const data2 = { query: opts.query, hits: result, degraded: false };
3687
+ const data2 = { query: opts.query, hits: hits2, degraded, mode };
2029
3688
  if (opts.json === true) {
2030
3689
  process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
2031
3690
  `);
2032
3691
  return;
2033
3692
  }
2034
- renderRecall(data2.query, result, data2.degraded, opts);
3693
+ renderRecall(data2.query, data2.hits, data2.degraded, opts);
2035
3694
  });
2036
3695
  return;
2037
3696
  }
@@ -2235,9 +3894,17 @@ async function memoryConsolidate(opts) {
2235
3894
  const suffix = logged ? " (logged)" : "";
2236
3895
  fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
2237
3896
  }
3897
+ var init_memory = __esm({
3898
+ "src/commands/memory.ts"() {
3899
+ "use strict";
3900
+ init_daemon_client();
3901
+ init_output();
3902
+ init_theme();
3903
+ }
3904
+ });
2238
3905
 
2239
3906
  // src/commands/release.ts
2240
- import { execSync, spawnSync as spawnSync2 } from "child_process";
3907
+ import { execSync, spawnSync as spawnSync3 } from "child_process";
2241
3908
  function describe(step, who) {
2242
3909
  const m = {
2243
3910
  preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
@@ -2256,21 +3923,6 @@ function describe(step, who) {
2256
3923
  };
2257
3924
  return `${m[step]} [${who}]`;
2258
3925
  }
2259
- var ALL = [
2260
- "preflight",
2261
- "bump",
2262
- "gate",
2263
- "commit",
2264
- "ci-develop",
2265
- "beta-tag",
2266
- "human-approve-beta",
2267
- "merge-main",
2268
- "ci-main",
2269
- "stable-tag",
2270
- "human-approve-stable",
2271
- "dist-bump",
2272
- "sync"
2273
- ];
2274
3926
  async function release(opts) {
2275
3927
  if (opts.dryRun) {
2276
3928
  log("noir release --dry-run (no changes will be made)", opts);
@@ -2296,7 +3948,7 @@ async function release(opts) {
2296
3948
  if (status2) fail(EXIT.ERROR, "release: tree must be clean", opts);
2297
3949
  success("preflight: \u2713 clean, on develop", opts);
2298
3950
  info(`[2/13] bump: ${describe("bump", "noir")}`, opts);
2299
- spawnSync2("node", ["scripts/bump-version.mjs", version], {
3951
+ spawnSync3("node", ["scripts/bump-version.mjs", version], {
2300
3952
  encoding: "utf8",
2301
3953
  stdio: "inherit"
2302
3954
  });
@@ -2308,8 +3960,8 @@ async function release(opts) {
2308
3960
  });
2309
3961
  success("gate: \u2713 full gate green", opts);
2310
3962
  info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
2311
- spawnSync2("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
2312
- spawnSync2("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
3963
+ spawnSync3("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
3964
+ spawnSync3("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
2313
3965
  encoding: "utf8",
2314
3966
  stdio: "inherit"
2315
3967
  });
@@ -2331,30 +3983,163 @@ async function release(opts) {
2331
3983
  fail(EXIT.ERROR, `release failed: ${err instanceof Error ? err.message : String(err)}`, opts);
2332
3984
  }
2333
3985
  }
3986
+ var ALL;
3987
+ var init_release = __esm({
3988
+ "src/commands/release.ts"() {
3989
+ "use strict";
3990
+ init_output();
3991
+ ALL = [
3992
+ "preflight",
3993
+ "bump",
3994
+ "gate",
3995
+ "commit",
3996
+ "ci-develop",
3997
+ "beta-tag",
3998
+ "human-approve-beta",
3999
+ "merge-main",
4000
+ "ci-main",
4001
+ "stable-tag",
4002
+ "human-approve-stable",
4003
+ "dist-bump",
4004
+ "sync"
4005
+ ];
4006
+ }
4007
+ });
2334
4008
 
2335
- // src/commands/run.ts
2336
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2337
- import { join as join2 } from "path";
2338
- import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
4009
+ // src/shell-bridge.ts
4010
+ import { spawn } from "child_process";
4011
+ import { basename } from "path";
4012
+ function isEligibleForShellFallback(name, opts = {}) {
4013
+ const { env = process.env, platform = process.platform } = opts;
4014
+ if (platform === "win32") return false;
4015
+ if (name.length === 0 || name.includes("/") || !NAME_RE.test(name)) return false;
4016
+ const shell = env.SHELL;
4017
+ if (!shell || shell.length === 0) return false;
4018
+ return SUPPORTED_SHELLS.has(basename(shell).toLowerCase());
4019
+ }
4020
+ function parseProbeOutput(stdout) {
4021
+ let path = "";
4022
+ let kind = "";
4023
+ for (const line of stdout.split("\n")) {
4024
+ if (line.startsWith("N:")) path = line.slice(2).trim();
4025
+ else if (line.startsWith("K:")) kind = line.slice(2).trim();
4026
+ }
4027
+ return { path, kind };
4028
+ }
4029
+ function classifyResolution(path, kind) {
4030
+ if (path.trim().startsWith("/")) return "path";
4031
+ const k = kind.trim().toLowerCase();
4032
+ if (k.includes("alias")) return "alias";
4033
+ if (k.includes("function")) return "function";
4034
+ return "none";
4035
+ }
4036
+ function shellKind(shell) {
4037
+ const kind = basename(shell).toLowerCase();
4038
+ return SUPPORTED_SHELLS.has(kind) ? kind : null;
4039
+ }
4040
+ function probeCommand(name, shell, opts) {
4041
+ const kind = shellKind(shell);
4042
+ const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS2;
4043
+ return new Promise((resolve2) => {
4044
+ const script = kind ? PROBE_SCRIPTS[kind] : "";
4045
+ const flags = kind ? PROBE_FLAGS[kind] : "-lic";
4046
+ const probeArgs = kind === "fish" ? [name] : ["noirbridge", name];
4047
+ const child = spawn(shell, [flags, script, ...probeArgs], {
4048
+ stdio: ["ignore", "pipe", "pipe"],
4049
+ detached: true,
4050
+ // new process group so the timeout can kill rc-spawned children
4051
+ ...opts.env ? { env: opts.env } : {}
4052
+ });
4053
+ let stdout = "";
4054
+ child.stdout.setEncoding("utf8");
4055
+ child.stdout.on("data", (c2) => {
4056
+ stdout += c2;
4057
+ });
4058
+ child.stderr.resume();
4059
+ const timer = setTimeout(() => {
4060
+ if (child.pid) {
4061
+ try {
4062
+ process.kill(-child.pid, "SIGKILL");
4063
+ } catch {
4064
+ }
4065
+ }
4066
+ resolve2({ stdout, timedOut: true });
4067
+ }, timeoutMs);
4068
+ child.on("error", () => {
4069
+ clearTimeout(timer);
4070
+ resolve2({ stdout, timedOut: false });
4071
+ });
4072
+ child.on("close", () => {
4073
+ clearTimeout(timer);
4074
+ resolve2({ stdout, timedOut: false });
4075
+ });
4076
+ });
4077
+ }
4078
+ async function resolveCommandViaShell(name, opts = {}) {
4079
+ const { env = process.env } = opts;
4080
+ if (!isEligibleForShellFallback(name, opts)) return { kind: "unsupported" };
4081
+ const shell = env.SHELL;
4082
+ const cached = pathCache.get(`${shell}:${name}`);
4083
+ if (cached) return { kind: "path", path: cached, shell };
4084
+ const probe = await probeCommand(name, shell, opts);
4085
+ if (probe.timedOut) {
4086
+ return {
4087
+ kind: "error",
4088
+ message: `shell resolution timed out after ${opts.timeoutMs ?? PROBE_TIMEOUT_MS2}ms`
4089
+ };
4090
+ }
4091
+ const { path, kind } = parseProbeOutput(probe.stdout);
4092
+ const cls = classifyResolution(path, kind);
4093
+ if (cls === "path") {
4094
+ pathCache.set(`${shell}:${name}`, path);
4095
+ return { kind: "path", path, shell };
4096
+ }
4097
+ if (cls === "alias" || cls === "function") return { kind: cls, shell };
4098
+ return { kind: "none", shell };
4099
+ }
4100
+ function buildBridgeArgs(name, hostArgs, shell) {
4101
+ const kind = shellKind(shell) ?? "zsh";
4102
+ if (kind === "fish") {
4103
+ return { binary: shell, args: ["-ic", `${name} $argv`, ...hostArgs] };
4104
+ }
4105
+ if (kind === "bash") {
4106
+ return {
4107
+ binary: shell,
4108
+ args: [
4109
+ "-lic",
4110
+ `shopt -s expand_aliases; [ -r "$HOME/.bashrc" ] && . "$HOME/.bashrc" 2>/dev/null; ${name} "$@"`,
4111
+ "noirbridge",
4112
+ ...hostArgs
4113
+ ]
4114
+ };
4115
+ }
4116
+ return { binary: shell, args: ["-lic", `${name} "$@"`, "noirbridge", ...hostArgs] };
4117
+ }
4118
+ var PROBE_TIMEOUT_MS2, NAME_RE, SUPPORTED_SHELLS, PROBE_FLAGS, PROBE_SCRIPTS, pathCache;
4119
+ var init_shell_bridge = __esm({
4120
+ "src/shell-bridge.ts"() {
4121
+ "use strict";
4122
+ PROBE_TIMEOUT_MS2 = 3e3;
4123
+ NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
4124
+ SUPPORTED_SHELLS = /* @__PURE__ */ new Set(["zsh", "bash", "fish"]);
4125
+ PROBE_FLAGS = {
4126
+ zsh: "-lic",
4127
+ bash: "-lic",
4128
+ fish: "-ic"
4129
+ };
4130
+ PROBE_SCRIPTS = {
4131
+ zsh: 'printf "\\nN:%s" "$(whence -p "$1")"; printf "\\nK:%s" "$(whence -w "$1")"',
4132
+ bash: 'shopt -s expand_aliases; [ -r "$HOME/.bashrc" ] && . "$HOME/.bashrc" 2>/dev/null; printf "\\nN:%s" "$(type -P "$1")"; printf "\\nK:%s" "$(type -t "$1")"',
4133
+ fish: 'printf "\\nN:%s" (type -P "$argv[1]"); printf "\\nK:%s" (type -t "$argv[1]")'
4134
+ };
4135
+ pathCache = /* @__PURE__ */ new Map();
4136
+ }
4137
+ });
2339
4138
 
2340
4139
  // src/orchestrator.ts
2341
- import { spawn } from "child_process";
4140
+ import { spawn as spawn2 } from "child_process";
4141
+ import { constants } from "os";
2342
4142
  import { createInterface } from "readline";
2343
- var HOST_BINARY = {
2344
- claude: "claude",
2345
- gemini: "gemini",
2346
- opencode: "opencode",
2347
- cursor: "cursor-agent",
2348
- "agents-md": null
2349
- // emits AGENTS.md — a file host, not a spawnable CLI
2350
- };
2351
- var HOST_FLAGS = {
2352
- claude: ["-p", "--output-format", "stream-json", "--verbose"],
2353
- gemini: ["-p", "--output-format", "stream-json"],
2354
- opencode: ["run"],
2355
- cursor: ["-p"],
2356
- "agents-md": []
2357
- };
2358
4143
  function resolveHostRun(host, customBinary) {
2359
4144
  const binary = customBinary && customBinary.length > 0 ? customBinary : HOST_BINARY[host];
2360
4145
  if (!binary) return null;
@@ -2369,6 +4154,10 @@ function str(v) {
2369
4154
  function num(v) {
2370
4155
  return typeof v === "number" && Number.isFinite(v) ? v : void 0;
2371
4156
  }
4157
+ function signalNumber(signal) {
4158
+ const n = constants.signals[signal];
4159
+ return typeof n === "number" ? n : void 0;
4160
+ }
2372
4161
  function usageFrom(v) {
2373
4162
  const o = asObj(v);
2374
4163
  if (!o) return void 0;
@@ -2416,7 +4205,14 @@ function normalizeStreamEvent(raw) {
2416
4205
  kind: "assistant",
2417
4206
  messageId: message ? str(message.id) : void 0,
2418
4207
  text: messageText(message),
2419
- usage: message ? usageFrom(message.usage) : void 0
4208
+ usage: message ? usageFrom(message.usage) : void 0,
4209
+ // An API error message is a diagnostic, not the answer — flag it so the
4210
+ // caller never streams it as assistant text (optional-when-true keeps
4211
+ // exact-shape consumers of the existing union intact).
4212
+ ...r.is_api_error_message === true || typeof r.error === "string" ? {
4213
+ isError: true,
4214
+ ...typeof r.error === "string" ? { errorCategory: r.error } : {}
4215
+ } : {}
2420
4216
  };
2421
4217
  }
2422
4218
  if (type === "result") {
@@ -2431,60 +4227,41 @@ function normalizeStreamEvent(raw) {
2431
4227
  }
2432
4228
  return { kind: "other" };
2433
4229
  }
2434
- var UsageReducer = class {
2435
- maxByMessage = /* @__PURE__ */ new Map();
2436
- input = 0;
2437
- output = 0;
2438
- costUsd = 0;
2439
- turns = 0;
2440
- /** Feed one normalized event into the reducer. */
2441
- add(event) {
2442
- if (event.kind === "assistant" && event.messageId && event.usage) {
2443
- const key = event.messageId;
2444
- const input = event.usage.inputTokens ?? 0;
2445
- const output = event.usage.outputTokens ?? 0;
2446
- const prev = this.maxByMessage.get(key);
2447
- if (!prev) {
2448
- this.maxByMessage.set(key, { input, output });
2449
- this.input += input;
2450
- this.output += output;
2451
- } else {
2452
- this.input += Math.max(0, input - prev.input);
2453
- this.output += Math.max(0, output - prev.output);
2454
- this.maxByMessage.set(key, {
2455
- input: Math.max(prev.input, input),
2456
- output: Math.max(prev.output, output)
2457
- });
2458
- }
2459
- } else if (event.kind === "result") {
2460
- if (event.totalCostUsd !== void 0) this.costUsd = event.totalCostUsd;
2461
- if (event.numTurns !== void 0) this.turns = event.numTurns;
2462
- }
2463
- }
2464
- /** The current accumulator snapshot. */
2465
- snapshot() {
2466
- return {
2467
- inputTokens: this.input,
2468
- outputTokens: this.output,
2469
- totalCostUsd: this.costUsd,
2470
- numTurns: this.turns
2471
- };
2472
- }
2473
- };
2474
4230
  function runHost(opts) {
2475
4231
  const spec = resolveHostRun(opts.host, opts.customBinary);
2476
- return new Promise((resolve, reject) => {
2477
- if (!spec) {
2478
- reject(new Error(`host '${opts.host}' is not a spawnable CLI (no default command)`));
2479
- return;
2480
- }
2481
- const child = spawn(spec.binary, [...spec.flags, opts.prompt], {
2482
- stdio: ["ignore", "pipe", "pipe"]
4232
+ if (!spec) {
4233
+ return Promise.reject(
4234
+ new Error(`host '${opts.host}' is not a spawnable CLI (no default command)`)
4235
+ );
4236
+ }
4237
+ return spawnAndConsume(
4238
+ spec.binary,
4239
+ [...spec.flags, ...opts.extraArgs ?? [], opts.prompt],
4240
+ opts,
4241
+ false
4242
+ );
4243
+ }
4244
+ function spawnAndConsume(binary, args, opts, shellRun) {
4245
+ return new Promise((resolve2, reject) => {
4246
+ const child = spawn2(binary, [...args], {
4247
+ stdio: ["ignore", "pipe", "pipe"],
4248
+ ...opts.env ? { env: opts.env } : {}
2483
4249
  });
2484
4250
  const reducer = new UsageReducer();
2485
4251
  let eventCount = 0;
2486
4252
  let stderrBuf = "";
2487
- child.on("error", (err) => reject(err));
4253
+ let errored = false;
4254
+ let errorText;
4255
+ let errorCategory;
4256
+ let spawnError = null;
4257
+ child.on("error", (err) => {
4258
+ spawnError = err;
4259
+ if (!shellRun && err.code === "ENOENT") {
4260
+ void shellFallback(binary, args, opts, resolve2, reject, err);
4261
+ return;
4262
+ }
4263
+ reject(err);
4264
+ });
2488
4265
  const rl = createInterface({ input: child.stdout });
2489
4266
  rl.on("line", (line) => {
2490
4267
  opts.onLine?.(line);
@@ -2493,6 +4270,13 @@ function runHost(opts) {
2493
4270
  const event = normalizeStreamEvent(raw);
2494
4271
  if (event === null) return;
2495
4272
  eventCount += 1;
4273
+ if (event.kind === "assistant" && event.isError === true) {
4274
+ errored = true;
4275
+ if (errorText === void 0 && event.text && event.text.length > 0) errorText = event.text;
4276
+ if (errorCategory === void 0 && event.errorCategory) errorCategory = event.errorCategory;
4277
+ } else if (event.kind === "result" && event.isError === true) {
4278
+ errored = true;
4279
+ }
2496
4280
  reducer.add(event);
2497
4281
  opts.onEvent?.(event);
2498
4282
  });
@@ -2500,13 +4284,161 @@ function runHost(opts) {
2500
4284
  child.stderr.on("data", (chunk) => {
2501
4285
  stderrBuf += chunk;
2502
4286
  });
2503
- child.on("close", (code) => {
2504
- resolve({ exitCode: code ?? 0, usage: reducer.snapshot(), eventCount, stderr: stderrBuf });
4287
+ child.on("close", (code, signal) => {
4288
+ if (spawnError !== null) return;
4289
+ const killed = code === null && signal != null;
4290
+ resolve2({
4291
+ exitCode: killed ? 128 + (signalNumber(signal) ?? 0) : code ?? 0,
4292
+ usage: reducer.snapshot(),
4293
+ eventCount,
4294
+ stderr: stderrBuf,
4295
+ isError: errored || killed,
4296
+ errorText: killed && errorText === void 0 ? `terminated by signal ${signal}` : errorText,
4297
+ errorCategory
4298
+ });
2505
4299
  });
2506
4300
  });
2507
4301
  }
4302
+ async function shellFallback(binary, args, opts, resolve2, reject, originalErr) {
4303
+ try {
4304
+ const res = await resolveCommandViaShell(binary, { env: opts.env ?? process.env });
4305
+ if (res.kind === "path") {
4306
+ resolve2(await spawnAndConsume(res.path, args, opts, true));
4307
+ return;
4308
+ }
4309
+ if (res.kind === "alias" || res.kind === "function") {
4310
+ const bridge = buildBridgeArgs(binary, args, res.shell);
4311
+ resolve2(await spawnAndConsume(bridge.binary, bridge.args, opts, true));
4312
+ return;
4313
+ }
4314
+ } catch {
4315
+ }
4316
+ reject(originalErr);
4317
+ }
4318
+ var HOST_BINARY, HOST_FLAGS, UsageReducer;
4319
+ var init_orchestrator = __esm({
4320
+ "src/orchestrator.ts"() {
4321
+ "use strict";
4322
+ init_shell_bridge();
4323
+ HOST_BINARY = {
4324
+ claude: "claude",
4325
+ gemini: "gemini",
4326
+ opencode: "opencode",
4327
+ cursor: "cursor-agent",
4328
+ "agents-md": null
4329
+ // emits AGENTS.md — a file host, not a spawnable CLI
4330
+ };
4331
+ HOST_FLAGS = {
4332
+ claude: ["-p", "--output-format", "stream-json", "--verbose"],
4333
+ gemini: ["-p", "--output-format", "stream-json"],
4334
+ opencode: ["run"],
4335
+ cursor: ["-p"],
4336
+ "agents-md": []
4337
+ };
4338
+ UsageReducer = class {
4339
+ maxByMessage = /* @__PURE__ */ new Map();
4340
+ input = 0;
4341
+ output = 0;
4342
+ costUsd = 0;
4343
+ turns = 0;
4344
+ /** Feed one normalized event into the reducer. */
4345
+ add(event) {
4346
+ if (event.kind === "assistant" && event.messageId && event.usage) {
4347
+ const key = event.messageId;
4348
+ const input = event.usage.inputTokens ?? 0;
4349
+ const output = event.usage.outputTokens ?? 0;
4350
+ const prev = this.maxByMessage.get(key);
4351
+ if (!prev) {
4352
+ this.maxByMessage.set(key, { input, output });
4353
+ this.input += input;
4354
+ this.output += output;
4355
+ } else {
4356
+ this.input += Math.max(0, input - prev.input);
4357
+ this.output += Math.max(0, output - prev.output);
4358
+ this.maxByMessage.set(key, {
4359
+ input: Math.max(prev.input, input),
4360
+ output: Math.max(prev.output, output)
4361
+ });
4362
+ }
4363
+ } else if (event.kind === "result") {
4364
+ if (event.totalCostUsd !== void 0) this.costUsd = event.totalCostUsd;
4365
+ if (event.numTurns !== void 0) this.turns = event.numTurns;
4366
+ }
4367
+ }
4368
+ /** The current accumulator snapshot. */
4369
+ snapshot() {
4370
+ return {
4371
+ inputTokens: this.input,
4372
+ outputTokens: this.output,
4373
+ totalCostUsd: this.costUsd,
4374
+ numTurns: this.turns
4375
+ };
4376
+ }
4377
+ };
4378
+ }
4379
+ });
4380
+
4381
+ // src/run-profiles.ts
4382
+ import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
4383
+ function loadRunConfig(root) {
4384
+ try {
4385
+ return loadProjectInfo10(root).config;
4386
+ } catch {
4387
+ return null;
4388
+ }
4389
+ }
4390
+ function expandEnvVars(value, env) {
4391
+ return value.replace(
4392
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
4393
+ (match, name) => env[name] !== void 0 ? env[name] : match
4394
+ );
4395
+ }
4396
+ function resolveRunProfile(requested, config, env) {
4397
+ const profiles = config.run?.profiles ?? {};
4398
+ const names = Object.keys(profiles);
4399
+ const name = requested ?? env.NOIR_PROFILE ?? config.run?.defaultProfile;
4400
+ if (name === void 0 || name.length === 0) {
4401
+ return { ok: true, profile: {} };
4402
+ }
4403
+ const profile = profiles[name];
4404
+ if (!profile) {
4405
+ const available = names.length > 0 ? names.join(", ") : "(none defined \u2014 add a run.profiles block to .noir/config.yml)";
4406
+ return {
4407
+ ok: false,
4408
+ message: `unknown run profile "${name}" \u2014 no profile named "${name}" is defined under run.profiles in .noir/config.yml. Available: ${available}`
4409
+ };
4410
+ }
4411
+ const envExpanded = {};
4412
+ if (profile.env) {
4413
+ for (const [k, v] of Object.entries(profile.env)) {
4414
+ envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env);
4415
+ }
4416
+ }
4417
+ return {
4418
+ ok: true,
4419
+ profile: { binary: profile.binary, env: envExpanded, args: profile.args, profileName: name }
4420
+ };
4421
+ }
4422
+ function listProfiles(config) {
4423
+ const profiles = config.run?.profiles ?? {};
4424
+ const def = config.run?.defaultProfile;
4425
+ return Object.keys(profiles).map((name) => ({
4426
+ NAME: name,
4427
+ DEFAULT: name === def ? "*" : "",
4428
+ BINARY: profiles[name]?.binary ?? ""
4429
+ }));
4430
+ }
4431
+ var init_run_profiles = __esm({
4432
+ "src/run-profiles.ts"() {
4433
+ "use strict";
4434
+ }
4435
+ });
2508
4436
 
2509
4437
  // src/commands/run.ts
4438
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
4439
+ import { join as join4 } from "path";
4440
+ import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
4441
+ import { parseConfig } from "@noir-ai/core";
2510
4442
  function formatUsage(u) {
2511
4443
  const cost = u.totalCostUsd > 0 ? ` $${u.totalCostUsd.toFixed(2)}` : "";
2512
4444
  const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
@@ -2514,7 +4446,7 @@ function formatUsage(u) {
2514
4446
  }
2515
4447
  function streamEvent(event, opts) {
2516
4448
  if (opts.json === true) return;
2517
- if (event.kind === "assistant" && event.text && event.text.length > 0) {
4449
+ if (event.kind === "assistant" && event.text && event.text.length > 0 && event.isError !== true) {
2518
4450
  process.stdout.write(event.text);
2519
4451
  }
2520
4452
  }
@@ -2524,100 +4456,108 @@ async function run(prompt, opts) {
2524
4456
  fail(EXIT.USAGE, `unknown host '${host}' (supported: ${SUPPORTED_HOSTS.join(", ")})`, opts);
2525
4457
  }
2526
4458
  if (prompt.length === 0) {
2527
- fail(EXIT.USAGE, "a prompt is required: `noir run <prompt>`", opts);
2528
- }
4459
+ const selectors = [];
4460
+ if (opts.command) selectors.push(`--command ${opts.command}`);
4461
+ if (opts.profile) selectors.push(`--profile ${opts.profile}`);
4462
+ const usage = selectors.length > 0 ? `noir run ${selectors.join(" ")} <prompt>` : "noir run <prompt>";
4463
+ fail(EXIT.USAGE, `a prompt is required: \`${usage}\``, opts);
4464
+ }
4465
+ const config = loadRunConfig(process.cwd()) ?? parseConfig({});
4466
+ const resolved = resolveRunProfile(opts.profile, config, process.env);
4467
+ if (!resolved.ok) fail(EXIT.USAGE, resolved.message, opts);
4468
+ const profile = resolved.profile;
4469
+ const customBinary = opts.command ?? profile.binary;
4470
+ const extraArgs = profile.args;
4471
+ const env = profile.env ? mergeEnv(process.env, profile.env) : void 0;
2529
4472
  const transcriptLines = [];
4473
+ let result;
2530
4474
  try {
2531
- const result = await runHost({
4475
+ result = await runHost({
2532
4476
  host,
2533
4477
  prompt,
2534
- customBinary: opts.command,
4478
+ customBinary,
4479
+ extraArgs,
4480
+ env,
2535
4481
  onLine: (line) => transcriptLines.push(line),
2536
4482
  onEvent: (event) => streamEvent(event, opts)
2537
4483
  });
2538
- if (opts.json !== true) {
2539
- process.stdout.write("\n");
2540
- }
2541
- const transcript = writeTranscript(host, transcriptLines);
2542
- if (opts.json === true) {
2543
- json({
2544
- ok: true,
2545
- data: {
2546
- host,
2547
- prompt,
2548
- exitCode: result.exitCode,
2549
- usage: result.usage,
2550
- numTurns: result.usage.numTurns,
2551
- events: result.eventCount,
2552
- transcript
2553
- }
2554
- });
2555
- return;
2556
- }
2557
- if (result.exitCode !== 0) {
2558
- log(`host exited ${result.exitCode}`, opts);
2559
- if (result.stderr.length > 0) log(result.stderr.trim(), opts);
2560
- }
2561
- success(`usage: ${formatUsage(result.usage)} (API-equivalent estimate, not billed)`, opts);
2562
- log(`transcript: ${transcript}`, opts);
2563
4484
  } catch (err) {
2564
- fail(
2565
- EXIT.ERROR,
2566
- `failed to run host '${host}': ${err instanceof Error ? err.message : String(err)}`,
2567
- opts
2568
- );
4485
+ const binary = customBinary && customBinary.length > 0 ? customBinary : host;
4486
+ const detail = err instanceof Error ? err.message : String(err);
4487
+ const enoent = err?.code === "ENOENT";
4488
+ const guidance = enoent ? ` No executable '${binary}' was found. Shell aliases and functions (e.g. from .zshrc) are invisible to noir \u2014 use an executable on PATH, an absolute path, or a launcher script such as ~/.local/bin/${binary}.` : "";
4489
+ const subject = customBinary && customBinary.length > 0 ? `custom command '${customBinary}'` : `host '${host}'`;
4490
+ fail(EXIT.ERROR, `failed to run ${subject}: ${detail}.${guidance}`, opts);
4491
+ }
4492
+ const transcript = writeTranscript(host, transcriptLines);
4493
+ const failed = result.exitCode !== 0 || result.isError;
4494
+ if (failed) {
4495
+ const binary = customBinary && customBinary.length > 0 ? customBinary : host;
4496
+ const reason = result.errorText && result.errorText.trim().length > 0 ? result.errorText.trim() : result.stderr.trim() || `exit code ${result.exitCode}`;
4497
+ const isAuth = result.errorCategory === "authentication_failed" || result.errorCategory === "oauth_org_not_allowed" || /not logged|login|authenticate|invalid api key/i.test(reason);
4498
+ let message = `host '${binary}' failed (exit ${result.exitCode}): ${reason}`;
4499
+ if (isAuth) {
4500
+ message += ` Open a terminal and run \`${binary} /login\` (interactive-only \u2014 it cannot run inside \`noir run\`), then retry.`;
4501
+ if (process.env.ANTHROPIC_API_KEY) {
4502
+ message += ` Note: ANTHROPIC_API_KEY is set in your environment \u2014 it overrides the logged-in account in \`-p\` mode; unset it (or fix the key) if you meant to use your subscription.`;
4503
+ }
4504
+ }
4505
+ message += ` If you use another profile, pass \`--command <binary>\` or define a run profile under run.profiles. transcript: ${transcript}`;
4506
+ fail(EXIT.ERROR, message, opts);
4507
+ }
4508
+ if (opts.json === true) {
4509
+ json({
4510
+ ok: true,
4511
+ data: {
4512
+ host,
4513
+ prompt,
4514
+ exitCode: result.exitCode,
4515
+ usage: result.usage,
4516
+ numTurns: result.usage.numTurns,
4517
+ events: result.eventCount,
4518
+ transcript
4519
+ }
4520
+ });
4521
+ return;
2569
4522
  }
4523
+ process.stdout.write("\n");
4524
+ success(`usage: ${formatUsage(result.usage)} (API-equivalent estimate, not billed)`, opts);
4525
+ log(`transcript: ${transcript}`, opts);
4526
+ }
4527
+ function mergeEnv(base, overlay) {
4528
+ const merged = { ...base, ...overlay };
4529
+ return Object.fromEntries(Object.entries(merged).filter(([, v]) => v !== void 0));
2570
4530
  }
2571
4531
  function writeTranscript(host, lines) {
2572
4532
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2573
- const dir = join2(process.cwd(), ".noir", "transcripts");
2574
- const file = join2(dir, `${host}-${ts}.jsonl`);
4533
+ const dir = join4(process.cwd(), ".noir", "transcripts");
4534
+ const file = join4(dir, `${host}-${ts}.jsonl`);
2575
4535
  try {
2576
- mkdirSync2(dir, { recursive: true });
2577
- writeFileSync2(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`);
4536
+ mkdirSync3(dir, { recursive: true, mode: 448 });
4537
+ writeFileSync3(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
2578
4538
  } catch {
2579
4539
  return "(not persisted)";
2580
4540
  }
2581
4541
  return file;
2582
4542
  }
4543
+ var init_run = __esm({
4544
+ "src/commands/run.ts"() {
4545
+ "use strict";
4546
+ init_orchestrator();
4547
+ init_output();
4548
+ init_run_profiles();
4549
+ }
4550
+ });
2583
4551
 
2584
4552
  // src/commands/skills.ts
2585
- import { claudeAdapter } from "@noir-ai/adapters";
2586
- import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
4553
+ import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
4554
+ import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
2587
4555
  import {
2588
4556
  buildRegistry,
2589
4557
  discoverAll,
2590
4558
  emitSkillsToDir,
2591
4559
  lintSkill
2592
4560
  } from "@noir-ai/skills";
2593
- var CATEGORY = {
2594
- "noir-brainstorming": "discovery",
2595
- "noir-sync": "discovery",
2596
- "noir-checkpoint": "discovery",
2597
- "noir-exploring": "discovery",
2598
- "noir-spec": "spec",
2599
- "noir-planning": "plan",
2600
- "noir-prd": "plan",
2601
- "noir-executing-plans": "execute",
2602
- "noir-test-driven-development": "execute",
2603
- "noir-systematic-debugging": "execute",
2604
- "noir-subagent": "execute",
2605
- "noir-parallel": "execute",
2606
- "noir-verifying": "verify",
2607
- "noir-security": "verify",
2608
- "noir-wrap": "document",
2609
- "noir-readme": "document",
2610
- "noir-shipping": "git",
2611
- "noir-worktree": "git",
2612
- "noir-recall": "memory",
2613
- "noir-remember": "memory",
2614
- "noir-context": "context",
2615
- "noir-frontend": "domain",
2616
- "noir-backend": "domain",
2617
- "noir-doctor": "meta",
2618
- "noir-writing-skills": "meta",
2619
- "noir-rules": "meta"
2620
- };
2621
4561
  function categoryOf(name) {
2622
4562
  const mapped = CATEGORY[name];
2623
4563
  if (mapped !== void 0) return mapped;
@@ -2677,8 +4617,14 @@ async function skillsList(opts) {
2677
4617
  }
2678
4618
  async function skillsSync(opts) {
2679
4619
  const root = process.cwd();
2680
- const project = loadProjectInfo7(root);
2681
- if (!claudeAdapter.skillsDir) {
4620
+ let project;
4621
+ try {
4622
+ project = loadProjectInfo11(root);
4623
+ } catch {
4624
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
4625
+ }
4626
+ const adapter = resolveAdapter3(project.config.host);
4627
+ if (!adapter.skillsDir) {
2682
4628
  const data2 = {
2683
4629
  emitted: [],
2684
4630
  references: 0,
@@ -2693,8 +4639,9 @@ async function skillsSync(opts) {
2693
4639
  info("This host has no skill emitter; nothing to sync.", opts);
2694
4640
  return;
2695
4641
  }
2696
- const dir = claudeAdapter.skillsDir({ root });
2697
- const summary = await emitSkillsToDir(dir);
4642
+ const dir = adapter.skillsDir({ root });
4643
+ const target = project.config.host;
4644
+ const summary = await emitSkillsToDir(dir, { target });
2698
4645
  const pruned = summary.pruned ?? [];
2699
4646
  const data = {
2700
4647
  emitted: summary.emitted,
@@ -2733,22 +4680,37 @@ async function skillsLint(opts) {
2733
4680
  const errored = skills.filter((s) => s.errors.length > 0);
2734
4681
  const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
2735
4682
  const data = { count: skills.length, errored: errored.length, skills };
2736
- if (opts.json === true) {
2737
- process.stdout.write(`${JSON.stringify({ ok: errored.length === 0, data })}
2738
- `);
2739
- return;
2740
- }
2741
- if (errored.length === 0) {
2742
- log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
2743
- } else {
4683
+ if (errored.length > 0) {
4684
+ const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
4685
+ if (opts.json === true) {
4686
+ process.stdout.write(
4687
+ `${JSON.stringify({ ok: false, error: { code: EXIT.ERROR, message }, data })}
4688
+ `
4689
+ );
4690
+ process.exitCode = EXIT.ERROR;
4691
+ return;
4692
+ }
2744
4693
  warn(
2745
4694
  `noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
2746
4695
  opts
2747
4696
  );
4697
+ for (const s of errored) {
4698
+ for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
4699
+ }
4700
+ if (warned.length > 0) {
4701
+ info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
4702
+ for (const s of warned) {
4703
+ for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
4704
+ }
4705
+ }
4706
+ fail(EXIT.ERROR, message, opts);
2748
4707
  }
2749
- for (const s of errored) {
2750
- for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
4708
+ if (opts.json === true) {
4709
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
4710
+ `);
4711
+ return;
2751
4712
  }
4713
+ log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
2752
4714
  if (warned.length > 0) {
2753
4715
  info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
2754
4716
  for (const s of warned) {
@@ -2788,8 +4750,725 @@ async function skillsRegistry(opts) {
2788
4750
  opts
2789
4751
  );
2790
4752
  }
4753
+ var CATEGORY;
4754
+ var init_skills = __esm({
4755
+ "src/commands/skills.ts"() {
4756
+ "use strict";
4757
+ init_output();
4758
+ CATEGORY = {
4759
+ "noir-brainstorming": "discovery",
4760
+ "noir-sync": "discovery",
4761
+ "noir-checkpoint": "discovery",
4762
+ "noir-exploring": "discovery",
4763
+ "noir-spec": "spec",
4764
+ "noir-planning": "plan",
4765
+ "noir-prd": "plan",
4766
+ "noir-executing-plans": "execute",
4767
+ "noir-test-driven-development": "execute",
4768
+ "noir-systematic-debugging": "execute",
4769
+ "noir-subagent": "execute",
4770
+ "noir-parallel": "execute",
4771
+ "noir-verifying": "verify",
4772
+ "noir-security": "verify",
4773
+ "noir-wrap": "document",
4774
+ "noir-readme": "document",
4775
+ "noir-shipping": "git",
4776
+ "noir-worktree": "git",
4777
+ "noir-recall": "memory",
4778
+ "noir-remember": "memory",
4779
+ "noir-context": "context",
4780
+ "noir-frontend": "domain",
4781
+ "noir-backend": "domain",
4782
+ "noir-doctor": "meta",
4783
+ "noir-writing-skills": "meta",
4784
+ "noir-rules": "meta"
4785
+ };
4786
+ }
4787
+ });
4788
+
4789
+ // src/conflict.ts
4790
+ import {
4791
+ lineDiff
4792
+ } from "@noir-ai/create";
4793
+ function flaggedNonInteractive() {
4794
+ const v = process.env.NOIR_NON_INTERACTIVE;
4795
+ return v !== void 0 && v !== "";
4796
+ }
4797
+ function buildConflictOpts(input = {}) {
4798
+ if (input.force === true) {
4799
+ return { conflictPolicy: "overwrite" };
4800
+ }
4801
+ if (input.interactive === false) {
4802
+ return { conflictPolicy: "preserve" };
4803
+ }
4804
+ if (input.interactive === true) {
4805
+ return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
4806
+ }
4807
+ if (flaggedNonInteractive() || !isInteractive()) {
4808
+ return { conflictPolicy: "preserve" };
4809
+ }
4810
+ return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
4811
+ }
4812
+ function renderDiffPreview(ctx) {
4813
+ const lines = lineDiff(ctx.existing, ctx.proposed);
4814
+ if (lines.length === 0) return;
4815
+ const out = [`noir: conflict in ${ctx.relPath}`];
4816
+ for (const dl of lines) {
4817
+ if (dl.type === "add") out.push(c.ok(`+ ${dl.line}`));
4818
+ else if (dl.type === "del") out.push(c.error(`- ${dl.line}`));
4819
+ else out.push(c.dim(` ${dl.line}`));
4820
+ }
4821
+ process.stderr.write(`${out.join("\n")}
4822
+ `);
4823
+ }
4824
+ async function clackConflictResolver(ctx) {
4825
+ renderDiffPreview(ctx);
4826
+ const clack = await import("@clack/prompts");
4827
+ const options = [
4828
+ {
4829
+ value: "replace",
4830
+ label: "Replace",
4831
+ hint: "overwrite with the template (discard your edits)"
4832
+ },
4833
+ { value: "rename", label: "Rename", hint: "keep yours as <file>.local; write the template" },
4834
+ {
4835
+ value: "duplicate",
4836
+ label: "Create duplicate",
4837
+ hint: "write template to <file>.noir; keep yours"
4838
+ },
4839
+ { value: "preserve", label: "Keep mine", hint: "skip; leave your file unchanged" },
4840
+ { value: "cancel", label: "Cancel", hint: "abort the scaffold" }
4841
+ ];
4842
+ if (ctx.mergedWithMarkers !== void 0) {
4843
+ options.splice(4, 0, {
4844
+ value: "merge",
4845
+ label: "Merge (with conflict markers)",
4846
+ hint: "write the 3-way merge with <<<<<<< / ||||||| / ======= / >>>>>>> markers"
4847
+ });
4848
+ }
4849
+ const choice = await clack.select({
4850
+ message: `${ctx.relPath} was edited and differs from the Noir template.`,
4851
+ initialValue: "preserve",
4852
+ options
4853
+ });
4854
+ if (clack.isCancel(choice)) return "cancel";
4855
+ const resolution = choice;
4856
+ const mode = ctx.mode ?? "regenerate";
4857
+ const supportsApplyToAll = mode !== "managedBlock" && mode !== "managedBlocks";
4858
+ if (supportsApplyToAll) {
4859
+ const remember = await clack.select({
4860
+ message: `Apply "${labelFor(resolution)}" to all remaining conflicts this run?`,
4861
+ initialValue: "no",
4862
+ options: [
4863
+ { value: "yes", label: "Yes", hint: "reuse this choice for the rest of this run" },
4864
+ { value: "no", label: "No", hint: "prompt again for each conflict" }
4865
+ ]
4866
+ });
4867
+ if (!clack.isCancel(remember) && remember === "yes") {
4868
+ return { resolution, applyToAll: true };
4869
+ }
4870
+ }
4871
+ return resolution;
4872
+ }
4873
+ function labelFor(resolution) {
4874
+ switch (resolution) {
4875
+ case "replace":
4876
+ return "Replace";
4877
+ case "rename":
4878
+ return "Rename";
4879
+ case "duplicate":
4880
+ return "Create duplicate";
4881
+ case "preserve":
4882
+ return "Keep mine";
4883
+ case "merge":
4884
+ return "Merge (with conflict markers)";
4885
+ case "cancel":
4886
+ return "Cancel";
4887
+ }
4888
+ }
4889
+ var init_conflict = __esm({
4890
+ "src/conflict.ts"() {
4891
+ "use strict";
4892
+ init_output();
4893
+ init_theme();
4894
+ }
4895
+ });
4896
+
4897
+ // src/dedup-write.ts
4898
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync4 } from "fs";
4899
+ import { join as join5 } from "path";
4900
+ import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
4901
+ async function checkWritePathDedup(root, res, opts) {
4902
+ const result = { found: [], conflicts: [] };
4903
+ const writtenHostFiles = res.written.filter((p) => HOST_CONTEXT_RELS.has(p));
4904
+ if (writtenHostFiles.length === 0) return result;
4905
+ const cache = loadCache(root);
4906
+ const embed = await resolveEmbedder(opts, cache);
4907
+ if (embed === void 0) return result;
4908
+ const cachedEmbed = makeCachedEmbed(cache, embed);
4909
+ for (const proposedRel of writtenHostFiles) {
4910
+ const proposedAbs = join5(root, proposedRel);
4911
+ let proposedText;
4912
+ try {
4913
+ proposedText = readFileSync2(proposedAbs, "utf8");
4914
+ } catch {
4915
+ continue;
4916
+ }
4917
+ if (proposedText.trim().length === 0) continue;
4918
+ const candidates = collectDedupCandidates(root).filter((c2) => c2.path !== proposedRel);
4919
+ if (candidates.length === 0) continue;
4920
+ const ctx = await import("@noir-ai/context");
4921
+ const proposed = { path: proposedRel, text: proposedText };
4922
+ let best;
4923
+ try {
4924
+ best = await ctx.findNearestDuplicate(
4925
+ proposed,
4926
+ candidates,
4927
+ async (t) => cachedEmbed(t),
4928
+ INFO_THRESHOLD
4929
+ );
4930
+ } catch (e) {
4931
+ process.stderr.write(
4932
+ `${c.dim(`noir: semantic-dedup skipped for ${proposedRel} (${errMsg(e)}).`)}
4933
+ `
4934
+ );
4935
+ continue;
4936
+ }
4937
+ if (best === null) continue;
4938
+ const matchedRel = best.a === proposedRel ? best.b : best.a;
4939
+ const sim = best.similarity;
4940
+ result.found.push({ proposedRel, matchedRel, similarity: sim });
4941
+ let resolution = "preserve";
4942
+ if (sim >= ACTION_THRESHOLD) {
4943
+ if (opts.interactive) {
4944
+ resolution = await promptDedupAction(root, proposedRel, matchedRel, proposedText, sim);
4945
+ } else {
4946
+ process.stderr.write(
4947
+ `${c.warn("noir: near-duplicate")} ${proposedRel} \u2248 ${matchedRel} (cosine ${sim.toFixed(2)}); creating anyway.
4948
+ `
4949
+ );
4950
+ }
4951
+ } else {
4952
+ process.stderr.write(
4953
+ `${c.dim(`noir: hint ${proposedRel} looks similar to existing ${matchedRel} (cosine ${sim.toFixed(2)}).`)}
4954
+ `
4955
+ );
4956
+ }
4957
+ const matchedText = safeRead(join5(root, matchedRel));
4958
+ result.conflicts.push({
4959
+ path: proposedRel,
4960
+ mode: "artifact",
4961
+ similarity: sim,
4962
+ existingSha: sha256Hex12(matchedText ?? ""),
4963
+ proposedSha: sha256Hex12(proposedText),
4964
+ resolution
4965
+ });
4966
+ }
4967
+ saveCache(root, cache);
4968
+ return result;
4969
+ }
4970
+ async function resolveEmbedder(opts, cache) {
4971
+ const ctx = await import("@noir-ai/context");
4972
+ let modelId;
4973
+ if (opts.project !== void 0) {
4974
+ const cfg = ctx.resolveEmbedderConfig(opts.project.config.context);
4975
+ if (cfg.kind === "none") {
4976
+ process.stderr.write(`${c.dim("noir: semantic-dedup skipped (embedder kind=none).")}
4977
+ `);
4978
+ return void 0;
4979
+ }
4980
+ modelId = cfg.kind === "local" ? cfg.model ?? ctx.DEFAULT_LOCAL_MODEL : cfg.model ?? cfg.kind;
4981
+ if (modelId !== void 0 && cache.model !== modelId) {
4982
+ cache.model = modelId;
4983
+ cache.entries = {};
4984
+ }
4985
+ }
4986
+ if (opts.embed !== void 0) return opts.embed;
4987
+ if (modelId === void 0) return void 0;
4988
+ let embed;
4989
+ try {
4990
+ const project = opts.project;
4991
+ if (project === void 0) return void 0;
4992
+ const cfg = ctx.resolveEmbedderConfig(project.config.context);
4993
+ embed = ctx.createEmbedFn(cfg).embed;
4994
+ } catch (e) {
4995
+ process.stderr.write(
4996
+ `${c.dim(`noir: semantic-dedup skipped (embedder unavailable: ${errMsg(e)}).`)}
4997
+ `
4998
+ );
4999
+ return void 0;
5000
+ }
5001
+ const ping = await withTimeout(
5002
+ embed("ping").then(() => true),
5003
+ EMBEDDER_PING_TIMEOUT_MS
5004
+ );
5005
+ if (ping !== true) {
5006
+ process.stderr.write(
5007
+ `${c.dim("noir: semantic-dedup skipped (embedder load too slow or unavailable).")}
5008
+ `
5009
+ );
5010
+ return void 0;
5011
+ }
5012
+ return embed;
5013
+ }
5014
+ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, sim) {
5015
+ const clack = await import("@clack/prompts");
5016
+ const choice = await clack.select({
5017
+ message: `${proposedRel} is near-duplicate of existing ${matchedRel} (cosine ${sim.toFixed(2)}). Create anyway?`,
5018
+ initialValue: "create",
5019
+ options: [
5020
+ {
5021
+ value: "create",
5022
+ label: "Create anyway",
5023
+ hint: "keep the new file as-is (default)"
5024
+ },
5025
+ { value: "skip", label: "Skip", hint: `delete ${proposedRel}; keep ${matchedRel}` },
5026
+ {
5027
+ value: "replace",
5028
+ label: "Replace existing",
5029
+ hint: `overwrite ${matchedRel} with this content`
5030
+ },
5031
+ {
5032
+ value: "mirror",
5033
+ label: "Mirror into existing",
5034
+ hint: `append this content to ${matchedRel}`
5035
+ }
5036
+ ]
5037
+ });
5038
+ if (clack.isCancel(choice)) return "preserve";
5039
+ if (choice === "skip") {
5040
+ try {
5041
+ rmSync(join5(root, proposedRel), { force: true });
5042
+ } catch {
5043
+ }
5044
+ return "preserve";
5045
+ } else if (choice === "replace") {
5046
+ try {
5047
+ writeFileSync4(join5(root, matchedRel), proposedText, "utf8");
5048
+ } catch {
5049
+ }
5050
+ return "replace";
5051
+ } else if (choice === "mirror") {
5052
+ try {
5053
+ const existing = readFileSync2(join5(root, matchedRel), "utf8");
5054
+ writeFileSync4(
5055
+ join5(root, matchedRel),
5056
+ `${existing}
5057
+
5058
+ <!-- mirrored from ${proposedRel} -->
5059
+ ${proposedText}
5060
+ `,
5061
+ "utf8"
5062
+ );
5063
+ } catch {
5064
+ }
5065
+ return "duplicate";
5066
+ }
5067
+ return "preserve";
5068
+ }
5069
+ function emptyCache() {
5070
+ return { version: DEDUP_CACHE_VERSION, model: "", entries: {} };
5071
+ }
5072
+ function cachePath(root) {
5073
+ return join5(root, ".noir", "dedup-cache.json");
5074
+ }
5075
+ function loadCache(root) {
5076
+ try {
5077
+ const raw = readFileSync2(cachePath(root), "utf8");
5078
+ const parsed = JSON.parse(raw);
5079
+ if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
5080
+ if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
5081
+ return {
5082
+ version: DEDUP_CACHE_VERSION,
5083
+ model: typeof parsed.model === "string" ? parsed.model : "",
5084
+ entries: parsed.entries
5085
+ };
5086
+ } catch {
5087
+ return emptyCache();
5088
+ }
5089
+ }
5090
+ function saveCache(root, cache) {
5091
+ try {
5092
+ mkdirSync4(join5(root, ".noir"), { recursive: true });
5093
+ writeFileSync4(cachePath(root), JSON.stringify(cache), "utf8");
5094
+ } catch {
5095
+ }
5096
+ }
5097
+ function makeCachedEmbed(cache, embed) {
5098
+ return async (text) => {
5099
+ const key = sha256Hex2(text);
5100
+ const cached = cache.entries[key];
5101
+ if (cached !== void 0) return Float32Array.from(cached.v);
5102
+ const v = await embed(text);
5103
+ cache.entries[key] = { v: Array.from(v) };
5104
+ return v;
5105
+ };
5106
+ }
5107
+ function safeRead(abs) {
5108
+ try {
5109
+ return readFileSync2(abs, "utf8");
5110
+ } catch {
5111
+ return void 0;
5112
+ }
5113
+ }
5114
+ function withTimeout(p, ms) {
5115
+ return new Promise((resolve2) => {
5116
+ const t = setTimeout(() => resolve2(void 0), ms);
5117
+ p.then(
5118
+ (v) => {
5119
+ clearTimeout(t);
5120
+ resolve2(v);
5121
+ },
5122
+ () => {
5123
+ clearTimeout(t);
5124
+ resolve2(void 0);
5125
+ }
5126
+ );
5127
+ });
5128
+ }
5129
+ function errMsg(e) {
5130
+ return e instanceof Error ? e.message : String(e);
5131
+ }
5132
+ var HOST_CONTEXT_RELS, ACTION_THRESHOLD, INFO_THRESHOLD, EMBEDDER_PING_TIMEOUT_MS, DEDUP_CACHE_VERSION;
5133
+ var init_dedup_write = __esm({
5134
+ "src/dedup-write.ts"() {
5135
+ "use strict";
5136
+ init_doctor();
5137
+ init_theme();
5138
+ HOST_CONTEXT_RELS = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"]);
5139
+ ACTION_THRESHOLD = 0.95;
5140
+ INFO_THRESHOLD = 0.85;
5141
+ EMBEDDER_PING_TIMEOUT_MS = 5e3;
5142
+ DEDUP_CACHE_VERSION = 1;
5143
+ }
5144
+ });
5145
+
5146
+ // src/init.ts
5147
+ import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
5148
+ import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
5149
+ import { scaffold } from "@noir-ai/create";
5150
+ import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
5151
+ async function init(root, opts) {
5152
+ assertTransportUrl(opts);
5153
+ const host = opts.host ?? "claude";
5154
+ const interactive = resolveInteractive();
5155
+ const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
5156
+ const dryRun = opts.dryRun === true || opts.preview === true;
5157
+ const res = await scaffold({
5158
+ root,
5159
+ mode: "init",
5160
+ host,
5161
+ transport: opts.transport,
5162
+ interactive,
5163
+ ...opts.url !== void 0 ? { url: opts.url } : {},
5164
+ ...opts.upgrade === true ? { upgrade: true } : {},
5165
+ ...opts.force === true ? { force: true } : {},
5166
+ ...dryRun ? { dryRun: true } : {},
5167
+ ...conflictOpts
5168
+ });
5169
+ if (dryRun) {
5170
+ reportPlannedWrites(res);
5171
+ return res;
5172
+ }
5173
+ if (res.noop) return res;
5174
+ await emitHostSkills(root, host, conflictOpts, interactive);
5175
+ let projectInfo;
5176
+ try {
5177
+ projectInfo = loadProjectInfo12(root);
5178
+ } catch {
5179
+ projectInfo = void 0;
5180
+ }
5181
+ const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
5182
+ if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
5183
+ process.stderr.write(
5184
+ `Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).
5185
+ `
5186
+ );
5187
+ process.stderr.write(
5188
+ "Next: run `noir` to open the home menu (or `noir status` for a snapshot).\n"
5189
+ );
5190
+ return res;
5191
+ }
5192
+ function reportPlannedWrites(res) {
5193
+ log("Dry run \u2014 no files were written.");
5194
+ if (res.written.length > 0) {
5195
+ log("Planned writes:");
5196
+ for (const p of res.written) log(` ${p}`);
5197
+ }
5198
+ if (res.skipped.length > 0) {
5199
+ log("Would leave as-is (already present):");
5200
+ for (const p of res.skipped) log(` ${p}`);
5201
+ }
5202
+ if (res.identical.length > 0) {
5203
+ log("Would rewrite (byte-identical, no-op):");
5204
+ for (const p of res.identical) log(` ${p}`);
5205
+ }
5206
+ }
5207
+ async function emitHostSkills(root, host, conflictOpts, interactive) {
5208
+ const adapter = resolveAdapter4(host);
5209
+ const skillsDir = adapter.skillsDir?.({ root });
5210
+ if (skillsDir === void 0) {
5211
+ process.stderr.write(`host '${host}' has no skill emitter; skipping skills
5212
+ `);
5213
+ return;
5214
+ }
5215
+ const target = host;
5216
+ const skillOpts = {
5217
+ includeIntegrations: true,
5218
+ target,
5219
+ conflictPolicy: conflictOpts.conflictPolicy,
5220
+ interactive
5221
+ };
5222
+ if (conflictOpts.onConflict !== void 0) {
5223
+ skillOpts.onConflict = conflictOpts.onConflict;
5224
+ }
5225
+ const summary = await emitSkillsToDir2(skillsDir, skillOpts);
5226
+ const relDir = skillsDir.replace(`${root}/`, "");
5227
+ process.stderr.write(
5228
+ `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
5229
+ `
5230
+ );
5231
+ }
5232
+ function assertTransportUrl(opts) {
5233
+ if (opts.transport === "streamable-http" && opts.url === void 0) {
5234
+ throw new Error("--transport streamable-http requires --url");
5235
+ }
5236
+ if (opts.url !== void 0) {
5237
+ assertLocalhostUrl(opts.url);
5238
+ }
5239
+ }
5240
+ function assertLocalhostUrl(raw) {
5241
+ let url;
5242
+ try {
5243
+ url = new URL(raw);
5244
+ } catch {
5245
+ throw new Error(`Invalid URL: ${raw}`);
5246
+ }
5247
+ if (!["http:", "https:"].includes(url.protocol)) {
5248
+ throw new Error("Only http/https URLs are supported");
5249
+ }
5250
+ if (!["localhost", "127.0.0.1", "::1"].includes(url.hostname)) {
5251
+ throw new Error(`Only localhost URLs are supported (got ${url.hostname})`);
5252
+ }
5253
+ }
5254
+ var init_init = __esm({
5255
+ "src/init.ts"() {
5256
+ "use strict";
5257
+ init_conflict();
5258
+ init_dedup_write();
5259
+ init_output();
5260
+ }
5261
+ });
5262
+
5263
+ // src/serve.ts
5264
+ import { applyNoirEnv, loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
5265
+ import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
5266
+ async function serve(opts) {
5267
+ applyNoirEnv(process.cwd());
5268
+ const project = loadProjectInfo13(process.cwd());
5269
+ if (opts.stdio) {
5270
+ await startStdioServer({ project, transport: "stdio", daemon: false });
5271
+ return;
5272
+ }
5273
+ try {
5274
+ const { url } = await ensureDaemonRunning3({
5275
+ project,
5276
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec
5277
+ });
5278
+ process.stderr.write(
5279
+ `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
5280
+ `
5281
+ );
5282
+ } catch (err) {
5283
+ process.stderr.write(
5284
+ `Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.
5285
+ `
5286
+ );
5287
+ await startStdioServer({ project, transport: "stdio", daemon: false });
5288
+ }
5289
+ }
5290
+ var init_serve = __esm({
5291
+ "src/serve.ts"() {
5292
+ "use strict";
5293
+ }
5294
+ });
5295
+
5296
+ // src/commands/create.ts
5297
+ var create_exports = {};
5298
+ __export(create_exports, {
5299
+ create: () => create
5300
+ });
5301
+ import { resolve } from "path";
5302
+ import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
5303
+ import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
5304
+ import { scaffold as scaffold2 } from "@noir-ai/create";
5305
+ import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
5306
+ async function create(dir, opts) {
5307
+ assertTransportUrl(opts);
5308
+ const root = resolve(dir ?? process.cwd());
5309
+ const host = opts.host ?? "claude";
5310
+ const interactive = resolveInteractive();
5311
+ const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
5312
+ const dryRun = opts.dryRun === true || opts.preview === true;
5313
+ const res = await scaffold2({
5314
+ root,
5315
+ mode: "create",
5316
+ host,
5317
+ transport: opts.transport,
5318
+ interactive,
5319
+ ...opts.url !== void 0 ? { url: opts.url } : {},
5320
+ ...opts.force === true ? { force: true } : {},
5321
+ ...dryRun ? { dryRun: true } : {},
5322
+ ...conflictOpts
5323
+ });
5324
+ if (dryRun) {
5325
+ reportPlannedWrites(res);
5326
+ return res;
5327
+ }
5328
+ if (res.noop) return res;
5329
+ const adapter = resolveAdapter5(host);
5330
+ const skillsDir = adapter.skillsDir?.({ root });
5331
+ if (skillsDir === void 0) {
5332
+ process.stderr.write(`host '${host}' has no skill emitter; skipping skills
5333
+ `);
5334
+ } else {
5335
+ const target = host;
5336
+ const skillOpts = {
5337
+ includeIntegrations: true,
5338
+ target,
5339
+ conflictPolicy: conflictOpts.conflictPolicy,
5340
+ interactive
5341
+ };
5342
+ if (conflictOpts.onConflict !== void 0) {
5343
+ skillOpts.onConflict = conflictOpts.onConflict;
5344
+ }
5345
+ const summary = await emitSkillsToDir3(skillsDir, skillOpts);
5346
+ const relDir = skillsDir.replace(`${root}/`, "");
5347
+ process.stderr.write(
5348
+ `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
5349
+ `
5350
+ );
5351
+ }
5352
+ let projectInfo;
5353
+ try {
5354
+ projectInfo = loadProjectInfo14(root);
5355
+ } catch {
5356
+ projectInfo = void 0;
5357
+ }
5358
+ const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
5359
+ if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
5360
+ process.stderr.write(`Noir created in ${root} (host: ${host}, transport: ${opts.transport}).
5361
+ `);
5362
+ process.stderr.write("Next: cd into the new directory, then run `noir` to open the home menu.\n");
5363
+ return res;
5364
+ }
5365
+ var init_create = __esm({
5366
+ "src/commands/create.ts"() {
5367
+ "use strict";
5368
+ init_conflict();
5369
+ init_dedup_write();
5370
+ init_init();
5371
+ init_output();
5372
+ }
5373
+ });
5374
+
5375
+ // src/sync.ts
5376
+ var sync_exports = {};
5377
+ __export(sync_exports, {
5378
+ sync: () => sync
5379
+ });
5380
+ import { existsSync as existsSync2 } from "fs";
5381
+ import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
5382
+ import { loadProjectInfo as loadProjectInfo15, paths as paths2 } from "@noir-ai/core";
5383
+ import { scaffold as scaffold3 } from "@noir-ai/create";
5384
+ import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
5385
+ async function sync(root, opts = {}) {
5386
+ const host = resolveSyncHost(root, opts);
5387
+ const interactive = resolveInteractive();
5388
+ const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
5389
+ let projectInfo;
5390
+ try {
5391
+ projectInfo = loadProjectInfo15(root);
5392
+ } catch {
5393
+ projectInfo = void 0;
5394
+ }
5395
+ const dryRun = opts.dryRun === true || opts.preview === true;
5396
+ const res = await scaffold3({
5397
+ root,
5398
+ mode: "sync",
5399
+ host,
5400
+ interactive,
5401
+ ...dryRun ? { dryRun: true } : {},
5402
+ ...conflictOpts,
5403
+ // Merge defaults TRUE inside scaffold; only forward an explicit opt-out.
5404
+ ...opts.mergeManagedRegions === false ? { mergeManagedRegions: false } : {}
5405
+ });
5406
+ if (dryRun) {
5407
+ reportPlannedWrites(res);
5408
+ return res;
5409
+ }
5410
+ if (res.written.length === 0 && res.identical.length > 0) {
5411
+ process.stderr.write("Nothing to sync (Noir-managed files are up to date).\n");
5412
+ }
5413
+ const adapter = resolveAdapter6(host);
5414
+ const skillsDir = adapter.skillsDir?.({ root });
5415
+ if (skillsDir === void 0) {
5416
+ process.stderr.write(`host '${host}' has no skill emitter; skipping skills
5417
+ `);
5418
+ } else {
5419
+ const target = host;
5420
+ const skillOpts = {
5421
+ includeIntegrations: true,
5422
+ target,
5423
+ conflictPolicy: conflictOpts.conflictPolicy,
5424
+ interactive
5425
+ };
5426
+ if (conflictOpts.onConflict !== void 0) {
5427
+ skillOpts.onConflict = conflictOpts.onConflict;
5428
+ }
5429
+ const summary = await emitSkillsToDir4(skillsDir, skillOpts);
5430
+ const relDir = skillsDir.replace(`${root}/`, "");
5431
+ process.stderr.write(
5432
+ `Synced ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
5433
+ `
5434
+ );
5435
+ const pruned = summary.pruned ?? [];
5436
+ if (pruned.length > 0) {
5437
+ process.stderr.write(
5438
+ `Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}
5439
+ `
5440
+ );
5441
+ }
5442
+ }
5443
+ const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
5444
+ if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
5445
+ return res;
5446
+ }
5447
+ function resolveSyncHost(root, opts) {
5448
+ if (opts.host !== void 0) return opts.host;
5449
+ if (!existsSync2(paths2.config(root))) return "claude";
5450
+ try {
5451
+ return loadProjectInfo15(root).config.host;
5452
+ } catch {
5453
+ return "claude";
5454
+ }
5455
+ }
5456
+ var init_sync = __esm({
5457
+ "src/sync.ts"() {
5458
+ "use strict";
5459
+ init_conflict();
5460
+ init_dedup_write();
5461
+ init_init();
5462
+ init_output();
5463
+ }
5464
+ });
2791
5465
 
2792
5466
  // src/bin.ts
5467
+ import { realpathSync } from "fs";
5468
+ import { pathToFileURL } from "url";
5469
+ import { SUPPORTED_HOSTS as SUPPORTED_HOSTS2 } from "@noir-ai/adapters";
5470
+ import { applyNoirEnv as applyNoirEnv2, NOIR_VERSION as NOIR_VERSION6 } from "@noir-ai/core";
5471
+ import { Command, Option } from "commander";
2793
5472
  function buildPaletteCommandsForTui() {
2794
5473
  return buildPaletteCommands(createProgram());
2795
5474
  }
@@ -2828,14 +5507,14 @@ function toStatusOptions(g) {
2828
5507
  json: g.json === true,
2829
5508
  quiet: g.quiet === true,
2830
5509
  verbose: g.verbose === true,
2831
- input: g.input !== false
5510
+ input: g.input !== false,
5511
+ ...g.tips === false ? { noTips: true } : {}
2832
5512
  };
2833
5513
  }
2834
5514
  function parseHost(raw) {
2835
5515
  if (raw === void 0) return void 0;
2836
5516
  return raw;
2837
5517
  }
2838
- var DEPRECATIONS = [];
2839
5518
  function commandPath(cmd) {
2840
5519
  const path = [];
2841
5520
  let c2 = cmd;
@@ -2859,7 +5538,7 @@ function emitDeprecationHintsFor(cmd, opts) {
2859
5538
  }
2860
5539
  function createProgram() {
2861
5540
  const program2 = new Command();
2862
- program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION4, "-v, --version").addOption(new Option("--json", "emit machine-readable JSON to stdout")).addOption(new Option("--no-input", "never prompt; error if input is required")).addOption(new Option("--quiet", "suppress non-essential diagnostics")).addOption(new Option("--verbose", "show additional diagnostic detail")).addOption(new Option("--cwd <path>", "run as if started in <path>")).addOption(
5541
+ program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION6, "-v, --version").addOption(new Option("--json", "emit machine-readable JSON to stdout")).addOption(new Option("--no-input", "never prompt; error if input is required")).addOption(new Option("--quiet", "suppress non-essential diagnostics")).addOption(new Option("--verbose", "show additional diagnostic detail")).addOption(new Option("--cwd <path>", "run as if started in <path>")).addOption(
2863
5542
  new Option("--tui", "prefer the interactive home menu for bare `noir` (advisory; TTY-only)")
2864
5543
  ).addOption(
2865
5544
  new Option(
@@ -2889,6 +5568,7 @@ function createProgram() {
2889
5568
  );
2890
5569
  }
2891
5570
  }
5571
+ applyNoirEnv2(process.cwd());
2892
5572
  const nonInteractive = opts.json === true || opts.input === false;
2893
5573
  if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
2894
5574
  else delete process.env.NOIR_NON_INTERACTIVE;
@@ -2937,8 +5617,8 @@ function createProgram() {
2937
5617
  const force = opts.force === true;
2938
5618
  const host = parseHost(opts.host);
2939
5619
  const dryRun = opts.dryRun === true || opts.preview === true;
2940
- const { create } = await import("./create-VK2QMXYS.js");
2941
- const result = await create(dir, {
5620
+ const { create: create2 } = await Promise.resolve().then(() => (init_create(), create_exports));
5621
+ const result = await create2(dir, {
2942
5622
  transport,
2943
5623
  url: opts.url,
2944
5624
  ...force ? { force } : {},
@@ -2976,13 +5656,13 @@ function createProgram() {
2976
5656
  ).choices(SUPPORTED_HOSTS2)
2977
5657
  ).action(
2978
5658
  async (opts, cmd) => {
2979
- const { sync } = await import("./sync-VXZKODQA.js");
5659
+ const { sync: sync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
2980
5660
  const host = parseHost(opts.host);
2981
5661
  const force = opts.force === true;
2982
5662
  const merge = opts.merge === true;
2983
5663
  const noMergeRegions = opts.mergeRegions === false;
2984
5664
  const dryRun = opts.dryRun === true || opts.preview === true;
2985
- const result = host === void 0 && !force && !merge && !noMergeRegions && !dryRun ? await sync(process.cwd()) : await sync(process.cwd(), {
5665
+ const result = host === void 0 && !force && !merge && !noMergeRegions && !dryRun ? await sync2(process.cwd()) : await sync2(process.cwd(), {
2986
5666
  ...host !== void 0 ? { host } : {},
2987
5667
  ...force ? { force } : {},
2988
5668
  ...merge ? { merge } : {},
@@ -3170,7 +5850,7 @@ function createProgram() {
3170
5850
  ...id === void 0 ? {} : { id }
3171
5851
  });
3172
5852
  });
3173
- taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").option("--no-artifacts", "skip the document-phase artifact writes at done").action(async (...args) => {
5853
+ taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").option("--skip", "record the landing gate as skipped (advance continues)").option("--no-artifacts", "skip the document-phase artifact writes at done").action(async (...args) => {
3174
5854
  const cmd = trailingCmd(args);
3175
5855
  const g = cmd.optsWithGlobals();
3176
5856
  const to = typeof g.to === "string" ? g.to : void 0;
@@ -3179,6 +5859,7 @@ function createProgram() {
3179
5859
  ...toCliOptions(g),
3180
5860
  ...to === void 0 ? {} : { to },
3181
5861
  ...force === void 0 ? {} : { force },
5862
+ ...g.skip === true ? { skip: true } : {},
3182
5863
  ...g.artifacts === false ? { noArtifacts: true } : {}
3183
5864
  });
3184
5865
  });
@@ -3230,17 +5911,15 @@ function createProgram() {
3230
5911
  ...typeof g.task === "string" && g.task.length > 0 ? { task: g.task } : {}
3231
5912
  });
3232
5913
  });
3233
- taskGrp.command("resume").description("resume the active (or named) in-flight/blocked task").argument("[id]", "task id (defaults to active)").option("--last", "target the active task explicitly (scripting)").option("--prompt <text>", "a continue instruction to surface in the briefing").action(async (...args) => {
5914
+ taskGrp.command("resume").description("resume the active (or named) in-flight/blocked task").argument("[id]", "task id (defaults to active)").option("--prompt <text>", "a continue instruction to surface in the briefing").action(async (...args) => {
3234
5915
  const cmd = trailingCmd(args);
3235
5916
  const g = cmd.optsWithGlobals();
3236
5917
  const first = args[0];
3237
5918
  const id = typeof first === "string" ? first : void 0;
3238
- const last = g.last === true;
3239
5919
  const prompt = typeof g.prompt === "string" ? g.prompt : void 0;
3240
5920
  await taskResume({
3241
5921
  ...toCliOptions(g),
3242
5922
  ...id === void 0 ? {} : { id },
3243
- ...last ? { last } : {},
3244
5923
  ...prompt === void 0 ? {} : { prompt }
3245
5924
  });
3246
5925
  });
@@ -3277,9 +5956,9 @@ function createProgram() {
3277
5956
  "dismiss the migration banner for the current version (persists in install.json)"
3278
5957
  ).argument("[spec]", "channel ('latest'|'beta') or exact version (default: latest)").action(
3279
5958
  async (spec, opts, cmd) => {
3280
- const { install } = await import("./install-4U4UDDNE.js");
5959
+ const { install: install2 } = await Promise.resolve().then(() => (init_install(), install_exports));
3281
5960
  const cli = cmd.optsWithGlobals();
3282
- await install({
5961
+ await install2({
3283
5962
  ...cli,
3284
5963
  spec,
3285
5964
  list: opts.list === true,
@@ -3290,9 +5969,9 @@ function createProgram() {
3290
5969
  );
3291
5970
  installCmd.alias("migrate");
3292
5971
  program2.command("update").description("update Noir to the latest version via the active install method").option("--check", "check for a new version without changing anything").argument("[spec]", "channel ('latest'|'beta') or exact version (default: latest)").action(async (spec, opts, cmd) => {
3293
- const { update } = await import("./update-ZDAVLOXB.js");
5972
+ const { update: update2 } = await Promise.resolve().then(() => (init_update(), update_exports));
3294
5973
  const cli = cmd.optsWithGlobals();
3295
- await update({ ...cli, spec, check: opts.check === true });
5974
+ await update2({ ...cli, spec, check: opts.check === true });
3296
5975
  });
3297
5976
  function buildHandoffOptions(g) {
3298
5977
  return { ...toCliOptions(g), ...g.write === true ? { write: true } : {} };
@@ -3353,17 +6032,35 @@ function createProgram() {
3353
6032
  };
3354
6033
  await runPalette(opts, dispatch);
3355
6034
  });
3356
- program2.command("run").description("ask the host agent a question and print the answer").argument("[prompt...]", "prompt to send to the host").addOption(new Option("--host <id>", "host to drive (default claude)").choices(SUPPORTED_HOSTS2)).option("--command <binary>", "custom host binary (e.g. claude-work)").action(async (...args) => {
6035
+ program2.command("run").description("ask the host agent a question and print the answer").argument("[prompt...]", "prompt to send to the host").addOption(new Option("--host <id>", "host to drive (default claude)").choices(SUPPORTED_HOSTS2)).option("--command <binary>", "custom host binary (e.g. claude-work)").option("--profile <name>", "use a named run profile (run.profiles in .noir/config.yml)").option("--list-profiles", "list configured run profiles and exit").action(async (...args) => {
3357
6036
  const cmd = trailingCmd(args);
3358
6037
  const g = cmd.optsWithGlobals();
3359
6038
  const prompt = cmd.args.join(" ").trim();
3360
6039
  const opts = {
3361
6040
  ...toCliOptions(g),
3362
6041
  ...typeof g.host === "string" ? { host: g.host } : {},
3363
- ...typeof g.command === "string" ? { command: g.command } : {}
6042
+ ...typeof g.command === "string" ? { command: g.command } : {},
6043
+ ...typeof g.profile === "string" ? { profile: g.profile } : {}
3364
6044
  };
6045
+ if (g.listProfiles === true) {
6046
+ listProfilesCommand(opts);
6047
+ return;
6048
+ }
3365
6049
  await run(prompt, opts);
3366
6050
  });
6051
+ function listProfilesCommand(opts) {
6052
+ const config = loadRunConfig(process.cwd());
6053
+ const rows = config ? listProfiles(config) : [];
6054
+ if (opts.json === true) {
6055
+ json({ ok: true, data: rows });
6056
+ return;
6057
+ }
6058
+ if (rows.length === 0) {
6059
+ info("(no run profiles configured \u2014 add a run.profiles block to .noir/config.yml)", opts);
6060
+ return;
6061
+ }
6062
+ table(rows, ["NAME", "DEFAULT", "BINARY"], opts);
6063
+ }
3367
6064
  program2.action(async (...args) => {
3368
6065
  const cmd = trailingCmd(args);
3369
6066
  const leftovers = cmd.args;
@@ -3378,21 +6075,6 @@ function createProgram() {
3378
6075
  });
3379
6076
  return program2;
3380
6077
  }
3381
- var program = createProgram();
3382
- var homeDeps = {
3383
- dispatch: async (argv) => {
3384
- const sub = createProgram();
3385
- try {
3386
- await sub.parseAsync([...argv], { from: "user" });
3387
- } catch (err) {
3388
- handleError(err);
3389
- }
3390
- },
3391
- // The grouped home menu resolves its sections against the LIVE palette
3392
- // registry (home-consolidation S1/S2) — the same source the TUI palette
3393
- // uses — so the menu cannot drift from the commander tree.
3394
- commands: buildPaletteCommands(createProgram())
3395
- };
3396
6078
  async function run2(argv = []) {
3397
6079
  try {
3398
6080
  await program.parseAsync([...argv], { from: "user" });
@@ -3401,18 +6083,56 @@ async function run2(argv = []) {
3401
6083
  }
3402
6084
  return typeof process.exitCode === "number" ? process.exitCode : EXIT.OK;
3403
6085
  }
3404
- var isMainModule = (() => {
3405
- try {
3406
- const entry = process.argv[1];
3407
- if (typeof entry !== "string" || entry.length === 0) return false;
3408
- return pathToFileURL(realpathSync(entry)).href === import.meta.url;
3409
- } catch {
3410
- return false;
6086
+ var DEPRECATIONS, program, homeDeps, isMainModule;
6087
+ var init_bin = __esm({
6088
+ "src/bin.ts"() {
6089
+ init_context();
6090
+ init_daemon();
6091
+ init_doctor();
6092
+ init_handoff();
6093
+ init_home();
6094
+ init_memory();
6095
+ init_release();
6096
+ init_run();
6097
+ init_skills();
6098
+ init_status();
6099
+ init_task();
6100
+ init_init();
6101
+ init_output();
6102
+ init_run_profiles();
6103
+ init_serve();
6104
+ init_registry();
6105
+ DEPRECATIONS = [];
6106
+ program = createProgram();
6107
+ homeDeps = {
6108
+ dispatch: async (argv) => {
6109
+ const sub = createProgram();
6110
+ try {
6111
+ await sub.parseAsync([...argv], { from: "user" });
6112
+ } catch (err) {
6113
+ handleError(err);
6114
+ }
6115
+ },
6116
+ // The grouped home menu resolves its sections against the LIVE palette
6117
+ // registry (home-consolidation S1/S2) — the same source the TUI palette
6118
+ // uses — so the menu cannot drift from the commander tree.
6119
+ commands: buildPaletteCommands(createProgram())
6120
+ };
6121
+ isMainModule = (() => {
6122
+ try {
6123
+ const entry = process.argv[1];
6124
+ if (typeof entry !== "string" || entry.length === 0) return false;
6125
+ return pathToFileURL(realpathSync(entry)).href === import.meta.url;
6126
+ } catch {
6127
+ return false;
6128
+ }
6129
+ })();
6130
+ if (isMainModule) {
6131
+ void run2(process.argv.slice(2));
6132
+ }
3411
6133
  }
3412
- })();
3413
- if (isMainModule) {
3414
- void run2(process.argv.slice(2));
3415
- }
6134
+ });
6135
+ init_bin();
3416
6136
  export {
3417
6137
  DEPRECATIONS,
3418
6138
  EXIT,