@noir-ai/cli 1.3.0-beta.6 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,28 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- doctor,
4
3
  serve
5
- } from "./chunk-3QMQXBII.js";
4
+ } from "./chunk-A5W2E3X2.js";
6
5
  import {
7
6
  init
8
- } from "./chunk-J2Y3BOVK.js";
7
+ } from "./chunk-SVFQGAOF.js";
9
8
  import {
10
9
  EXIT,
11
10
  NoirCliError,
11
+ accessibleMode,
12
+ badge,
13
+ c,
14
+ definitionList,
15
+ doctor,
12
16
  fail,
13
17
  handleError,
14
18
  inferExitCode,
15
19
  info,
16
20
  isInteractive,
21
+ json,
17
22
  log,
23
+ requireInteractive,
18
24
  table,
25
+ tip,
26
+ useColor,
19
27
  warn
20
- } from "./chunk-OF4OO5W6.js";
28
+ } from "./chunk-YES5UCU5.js";
21
29
 
22
30
  // src/bin.ts
23
31
  import { realpathSync } from "fs";
24
32
  import { pathToFileURL } from "url";
25
- import { SUPPORTED_HOSTS as SUPPORTED_HOSTS2 } from "@noir-ai/adapters";
33
+ import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
26
34
  import { NOIR_VERSION as NOIR_VERSION3 } from "@noir-ai/core";
27
35
  import { Command, Option } from "commander";
28
36
 
@@ -219,7 +227,7 @@ async function contextSearch(opts) {
219
227
  renderSearch(data, opts);
220
228
  }
221
229
  function renderSearch(data, opts) {
222
- const flag = data.degraded ? " [degraded: BM25-only]" : "";
230
+ const flag = data.degraded ? ` ${badge("warn", "degraded: BM25-only")}` : "";
223
231
  const trunc = data.truncated ? " (budget hit \u2014 results truncated)" : "";
224
232
  log(
225
233
  `context search \u2014 ${data.hits.length} hit${data.hits.length === 1 ? "" : "s"} \xB7 ${data.mode} \xB7 ${data.consumedTokens} tokens${flag}${trunc}`,
@@ -263,17 +271,16 @@ async function contextIndex(opts) {
263
271
  }
264
272
  function renderIndex(data, paths, opts) {
265
273
  const scope = paths && paths.length > 0 ? paths.join(", ") : ".";
266
- const flag = data.degraded ? " [degraded: vectors skipped]" : "";
274
+ const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
267
275
  log(`context index \u2014 ${scope}${flag}`, opts);
268
- table(
276
+ definitionList(
269
277
  [
270
- { Field: "Indexed (new)", Value: data.indexed },
271
- { Field: "Skipped (unchanged)", Value: data.skipped },
272
- { Field: "Deleted (removed)", Value: data.deleted },
273
- { Field: "Failed", Value: data.failed },
274
- { Field: "Total chunks", Value: data.totalChunks }
278
+ { label: "Indexed (new)", value: data.indexed },
279
+ { label: "Skipped (unchanged)", value: data.skipped },
280
+ { label: "Deleted (removed)", value: data.deleted },
281
+ { label: "Failed", value: data.failed },
282
+ { label: "Total chunks", value: data.totalChunks }
275
283
  ],
276
- ["Field", "Value"],
277
284
  opts
278
285
  );
279
286
  if (data.failed > 0) {
@@ -297,18 +304,17 @@ function describeEmbedder(e) {
297
304
  return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
298
305
  }
299
306
  function renderStatus(data, opts) {
300
- const flag = data.degraded ? " [degraded]" : "";
307
+ const flag = data.degraded ? ` ${badge("warn", "degraded")}` : "";
301
308
  log(`context status \u2014 ${data.projectId}${flag}`, opts);
302
- table(
309
+ definitionList(
303
310
  [
304
- { Field: "Project", Value: data.projectId },
305
- { Field: "Docs", Value: data.docCount },
306
- { Field: "Vectors", Value: data.vecCount },
307
- { Field: "Indexed files", Value: data.indexedFiles },
308
- { Field: "Embedder", Value: describeEmbedder(data.embedder) },
309
- { Field: "Degraded", Value: data.degraded ? "yes" : "no" }
311
+ { label: "Project", value: data.projectId },
312
+ { label: "Docs", value: data.docCount },
313
+ { label: "Vectors", value: data.vecCount },
314
+ { label: "Indexed files", value: data.indexedFiles },
315
+ { label: "Embedder", value: describeEmbedder(data.embedder) },
316
+ { label: "Degraded", value: data.degraded ? "yes" : "no" }
310
317
  ],
311
- ["Field", "Value"],
312
318
  opts
313
319
  );
314
320
  }
@@ -461,191 +467,676 @@ async function daemonRestart(opts) {
461
467
  await daemonStart(opts);
462
468
  }
463
469
 
464
- // src/commands/home.ts
465
- import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
466
- import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
467
-
468
- // src/banner.ts
469
- import gradient from "gradient-string";
470
- import pc from "picocolors";
471
- var NOIR_BLOCK = [
472
- "\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
473
- "\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",
474
- "\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",
475
- "\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",
476
- "\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",
477
- "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
478
- ];
479
- var NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
480
- var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
481
- function renderBanner(opts = {}) {
482
- const width = opts.width ?? process.stdout.columns ?? 80;
483
- const color = opts.color ?? true;
484
- if (width < 50) {
485
- return color ? `${pc.magenta("\u25C6")} noir` : "\u25C6 noir";
486
- }
487
- const block = NOIR_BLOCK.join("\n");
488
- return color ? NOIR_GRADIENT.multiline(block) : block;
489
- }
490
- function shouldShowBanner(opts) {
491
- if (opts.quiet === true || opts.json === true) return false;
492
- const v = process.env.NOIR_NO_BANNER;
493
- if (v !== void 0 && v !== "") return false;
494
- return true;
495
- }
470
+ // src/commands/handoff.ts
471
+ import { mkdirSync, writeFileSync } from "fs";
472
+ import { join } from "path";
473
+ import {
474
+ defaultHandoffBlock,
475
+ hostLaunchDirective,
476
+ resolveAdapter
477
+ } from "@noir-ai/adapters";
478
+ import { loadProjectInfo as loadProjectInfo4, NOIR_DIR } from "@noir-ai/core";
496
479
 
497
- // src/commands/home.ts
498
- function tryProject() {
480
+ // src/commands/status.ts
481
+ import { loadProjectInfo as loadProjectInfo3, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
482
+ async function tryTool(caller, name, args) {
499
483
  try {
500
- const info2 = loadProjectInfo3(process.cwd());
501
- return { id: info2.id, name: info2.name, host: info2.config.host };
484
+ return await caller.callTool(name, args);
502
485
  } catch {
503
486
  return null;
504
487
  }
505
488
  }
506
- var COMMANDS_HINT = "Commands: init \xB7 create \xB7 sync \xB7 status \xB7 context \xB7 memory \xB7 skills \xB7 task \xB7 daemon \xB7 doctor";
507
- function hostDirection(host) {
508
- const others = SUPPORTED_HOSTS.filter((h) => h !== host);
509
- return `\u2192 host: ${host}. Open \`${host}\` to start development \u2014 Noir set the rules, skills, and memory; ${host} runs the code. (other hosts: ${others.join(", ")})`;
510
- }
511
- async function home(opts, deps) {
512
- if (isInteractive(opts)) {
513
- await runMenu(opts, deps);
514
- return;
515
- }
516
- if (opts.json === true) {
517
- await deps.dispatch(["status", "--json"]);
518
- return;
519
- }
520
- await deps.dispatch(["status"]);
489
+ function pickStore(s) {
490
+ if (s?.ok !== true) return null;
491
+ return {
492
+ docCount: s.docCount,
493
+ vecCount: s.vecCount,
494
+ dbPath: s.dbPath,
495
+ degraded: s.degraded === true
496
+ };
521
497
  }
522
- async function runMenu(opts, deps) {
523
- const clack = await import("@clack/prompts");
524
- const project = tryProject();
525
- if (shouldShowBanner(opts)) {
526
- const host = project?.host ?? "claude";
527
- process.stderr.write(
528
- `
529
- ${renderBanner()}
530
- ${NOIR_TAGLINE}
531
-
532
- ${hostDirection(host)}
533
- ${COMMANDS_HINT}
534
-
535
- `
536
- );
537
- }
538
- clack.intro(project ? `noir \u2014 ${project.name}` : "noir");
539
- const choice = await clack.select({
540
- message: "What would you like to do?",
541
- initialValue: "status",
542
- options: [
543
- { value: "status", label: "Status", hint: "project + daemon + store snapshot" },
544
- { value: "index", label: "Index project", hint: "(re)index files into context" },
545
- { value: "recall", label: "Recall memory", hint: "search cross-session memory" },
546
- { value: "next", label: "Next task", hint: "suggest next phase + skill" },
547
- { value: "daemon", label: "Start daemon", hint: "foreground daemon" },
548
- { value: "sync", label: "Sync skills", hint: "re-emit builtin skills" },
549
- { value: "exit", label: "Exit" }
550
- ]
551
- });
552
- if (clack.isCancel(choice)) {
553
- clack.cancel("Cancelled.");
554
- fail(EXIT.CANCELLED, "cancelled", opts);
555
- }
556
- const argv = await argvForChoice(choice, clack, opts);
557
- if (argv === null) {
558
- clack.outro("bye");
559
- return;
560
- }
561
- await deps.dispatch(argv);
562
- clack.outro("done");
498
+ function describeEmbedder2(e) {
499
+ if (!e || e.kind === "none") return "none (BM25-only)";
500
+ const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
501
+ return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
563
502
  }
564
- async function argvForChoice(choice, clack, opts) {
565
- switch (choice) {
566
- case "status":
567
- return ["status"];
568
- case "index":
569
- return ["context", "index"];
570
- case "next":
571
- return ["task", "next"];
572
- case "daemon":
573
- return ["daemon", "start"];
574
- case "sync":
575
- return ["sync"];
576
- case "exit":
577
- return null;
578
- case "recall": {
579
- const query = await clack.text({
580
- message: "Recall query:",
581
- placeholder: "e.g. auth flow, ContextEngine, deploy steps"
582
- });
583
- if (clack.isCancel(query)) {
584
- clack.cancel("Cancelled.");
585
- fail(EXIT.CANCELLED, "cancelled", opts);
586
- }
587
- return ["memory", "recall", String(query)];
588
- }
589
- default:
590
- return null;
591
- }
503
+ function pickContext(c2) {
504
+ if (c2?.ok !== true) return null;
505
+ return {
506
+ docCount: c2.docCount,
507
+ vecCount: c2.vecCount,
508
+ indexedFiles: c2.indexedFiles,
509
+ embedder: describeEmbedder2(c2.embedder),
510
+ degraded: c2.degraded === true
511
+ };
592
512
  }
593
-
594
- // src/commands/memory.ts
595
- function parseLimit2(raw, label, opts) {
596
- if (raw === void 0) return void 0;
597
- const n = Number(raw);
598
- if (!Number.isInteger(n) || n <= 0) {
599
- fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
600
- }
601
- return n;
513
+ function pickWorkflow(w) {
514
+ if (w?.ok !== true) return null;
515
+ return {
516
+ taskId: w.taskId,
517
+ phase: w.phase,
518
+ state: w.state,
519
+ mode: w.mode,
520
+ nextGate: w.nextGate,
521
+ degraded: w.degraded === true
522
+ };
602
523
  }
603
- function splitCsv(raw) {
604
- if (raw === void 0) return void 0;
605
- const out = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
606
- return out.length > 0 ? out : void 0;
524
+ function pickMemory(m) {
525
+ if (m?.ok !== true || !Array.isArray(m.sessions)) return null;
526
+ let observations = 0;
527
+ for (const s of m.sessions) observations += s.count;
528
+ return { sessions: m.sessions.length, observations };
607
529
  }
608
- function toHit2(raw) {
609
- const h = raw ?? {};
610
- const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
530
+ function buildPayload(project, probe, host, store, context, workflow, memory) {
611
531
  return {
612
- id: typeof h.id === "string" ? h.id : "",
613
- type: typeof h.type === "string" ? h.type : "",
614
- score: typeof h.score === "number" ? h.score : 0,
615
- content: typeof h.content === "string" ? h.content : "",
616
- concepts: arr(h.concepts),
617
- files: arr(h.files),
618
- ts: typeof h.ts === "number" ? h.ts : 0,
619
- importance: typeof h.importance === "number" ? h.importance : 0,
620
- source: typeof h.source === "string" ? h.source : ""
532
+ noir: NOIR_VERSION2,
533
+ project: { id: project.id, name: project.name },
534
+ host: project.config.host,
535
+ daemon: {
536
+ running: probe.running,
537
+ ...probe.pid !== void 0 ? { pid: probe.pid } : {},
538
+ ...probe.uptimeSec !== void 0 ? { uptimeSec: probe.uptimeSec } : {},
539
+ ...host && typeof host.transport === "string" ? { transport: host.transport } : {}
540
+ },
541
+ store: pickStore(store),
542
+ context: pickContext(context),
543
+ workflow: pickWorkflow(workflow),
544
+ memory: pickMemory(memory)
621
545
  };
622
546
  }
623
- function failTool2(label, envelope, opts) {
624
- const detail = envelope.error ?? envelope.reason ?? "unknown failure";
625
- fail(EXIT.ERROR, `${label}: ${detail}`, opts);
547
+ function formatDuration(sec) {
548
+ if (typeof sec !== "number" || sec < 0) return "unknown";
549
+ if (sec < 60) return `${sec}s`;
550
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
551
+ return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
626
552
  }
627
- function stamp(ts) {
628
- if (typeof ts !== "number" || ts <= 0) return "-";
629
- return new Date(ts).toISOString().replace("T", " ").replace(/\..*$/, "Z");
553
+ function describeDaemon(d) {
554
+ if (!d.running) return "not running (start with `noir daemon start`)";
555
+ const pid = typeof d.pid === "number" ? `pid ${d.pid}` : "pid unknown";
556
+ const up = typeof d.uptimeSec === "number" ? `, up ${formatDuration(d.uptimeSec)}` : "";
557
+ const transport = typeof d.transport === "string" ? `, ${d.transport}` : "";
558
+ return `running (${pid}${up}${transport})`;
630
559
  }
631
- async function memoryRecall(opts) {
632
- const limit = parseLimit2(opts.limit, "memory recall", opts);
633
- const res = await callDaemonTool(opts, "memory_recall", {
634
- query: opts.query,
635
- ...limit === void 0 ? {} : { limit }
636
- });
637
- if (res.ok !== true) failTool2("memory recall", res, opts);
638
- const hits = Array.isArray(res.results) ? res.results.map(toHit2) : [];
639
- const data = { query: opts.query, hits, degraded: res.degraded === true };
560
+ function describeStore(s) {
561
+ if (!s) return "unavailable (store engine not wired)";
562
+ const flag = s.degraded ? ` ${badge("warn", "degraded: read-only")}` : "";
563
+ return `${s.docCount} docs / ${s.vecCount} vecs${flag}`;
564
+ }
565
+ function describeContext(c2) {
566
+ if (!c2) return "unavailable (context engine not wired)";
567
+ const flag = c2.degraded ? ` ${badge("warn", "degraded")}` : "";
568
+ return `${c2.docCount} docs, ${c2.vecCount} vecs, ${c2.indexedFiles} files \xB7 embedder ${c2.embedder}${flag}`;
569
+ }
570
+ function describeWorkflow(w) {
571
+ if (!w) return "no active task";
572
+ const gate = w.nextGate ? ` \u2192 next gate: ${w.nextGate}` : "";
573
+ const flag = w.degraded ? ` ${badge("warn", "degraded")}` : "";
574
+ return `${w.phase} (${w.state}, ${w.mode}) \xB7 task ${w.taskId}${gate}${flag}`;
575
+ }
576
+ function describeMemory(m) {
577
+ if (!m) return "unavailable (memory engine not wired)";
578
+ return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
579
+ }
580
+ function renderHuman(p, opts) {
581
+ log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
582
+ definitionList(
583
+ [
584
+ { label: "Project", value: `${p.project.name} (${p.project.id})` },
585
+ { label: "Host", value: p.host },
586
+ { label: "Noir", value: p.noir },
587
+ { label: "Daemon", value: describeDaemon(p.daemon) },
588
+ { label: "Store", value: describeStore(p.store) },
589
+ { label: "Context", value: describeContext(p.context) },
590
+ { label: "Workflow", value: describeWorkflow(p.workflow) },
591
+ { label: "Memory", value: describeMemory(p.memory) }
592
+ ],
593
+ opts
594
+ );
595
+ }
596
+ async function gatherStatusPayload(opts) {
597
+ const project = loadProjectInfo3(process.cwd());
598
+ const probe = await probeDaemon(opts);
599
+ let host = null;
600
+ let store = null;
601
+ let context = null;
602
+ let workflow = null;
603
+ let memory = null;
604
+ if (probe.running) {
605
+ await withRunningDaemon(
606
+ opts,
607
+ async (caller) => {
608
+ host = await tryTool(caller, "host_status");
609
+ store = await tryTool(caller, "store_status");
610
+ context = await tryTool(caller, "context_status");
611
+ workflow = await tryTool(caller, "workflow_status");
612
+ memory = await tryTool(caller, "memory_sessions");
613
+ },
614
+ probe
615
+ );
616
+ }
617
+ return buildPayload(project, probe, host, store, context, workflow, memory);
618
+ }
619
+ async function status(opts) {
620
+ const payload = await gatherStatusPayload(opts);
640
621
  if (opts.json === true) {
641
- process.stdout.write(`${JSON.stringify({ ok: true, data })}
622
+ process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
642
623
  `);
643
624
  return;
644
625
  }
645
- renderRecall(data.query, hits, data.degraded, opts);
626
+ renderHuman(payload, opts);
627
+ }
628
+
629
+ // src/commands/task.ts
630
+ var PHASE_SKILL = {
631
+ intake: "noir-intake",
632
+ clarify: "noir-clarify",
633
+ spec: "noir-spec",
634
+ plan: "noir-plan",
635
+ execute: "noir-execute",
636
+ verify: "noir-verify",
637
+ document: "noir-document"
638
+ };
639
+ function skillFor(phase) {
640
+ if (typeof phase !== "string") return null;
641
+ return PHASE_SKILL[phase] ?? null;
642
+ }
643
+ async function fetchStatus(opts, taskId) {
644
+ const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
645
+ const res = await callDaemonTool(
646
+ opts,
647
+ "workflow_status",
648
+ args
649
+ );
650
+ if (res.ok === true) return res;
651
+ const detail = res.error ?? "task not found";
652
+ fail(EXIT.NOT_FOUND, `task: ${detail}`, opts);
653
+ }
654
+ function formatStamp(ms) {
655
+ if (typeof ms !== "number" || ms <= 0) return "-";
656
+ return new Date(ms).toISOString().replace("T", " ").replace(/\..*$/, "Z");
657
+ }
658
+ function renderStatusRow(s, opts) {
659
+ const flag = s.degraded === true ? ` ${badge("warn", "degraded")}` : "";
660
+ log(`task status \u2014 ${s.taskId}${flag}`, opts);
661
+ definitionList(
662
+ [
663
+ { label: "Task", value: s.taskId },
664
+ { label: "Phase", value: s.phase },
665
+ { label: "State", value: s.state },
666
+ { label: "Mode", value: s.mode },
667
+ { label: "Next gate", value: s.nextGate ?? "\u2014" },
668
+ { label: "Updated", value: formatStamp(s.updatedAt) }
669
+ ],
670
+ opts
671
+ );
672
+ }
673
+ async function taskStatus(opts) {
674
+ const s = await fetchStatus(opts, opts.id);
675
+ if (opts.json === true) {
676
+ process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
677
+ `);
678
+ return;
679
+ }
680
+ renderStatusRow(s, opts);
681
+ }
682
+ async function taskNext(opts) {
683
+ const s = await fetchStatus(opts);
684
+ const suggestion = skillFor(s.phase);
685
+ const nextSkill = skillFor(s.nextGate);
686
+ const data = {
687
+ taskId: s.taskId,
688
+ phase: s.phase,
689
+ state: s.state,
690
+ mode: s.mode,
691
+ nextGate: s.nextGate,
692
+ suggestion,
693
+ nextSkill,
694
+ degraded: s.degraded === true
695
+ };
696
+ if (opts.json === true) {
697
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
698
+ `);
699
+ return;
700
+ }
701
+ const flag = data.degraded ? ` ${badge("warn", "degraded")}` : "";
702
+ log(`task next \u2014 ${s.taskId} \xB7 phase ${s.phase} (${s.state}, ${s.mode})${flag}`, opts);
703
+ if (s.nextGate) {
704
+ info(`next gate: ${s.nextGate}`, opts);
705
+ } else {
706
+ info("no further gate ahead (past verify, or stopped).", opts);
707
+ }
708
+ if (suggestion) info(`skill for current phase (${s.phase}): ${suggestion}`, opts);
709
+ if (nextSkill) info(`skill for next gate (${s.nextGate}): ${nextSkill}`, opts);
710
+ if (!suggestion && !nextSkill) {
711
+ info("run `noir skills list` to see applicable skills.", opts);
712
+ }
713
+ }
714
+ async function taskNew(opts) {
715
+ let mode;
716
+ if (opts.mode !== void 0) {
717
+ if (opts.mode !== "full" && opts.mode !== "quick") {
718
+ fail(EXIT.USAGE, `task new: invalid mode '${opts.mode}' (expected 'full' or 'quick')`, opts);
719
+ }
720
+ mode = opts.mode;
721
+ }
722
+ const res = await callDaemonTool(
723
+ opts,
724
+ "workflow_start",
725
+ {
726
+ taskId: opts.slug,
727
+ slug: opts.slug,
728
+ ...mode === void 0 ? {} : { mode }
729
+ }
730
+ );
731
+ if (res.ok !== true) {
732
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "start failed";
733
+ fail(EXIT.ERROR, `task new --slug ${opts.slug}: ${detail}`, opts);
734
+ }
735
+ if (opts.json === true) {
736
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
737
+ `);
738
+ return;
739
+ }
740
+ renderStatusRow(res, opts);
741
+ }
742
+ async function taskAdvance(opts) {
743
+ const args = {};
744
+ if (typeof opts.to === "string" && opts.to.length > 0) {
745
+ if (!Object.keys(PHASE_SKILL).includes(opts.to)) {
746
+ fail(EXIT.USAGE, `task advance: invalid phase '${opts.to}'`, opts);
747
+ }
748
+ args.to = opts.to;
749
+ }
750
+ if (typeof opts.force === "string" && opts.force.length > 0) {
751
+ args.force = { reason: opts.force };
752
+ }
753
+ const res = await callDaemonTool(
754
+ opts,
755
+ "workflow_advance",
756
+ args
757
+ );
758
+ if (res.ok !== true) {
759
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
760
+ fail(EXIT.ERROR, `task advance: ${detail}`, opts);
761
+ }
762
+ if (typeof opts.to === "string" && opts.to === "verify") {
763
+ tip("run `noir handoff` for a ready-to-paste host prompt", opts);
764
+ }
765
+ if (opts.json === true) {
766
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
767
+ `);
768
+ return;
769
+ }
770
+ renderStatusRow(res, opts);
771
+ }
772
+
773
+ // src/commands/handoff.ts
774
+ var SEED_LIMIT = 5;
775
+ function heading(name, id) {
776
+ return `# Noir handoff \u2014 ${name} (${id})`;
777
+ }
778
+ function renderMarkdown(p) {
779
+ const lines = [];
780
+ lines.push(heading(p.project.name, p.project.id));
781
+ lines.push("");
782
+ if (p.task) {
783
+ const gate = p.task.nextGate ?? "\u2014";
784
+ lines.push(`**Phase:** ${p.task.phase} \u2014 next gate: ${gate}`);
785
+ } else {
786
+ lines.push("**Phase:** no active task");
787
+ }
788
+ lines.push("");
789
+ lines.push("## Open host");
790
+ lines.push(p.directive);
791
+ lines.push("");
792
+ lines.push("## Next step");
793
+ if (p.task?.nextSkill) {
794
+ lines.push(`Run \`${p.task.nextSkill}\` \u2014 the host's next gate skill.`);
795
+ } else {
796
+ lines.push("No further gate ahead (past verify, or no active task).");
797
+ }
798
+ lines.push("");
799
+ lines.push("## Extracted context (seed)");
800
+ if (p.contextSeed && p.contextSeed.length > 0) {
801
+ for (const h of p.contextSeed) {
802
+ lines.push(`- \`${h.path}\` (score ${h.score.toFixed(4)})`);
803
+ const snip = h.snippet.trim();
804
+ if (snip.length > 0) lines.push(` > ${snip.replace(/\n/g, "\n > ")}`);
805
+ }
806
+ } else if (p.degraded.embedderMissing) {
807
+ lines.push(
808
+ "_Embedder unavailable \u2014 degraded to BM25-only or no hits. Start `noir daemon start` for live context._"
809
+ );
810
+ } else {
811
+ lines.push("_No context hits (or daemon down). Start `noir daemon start` for live context._");
812
+ }
813
+ lines.push("");
814
+ lines.push("## Extracted memory (seed)");
815
+ if (p.memorySeed && p.memorySeed.length > 0) {
816
+ for (const m of p.memorySeed) {
817
+ lines.push(`- \`${m.id}\` \u2014 ${m.summary}`);
818
+ }
819
+ } else {
820
+ lines.push(
821
+ "_No memory observations (or daemon down). Start `noir daemon start` for live memory._"
822
+ );
823
+ }
824
+ lines.push("");
825
+ lines.push("## Live data \u2014 call these from the host (already wired via the host MCP config)");
826
+ lines.push("- `noir.workflow_status` \xB7 `noir.context_search` \xB7 `noir.memory_recall`");
827
+ if (p.degraded.daemonDown) {
828
+ lines.push("");
829
+ lines.push("> **Note:** daemon not running \u2014 start `noir daemon start` for live task/memory.");
830
+ }
831
+ lines.push("");
832
+ return lines.join("\n");
833
+ }
834
+ function toContextHit(raw) {
835
+ const h = raw ?? {};
836
+ return {
837
+ path: typeof h.path === "string" ? h.path : "<unknown>",
838
+ score: typeof h.score === "number" ? h.score : 0,
839
+ snippet: typeof h.snippet === "string" ? h.snippet : ""
840
+ };
841
+ }
842
+ function observationSummary(raw) {
843
+ const obs = raw.observation;
844
+ if (obs && typeof obs === "object") {
845
+ const content = obs.content;
846
+ if (typeof content === "string" && content.length > 0) {
847
+ return content.length > 160 ? `${content.slice(0, 157)}\u2026` : content;
848
+ }
849
+ const type = obs.type;
850
+ if (typeof type === "string") return `(${type})`;
851
+ }
852
+ return "";
853
+ }
854
+ function toMemoryHit(raw) {
855
+ const h = raw ?? {};
856
+ return {
857
+ id: typeof h.id === "string" ? h.id : "<unknown>",
858
+ summary: observationSummary(h)
859
+ };
860
+ }
861
+ function toHostPayload(p) {
862
+ return {
863
+ project: p.project,
864
+ host: p.host,
865
+ task: p.task
866
+ };
867
+ }
868
+ function resolveDirective(root, p) {
869
+ const ctx = { root };
870
+ const adapter = resolveAdapter(p.host);
871
+ const hostPayload = toHostPayload(p);
872
+ if (typeof adapter.emitHandoff === "function") {
873
+ return adapter.emitHandoff(ctx, hostPayload);
874
+ }
875
+ return defaultHandoffBlock(ctx, hostPayload);
876
+ }
877
+ function handoffFilePath(root, taskId, projectId) {
878
+ const id = typeof taskId === "string" && taskId.length > 0 ? taskId : projectId;
879
+ return join(root, NOIR_DIR, "handoff", `${id}.md`);
880
+ }
881
+ async function extractSeeds(opts, probe, query) {
882
+ if (!probe.running) return { context: null, memory: null };
883
+ let context = null;
884
+ let memory = null;
885
+ await withRunningDaemon(
886
+ opts,
887
+ async (caller) => {
888
+ const ctxRes = await tryTool(caller, "context_search", {
889
+ query,
890
+ limit: SEED_LIMIT
891
+ });
892
+ if (ctxRes && ctxRes.ok !== false && Array.isArray(ctxRes.results)) {
893
+ context = ctxRes.results.slice(0, SEED_LIMIT).map(toContextHit).filter((h) => h.path !== "<unknown>" || h.snippet.length > 0);
894
+ }
895
+ const memRes = await tryTool(caller, "memory_recall", {
896
+ query,
897
+ limit: SEED_LIMIT
898
+ });
899
+ if (memRes && memRes.ok !== false && Array.isArray(memRes.results)) {
900
+ memory = memRes.results.slice(0, SEED_LIMIT).map(toMemoryHit).filter((h) => h.summary.length > 0);
901
+ }
902
+ },
903
+ probe
904
+ );
905
+ return { context, memory };
906
+ }
907
+ async function probeOnly(opts) {
908
+ return probeDaemon(opts);
909
+ }
910
+ async function handoff(opts) {
911
+ const project = loadProjectInfo4(process.cwd());
912
+ const host = project.config.host;
913
+ const snapshot = await gatherStatusPayload(opts);
914
+ const probe = await probeOnly(opts);
915
+ const task = snapshot.workflow ? {
916
+ taskId: snapshot.workflow.taskId,
917
+ phase: snapshot.workflow.phase,
918
+ nextGate: snapshot.workflow.nextGate,
919
+ nextSkill: skillFor(snapshot.workflow.nextGate) ?? skillFor(snapshot.workflow.phase)
920
+ } : null;
921
+ const query = task?.taskId ?? project.name;
922
+ const { context, memory } = await extractSeeds(opts, probe, query);
923
+ const full = {
924
+ project: { id: project.id, name: project.name },
925
+ host,
926
+ directive: "",
927
+ // filled after the host payload is assembled (below)
928
+ task,
929
+ contextSeed: context,
930
+ memorySeed: memory,
931
+ degraded: {
932
+ daemonDown: !probe.running,
933
+ embedderMissing: context === null && probe.running
934
+ }
935
+ };
936
+ full.directive = resolveDirective(project.root, full);
937
+ if (opts.json === true) {
938
+ process.stdout.write(`${JSON.stringify({ ok: true, data: full })}
939
+ `);
940
+ return;
941
+ }
942
+ const md = renderMarkdown(full);
943
+ if (opts.write === true) {
944
+ const path = handoffFilePath(project.root, task?.taskId, project.id);
945
+ mkdirSync(join(project.root, NOIR_DIR, "handoff"), { recursive: true });
946
+ writeFileSync(path, md, "utf8");
947
+ info(`handoff written to ${path}`, opts);
948
+ return;
949
+ }
950
+ process.stdout.write(md);
951
+ }
952
+
953
+ // src/commands/home.ts
954
+ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
955
+ import { loadProjectInfo as loadProjectInfo5 } from "@noir-ai/core";
956
+
957
+ // src/banner.ts
958
+ import gradient from "gradient-string";
959
+ var NOIR_BLOCK = [
960
+ "\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
961
+ "\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",
962
+ "\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",
963
+ "\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",
964
+ "\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",
965
+ "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
966
+ ];
967
+ var NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
968
+ var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
969
+ function renderBanner(opts = {}) {
970
+ const width = opts.width ?? process.stdout.columns ?? 80;
971
+ const colorOn = opts.color !== false && useColor();
972
+ if (width < 50) {
973
+ return colorOn ? `${c.accent("\u25C6")} noir` : "\u25C6 noir";
974
+ }
975
+ const block = NOIR_BLOCK.join("\n");
976
+ if (!colorOn) return block;
977
+ if (accessibleMode()) {
978
+ return block.split("\n").map((line) => c.accent(line)).join("\n");
979
+ }
980
+ return NOIR_GRADIENT.multiline(block);
981
+ }
982
+ function shouldShowBanner(opts) {
983
+ if (opts.quiet === true || opts.json === true) return false;
984
+ const v = process.env.NOIR_NO_BANNER;
985
+ if (v !== void 0 && v !== "") return false;
986
+ return true;
987
+ }
988
+
989
+ // src/commands/home.ts
990
+ function tryProject() {
991
+ try {
992
+ const info2 = loadProjectInfo5(process.cwd());
993
+ return { id: info2.id, name: info2.name, host: info2.config.host };
994
+ } catch {
995
+ return null;
996
+ }
997
+ }
998
+ var COMMANDS_HINT = "Commands: init \xB7 create \xB7 sync \xB7 status \xB7 context \xB7 memory \xB7 skills \xB7 task \xB7 daemon \xB7 doctor";
999
+ async function home(opts, deps) {
1000
+ if (isInteractive(opts) && opts.tui !== false) {
1001
+ await runMenu(opts, deps);
1002
+ return;
1003
+ }
1004
+ if (opts.json === true) {
1005
+ await deps.dispatch(["status", "--json"]);
1006
+ return;
1007
+ }
1008
+ await deps.dispatch(["status"]);
1009
+ }
1010
+ async function runMenu(opts, deps) {
1011
+ const clack = await import("@clack/prompts");
1012
+ const project = tryProject();
1013
+ if (shouldShowBanner(opts)) {
1014
+ const host = project?.host ?? "claude";
1015
+ process.stderr.write(
1016
+ `
1017
+ ${renderBanner()}
1018
+ ${NOIR_TAGLINE}
1019
+
1020
+ ${hostLaunchDirective2(host)}
1021
+ ${COMMANDS_HINT}
1022
+
1023
+ `
1024
+ );
1025
+ }
1026
+ clack.intro(project ? `noir \u2014 ${project.name}` : "noir");
1027
+ const choice = await clack.select({
1028
+ message: "What would you like to do?",
1029
+ initialValue: "status",
1030
+ options: [
1031
+ { value: "status", label: "Status", hint: "project + daemon + store snapshot" },
1032
+ { value: "index", label: "Index project", hint: "(re)index files into context" },
1033
+ { value: "recall", label: "Recall memory", hint: "search cross-session memory" },
1034
+ { value: "next", label: "Next task", hint: "suggest next phase + skill" },
1035
+ { value: "handoff", label: "Handoff", hint: "ready-to-paste host prompt" },
1036
+ { value: "daemon", label: "Start daemon", hint: "foreground daemon" },
1037
+ { value: "sync", label: "Sync skills", hint: "re-emit builtin skills" },
1038
+ { value: "exit", label: "Exit" }
1039
+ ]
1040
+ });
1041
+ if (clack.isCancel(choice)) {
1042
+ clack.cancel("Cancelled.");
1043
+ fail(EXIT.CANCELLED, "cancelled", opts);
1044
+ }
1045
+ const argv = await argvForChoice(choice, clack, opts);
1046
+ if (argv === null) {
1047
+ clack.outro("bye");
1048
+ return;
1049
+ }
1050
+ await deps.dispatch(argv);
1051
+ clack.outro("done");
1052
+ }
1053
+ async function argvForChoice(choice, clack, opts) {
1054
+ switch (choice) {
1055
+ case "status":
1056
+ return ["status"];
1057
+ case "index":
1058
+ return ["context", "index"];
1059
+ case "next":
1060
+ return ["task", "next"];
1061
+ case "handoff":
1062
+ return ["handoff"];
1063
+ case "daemon":
1064
+ return ["daemon", "start"];
1065
+ case "sync":
1066
+ return ["sync"];
1067
+ case "exit":
1068
+ return null;
1069
+ case "recall": {
1070
+ const query = await clack.text({
1071
+ message: "Recall query:",
1072
+ placeholder: "e.g. auth flow, ContextEngine, deploy steps"
1073
+ });
1074
+ if (clack.isCancel(query)) {
1075
+ clack.cancel("Cancelled.");
1076
+ fail(EXIT.CANCELLED, "cancelled", opts);
1077
+ }
1078
+ return ["memory", "recall", String(query)];
1079
+ }
1080
+ default:
1081
+ return null;
1082
+ }
1083
+ }
1084
+
1085
+ // src/commands/memory.ts
1086
+ function parseLimit2(raw, label, opts) {
1087
+ if (raw === void 0) return void 0;
1088
+ const n = Number(raw);
1089
+ if (!Number.isInteger(n) || n <= 0) {
1090
+ fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
1091
+ }
1092
+ return n;
1093
+ }
1094
+ function splitCsv(raw) {
1095
+ if (raw === void 0) return void 0;
1096
+ const out = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1097
+ return out.length > 0 ? out : void 0;
1098
+ }
1099
+ function toHit2(raw) {
1100
+ const h = raw ?? {};
1101
+ const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
1102
+ return {
1103
+ id: typeof h.id === "string" ? h.id : "",
1104
+ type: typeof h.type === "string" ? h.type : "",
1105
+ score: typeof h.score === "number" ? h.score : 0,
1106
+ content: typeof h.content === "string" ? h.content : "",
1107
+ concepts: arr(h.concepts),
1108
+ files: arr(h.files),
1109
+ ts: typeof h.ts === "number" ? h.ts : 0,
1110
+ importance: typeof h.importance === "number" ? h.importance : 0,
1111
+ source: typeof h.source === "string" ? h.source : ""
1112
+ };
1113
+ }
1114
+ function failTool2(label, envelope, opts) {
1115
+ const detail = envelope.error ?? envelope.reason ?? "unknown failure";
1116
+ fail(EXIT.ERROR, `${label}: ${detail}`, opts);
1117
+ }
1118
+ function stamp(ts) {
1119
+ if (typeof ts !== "number" || ts <= 0) return "-";
1120
+ return new Date(ts).toISOString().replace("T", " ").replace(/\..*$/, "Z");
1121
+ }
1122
+ async function memoryRecall(opts) {
1123
+ const limit = parseLimit2(opts.limit, "memory recall", opts);
1124
+ const res = await callDaemonTool(opts, "memory_recall", {
1125
+ query: opts.query,
1126
+ ...limit === void 0 ? {} : { limit }
1127
+ });
1128
+ if (res.ok !== true) failTool2("memory recall", res, opts);
1129
+ const hits = Array.isArray(res.results) ? res.results.map(toHit2) : [];
1130
+ const data = { query: opts.query, hits, degraded: res.degraded === true };
1131
+ if (opts.json === true) {
1132
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
1133
+ `);
1134
+ return;
1135
+ }
1136
+ renderRecall(data.query, hits, data.degraded, opts);
646
1137
  }
647
1138
  function renderRecall(query, hits, degraded, opts) {
648
- const flag = degraded ? " [degraded: BM25-only]" : "";
1139
+ const flag = degraded ? ` ${badge("warn", "degraded: BM25-only")}` : "";
649
1140
  log(
650
1141
  `memory recall \u2014 ${hits.length} hit${hits.length === 1 ? "" : "s"} for '${query}'${flag}`,
651
1142
  opts
@@ -707,9 +1198,9 @@ async function resolveContent(opts) {
707
1198
  function renderObservation(obs, opts) {
708
1199
  const rows = [];
709
1200
  for (const key of ["id", "type", "importance", "ts", "source", "project", "sessionId"]) {
710
- if (obs[key] !== void 0) rows.push({ Field: key, Value: obs[key] });
1201
+ if (obs[key] !== void 0) rows.push({ label: key, value: obs[key] });
711
1202
  }
712
- if (rows.length > 0) table(rows, ["Field", "Value"], opts);
1203
+ if (rows.length > 0) definitionList(rows, opts);
713
1204
  const content = obs.content;
714
1205
  if (typeof content === "string") log(`
715
1206
  ${content}`, opts);
@@ -806,7 +1297,7 @@ async function memoryConsolidate(opts) {
806
1297
 
807
1298
  // src/commands/skills.ts
808
1299
  import { claudeAdapter } from "@noir-ai/adapters";
809
- import { loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
1300
+ import { loadProjectInfo as loadProjectInfo6 } from "@noir-ai/core";
810
1301
  import {
811
1302
  discoverAll,
812
1303
  emitSkillsToDir
@@ -835,400 +1326,111 @@ var CATEGORY = {
835
1326
  "noir-worktree": "git",
836
1327
  "noir-recall": "memory",
837
1328
  "noir-remember": "memory",
838
- "noir-checkpoint": "memory",
839
- "noir-context": "context",
840
- "noir-frontend": "domain",
841
- "noir-backend": "domain",
842
- "noir-doctor": "meta",
843
- "noir-skill-author": "meta",
844
- "noir-sync": "meta",
845
- "noir-wrap": "meta"
846
- };
847
- function categoryOf(name) {
848
- const mapped = CATEGORY[name];
849
- if (mapped !== void 0) return mapped;
850
- return name.replace(/^noir-/, "") || "general";
851
- }
852
- function toRow(s, kind = "builtin") {
853
- const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
854
- return { name: s.name, category: categoryOf(s.name), description, kind };
855
- }
856
- function truncate(text, max) {
857
- if (text.length <= max) return text;
858
- const slice = text.slice(0, Math.max(0, max - 1));
859
- const boundary = slice.lastIndexOf(" ");
860
- const head = boundary > 0 ? slice.slice(0, boundary) : slice;
861
- return `${head.trimEnd()}\u2026`;
862
- }
863
- async function skillsList(opts) {
864
- let builtins;
865
- let integrations;
866
- try {
867
- const all = discoverAll();
868
- builtins = all.builtins;
869
- integrations = all.integrations;
870
- } catch (err) {
871
- fail(
872
- EXIT.ERROR,
873
- `skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
874
- opts
875
- );
876
- }
877
- const rows = [
878
- ...builtins.map((b) => toRow(b, "builtin")),
879
- ...integrations.map((i) => toRow(i, "integration"))
880
- ];
881
- const data = { count: rows.length, skills: rows };
882
- if (opts.json === true) {
883
- process.stdout.write(`${JSON.stringify({ ok: true, data })}
884
- `);
885
- return;
886
- }
887
- const intCount = integrations.length;
888
- const banner = intCount > 0 ? `noir skills \u2014 ${rows.length} skill${rows.length === 1 ? "" : "s"} (${builtins.length} builtin, ${intCount} integration${intCount === 1 ? "" : "s"})` : `noir skills \u2014 ${rows.length} builtin skill${rows.length === 1 ? "" : "s"}`;
889
- log(banner, opts);
890
- table(
891
- rows.map((r) => ({
892
- Skill: r.name,
893
- Kind: r.kind,
894
- Category: r.category,
895
- Description: truncate(r.description, 80)
896
- })),
897
- ["Skill", "Kind", "Category", "Description"],
898
- opts
899
- );
900
- }
901
- async function skillsSync(opts) {
902
- const root = process.cwd();
903
- const project = loadProjectInfo4(root);
904
- if (!claudeAdapter.skillsDir) {
905
- const data2 = {
906
- emitted: [],
907
- references: 0,
908
- host: project.config.host,
909
- reason: "no-skill-emitter"
910
- };
911
- if (opts.json === true) {
912
- process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
913
- `);
914
- return;
915
- }
916
- info("This host has no skill emitter; nothing to sync.", opts);
917
- return;
918
- }
919
- const dir = claudeAdapter.skillsDir({ root });
920
- const summary = await emitSkillsToDir(dir);
921
- const pruned = summary.pruned ?? [];
922
- const data = {
923
- emitted: summary.emitted,
924
- references: summary.references,
925
- dir,
926
- pruned
927
- };
928
- if (opts.json === true) {
929
- process.stdout.write(`${JSON.stringify({ ok: true, data })}
930
- `);
931
- return;
932
- }
933
- log(
934
- `Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
935
- opts
936
- );
937
- if (pruned.length > 0) {
938
- warn(
939
- `Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
940
- opts
941
- );
942
- }
943
- }
944
-
945
- // src/commands/status.ts
946
- import { loadProjectInfo as loadProjectInfo5, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
947
- async function tryTool(caller, name) {
948
- try {
949
- return await caller.callTool(name);
950
- } catch {
951
- return null;
952
- }
953
- }
954
- function pickStore(s) {
955
- if (s?.ok !== true) return null;
956
- return {
957
- docCount: s.docCount,
958
- vecCount: s.vecCount,
959
- dbPath: s.dbPath,
960
- degraded: s.degraded === true
961
- };
962
- }
963
- function describeEmbedder2(e) {
964
- if (!e || e.kind === "none") return "none (BM25-only)";
965
- const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
966
- return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
967
- }
968
- function pickContext(c) {
969
- if (c?.ok !== true) return null;
970
- return {
971
- docCount: c.docCount,
972
- vecCount: c.vecCount,
973
- indexedFiles: c.indexedFiles,
974
- embedder: describeEmbedder2(c.embedder),
975
- degraded: c.degraded === true
976
- };
977
- }
978
- function pickWorkflow(w) {
979
- if (w?.ok !== true) return null;
980
- return {
981
- taskId: w.taskId,
982
- phase: w.phase,
983
- state: w.state,
984
- mode: w.mode,
985
- nextGate: w.nextGate,
986
- degraded: w.degraded === true
987
- };
988
- }
989
- function pickMemory(m) {
990
- if (m?.ok !== true || !Array.isArray(m.sessions)) return null;
991
- let observations = 0;
992
- for (const s of m.sessions) observations += s.count;
993
- return { sessions: m.sessions.length, observations };
994
- }
995
- function buildPayload(project, probe, host, store, context, workflow, memory) {
996
- return {
997
- noir: NOIR_VERSION2,
998
- project: { id: project.id, name: project.name },
999
- host: project.config.host,
1000
- daemon: {
1001
- running: probe.running,
1002
- ...probe.pid !== void 0 ? { pid: probe.pid } : {},
1003
- ...probe.uptimeSec !== void 0 ? { uptimeSec: probe.uptimeSec } : {},
1004
- ...host && typeof host.transport === "string" ? { transport: host.transport } : {}
1005
- },
1006
- store: pickStore(store),
1007
- context: pickContext(context),
1008
- workflow: pickWorkflow(workflow),
1009
- memory: pickMemory(memory)
1010
- };
1011
- }
1012
- function formatDuration(sec) {
1013
- if (typeof sec !== "number" || sec < 0) return "unknown";
1014
- if (sec < 60) return `${sec}s`;
1015
- if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
1016
- return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
1017
- }
1018
- function describeDaemon(d) {
1019
- if (!d.running) return "not running (start with `noir daemon start`)";
1020
- const pid = typeof d.pid === "number" ? `pid ${d.pid}` : "pid unknown";
1021
- const up = typeof d.uptimeSec === "number" ? `, up ${formatDuration(d.uptimeSec)}` : "";
1022
- const transport = typeof d.transport === "string" ? `, ${d.transport}` : "";
1023
- return `running (${pid}${up}${transport})`;
1024
- }
1025
- function describeStore(s) {
1026
- if (!s) return "unavailable (store engine not wired)";
1027
- const flag = s.degraded ? " [degraded: read-only]" : "";
1028
- return `${s.docCount} docs / ${s.vecCount} vecs${flag}`;
1029
- }
1030
- function describeContext(c) {
1031
- if (!c) return "unavailable (context engine not wired)";
1032
- const flag = c.degraded ? " [degraded]" : "";
1033
- return `${c.docCount} docs, ${c.vecCount} vecs, ${c.indexedFiles} files \xB7 embedder ${c.embedder}${flag}`;
1034
- }
1035
- function describeWorkflow(w) {
1036
- if (!w) return "no active task";
1037
- const gate = w.nextGate ? ` \u2192 next gate: ${w.nextGate}` : "";
1038
- const flag = w.degraded ? " [degraded]" : "";
1039
- return `${w.phase} (${w.state}, ${w.mode}) \xB7 task ${w.taskId}${gate}${flag}`;
1329
+ "noir-checkpoint": "memory",
1330
+ "noir-context": "context",
1331
+ "noir-frontend": "domain",
1332
+ "noir-backend": "domain",
1333
+ "noir-doctor": "meta",
1334
+ "noir-skill-author": "meta",
1335
+ "noir-sync": "meta",
1336
+ "noir-wrap": "meta"
1337
+ };
1338
+ function categoryOf(name) {
1339
+ const mapped = CATEGORY[name];
1340
+ if (mapped !== void 0) return mapped;
1341
+ return name.replace(/^noir-/, "") || "general";
1040
1342
  }
1041
- function describeMemory(m) {
1042
- if (!m) return "unavailable (memory engine not wired)";
1043
- return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
1343
+ function toRow(s, kind = "builtin") {
1344
+ const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
1345
+ return { name: s.name, category: categoryOf(s.name), description, kind };
1044
1346
  }
1045
- function renderHuman(p, opts) {
1046
- log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
1047
- table(
1048
- [
1049
- { Field: "Project", Value: `${p.project.name} (${p.project.id})` },
1050
- { Field: "Host", Value: p.host },
1051
- { Field: "Noir", Value: p.noir },
1052
- { Field: "Daemon", Value: describeDaemon(p.daemon) },
1053
- { Field: "Store", Value: describeStore(p.store) },
1054
- { Field: "Context", Value: describeContext(p.context) },
1055
- { Field: "Workflow", Value: describeWorkflow(p.workflow) },
1056
- { Field: "Memory", Value: describeMemory(p.memory) }
1057
- ],
1058
- ["Field", "Value"],
1059
- opts
1060
- );
1347
+ function truncate(text, max) {
1348
+ if (text.length <= max) return text;
1349
+ const slice = text.slice(0, Math.max(0, max - 1));
1350
+ const boundary = slice.lastIndexOf(" ");
1351
+ const head = boundary > 0 ? slice.slice(0, boundary) : slice;
1352
+ return `${head.trimEnd()}\u2026`;
1061
1353
  }
1062
- async function status(opts) {
1063
- const project = loadProjectInfo5(process.cwd());
1064
- const probe = await probeDaemon(opts);
1065
- let host = null;
1066
- let store = null;
1067
- let context = null;
1068
- let workflow = null;
1069
- let memory = null;
1070
- if (probe.running) {
1071
- await withRunningDaemon(
1072
- opts,
1073
- async (caller) => {
1074
- host = await tryTool(caller, "host_status");
1075
- store = await tryTool(caller, "store_status");
1076
- context = await tryTool(caller, "context_status");
1077
- workflow = await tryTool(caller, "workflow_status");
1078
- memory = await tryTool(caller, "memory_sessions");
1079
- },
1080
- probe
1354
+ async function skillsList(opts) {
1355
+ let builtins;
1356
+ let integrations;
1357
+ try {
1358
+ const all = discoverAll();
1359
+ builtins = all.builtins;
1360
+ integrations = all.integrations;
1361
+ } catch (err) {
1362
+ fail(
1363
+ EXIT.ERROR,
1364
+ `skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
1365
+ opts
1081
1366
  );
1082
1367
  }
1083
- const payload = buildPayload(project, probe, host, store, context, workflow, memory);
1368
+ const rows = [
1369
+ ...builtins.map((b) => toRow(b, "builtin")),
1370
+ ...integrations.map((i) => toRow(i, "integration"))
1371
+ ];
1372
+ const data = { count: rows.length, skills: rows };
1084
1373
  if (opts.json === true) {
1085
- process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
1374
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
1086
1375
  `);
1087
1376
  return;
1088
1377
  }
1089
- renderHuman(payload, opts);
1090
- }
1091
-
1092
- // src/commands/task.ts
1093
- var PHASE_SKILL = {
1094
- intake: "noir-intake",
1095
- clarify: "noir-clarify",
1096
- spec: "noir-spec",
1097
- plan: "noir-plan",
1098
- execute: "noir-execute",
1099
- verify: "noir-verify",
1100
- document: "noir-document"
1101
- };
1102
- function skillFor(phase) {
1103
- if (typeof phase !== "string") return null;
1104
- return PHASE_SKILL[phase] ?? null;
1105
- }
1106
- async function fetchStatus(opts, taskId) {
1107
- const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
1108
- const res = await callDaemonTool(
1109
- opts,
1110
- "workflow_status",
1111
- args
1112
- );
1113
- if (res.ok === true) return res;
1114
- const detail = res.error ?? "task not found";
1115
- fail(EXIT.NOT_FOUND, `task: ${detail}`, opts);
1116
- }
1117
- function formatStamp(ms) {
1118
- if (typeof ms !== "number" || ms <= 0) return "-";
1119
- return new Date(ms).toISOString().replace("T", " ").replace(/\..*$/, "Z");
1120
- }
1121
- function renderStatusRow(s, opts) {
1122
- const flag = s.degraded === true ? " [degraded]" : "";
1123
- log(`task status \u2014 ${s.taskId}${flag}`, opts);
1378
+ const intCount = integrations.length;
1379
+ const banner = intCount > 0 ? `noir skills \u2014 ${rows.length} skill${rows.length === 1 ? "" : "s"} (${builtins.length} builtin, ${intCount} integration${intCount === 1 ? "" : "s"})` : `noir skills \u2014 ${rows.length} builtin skill${rows.length === 1 ? "" : "s"}`;
1380
+ log(banner, opts);
1124
1381
  table(
1125
- [
1126
- { Field: "Task", Value: s.taskId },
1127
- { Field: "Phase", Value: s.phase },
1128
- { Field: "State", Value: s.state },
1129
- { Field: "Mode", Value: s.mode },
1130
- { Field: "Next gate", Value: s.nextGate ?? "\u2014" },
1131
- { Field: "Updated", Value: formatStamp(s.updatedAt) }
1132
- ],
1133
- ["Field", "Value"],
1382
+ rows.map((r) => ({
1383
+ Skill: r.name,
1384
+ Kind: r.kind,
1385
+ Category: r.category,
1386
+ Description: truncate(r.description, 80)
1387
+ })),
1388
+ ["Skill", "Kind", "Category", "Description"],
1134
1389
  opts
1135
1390
  );
1136
1391
  }
1137
- async function taskStatus(opts) {
1138
- const s = await fetchStatus(opts, opts.id);
1139
- if (opts.json === true) {
1140
- process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
1392
+ async function skillsSync(opts) {
1393
+ const root = process.cwd();
1394
+ const project = loadProjectInfo6(root);
1395
+ if (!claudeAdapter.skillsDir) {
1396
+ const data2 = {
1397
+ emitted: [],
1398
+ references: 0,
1399
+ host: project.config.host,
1400
+ reason: "no-skill-emitter"
1401
+ };
1402
+ if (opts.json === true) {
1403
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
1141
1404
  `);
1405
+ return;
1406
+ }
1407
+ info("This host has no skill emitter; nothing to sync.", opts);
1142
1408
  return;
1143
1409
  }
1144
- renderStatusRow(s, opts);
1145
- }
1146
- async function taskNext(opts) {
1147
- const s = await fetchStatus(opts);
1148
- const suggestion = skillFor(s.phase);
1149
- const nextSkill = skillFor(s.nextGate);
1410
+ const dir = claudeAdapter.skillsDir({ root });
1411
+ const summary = await emitSkillsToDir(dir);
1412
+ const pruned = summary.pruned ?? [];
1150
1413
  const data = {
1151
- taskId: s.taskId,
1152
- phase: s.phase,
1153
- state: s.state,
1154
- mode: s.mode,
1155
- nextGate: s.nextGate,
1156
- suggestion,
1157
- nextSkill,
1158
- degraded: s.degraded === true
1414
+ emitted: summary.emitted,
1415
+ references: summary.references,
1416
+ dir,
1417
+ pruned
1159
1418
  };
1160
1419
  if (opts.json === true) {
1161
1420
  process.stdout.write(`${JSON.stringify({ ok: true, data })}
1162
1421
  `);
1163
1422
  return;
1164
1423
  }
1165
- const flag = data.degraded ? " [degraded]" : "";
1166
- log(`task next \u2014 ${s.taskId} \xB7 phase ${s.phase} (${s.state}, ${s.mode})${flag}`, opts);
1167
- if (s.nextGate) {
1168
- info(`next gate: ${s.nextGate}`, opts);
1169
- } else {
1170
- info("no further gate ahead (past verify, or stopped).", opts);
1171
- }
1172
- if (suggestion) info(`skill for current phase (${s.phase}): ${suggestion}`, opts);
1173
- if (nextSkill) info(`skill for next gate (${s.nextGate}): ${nextSkill}`, opts);
1174
- if (!suggestion && !nextSkill) {
1175
- info("run `noir skills list` to see applicable skills.", opts);
1176
- }
1177
- }
1178
- async function taskNew(opts) {
1179
- let mode;
1180
- if (opts.mode !== void 0) {
1181
- if (opts.mode !== "full" && opts.mode !== "quick") {
1182
- fail(EXIT.USAGE, `task new: invalid mode '${opts.mode}' (expected 'full' or 'quick')`, opts);
1183
- }
1184
- mode = opts.mode;
1185
- }
1186
- const res = await callDaemonTool(
1187
- opts,
1188
- "workflow_start",
1189
- {
1190
- taskId: opts.slug,
1191
- slug: opts.slug,
1192
- ...mode === void 0 ? {} : { mode }
1193
- }
1194
- );
1195
- if (res.ok !== true) {
1196
- const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "start failed";
1197
- fail(EXIT.ERROR, `task new --slug ${opts.slug}: ${detail}`, opts);
1198
- }
1199
- if (opts.json === true) {
1200
- process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1201
- `);
1202
- return;
1203
- }
1204
- renderStatusRow(res, opts);
1205
- }
1206
- async function taskAdvance(opts) {
1207
- const args = {};
1208
- if (typeof opts.to === "string" && opts.to.length > 0) {
1209
- if (!Object.keys(PHASE_SKILL).includes(opts.to)) {
1210
- fail(EXIT.USAGE, `task advance: invalid phase '${opts.to}'`, opts);
1211
- }
1212
- args.to = opts.to;
1213
- }
1214
- if (typeof opts.force === "string" && opts.force.length > 0) {
1215
- args.force = { reason: opts.force };
1216
- }
1217
- const res = await callDaemonTool(
1218
- opts,
1219
- "workflow_advance",
1220
- args
1424
+ log(
1425
+ `Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
1426
+ opts
1221
1427
  );
1222
- if (res.ok !== true) {
1223
- const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
1224
- fail(EXIT.ERROR, `task advance: ${detail}`, opts);
1225
- }
1226
- if (opts.json === true) {
1227
- process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1228
- `);
1229
- return;
1428
+ if (pruned.length > 0) {
1429
+ warn(
1430
+ `Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
1431
+ opts
1432
+ );
1230
1433
  }
1231
- renderStatusRow(res, opts);
1232
1434
  }
1233
1435
 
1234
1436
  // src/bin.ts
@@ -1251,7 +1453,15 @@ function toCliOptions(g) {
1251
1453
  json: g.json === true,
1252
1454
  quiet: g.quiet === true,
1253
1455
  verbose: g.verbose === true,
1254
- input: g.input !== false
1456
+ input: g.input !== false,
1457
+ // TUI policy — additive only. `--tui`/`--no-tui` land on `tui`
1458
+ // (true/false; absent when neither flag is given). Spread conditionally so
1459
+ // the default-args case (no flag) does NOT add a `tui` key — keeps the
1460
+ // exact-shape assertions in bin.test.ts green.
1461
+ ...g.tui === true || g.tui === false ? { tui: g.tui === true } : {},
1462
+ // `--no-tips` is stored under `tips` (default true; flag ⇒ false). Map to
1463
+ // the additive `noTips` spelling used by CliOptions + `tip()`.
1464
+ ...g.tips === false ? { noTips: true } : {}
1255
1465
  };
1256
1466
  }
1257
1467
  function toStatusOptions(g) {
@@ -1266,9 +1476,38 @@ function parseHost(raw) {
1266
1476
  if (raw === void 0) return void 0;
1267
1477
  return raw;
1268
1478
  }
1479
+ var DEPRECATIONS = [];
1480
+ function commandPath(cmd) {
1481
+ const path = [];
1482
+ let c2 = cmd;
1483
+ while (c2 !== null) {
1484
+ path.unshift(c2.name());
1485
+ c2 = c2.parent;
1486
+ }
1487
+ return path;
1488
+ }
1489
+ function emitDeprecationHintsFor(cmd, opts) {
1490
+ if (DEPRECATIONS.length === 0) return;
1491
+ const rel = commandPath(cmd).slice(1);
1492
+ for (const d of DEPRECATIONS) {
1493
+ if (d.oldArgv.every((a, i) => rel[i] === a)) {
1494
+ tip(
1495
+ `\`noir ${d.oldArgv.join(" ")}\` is deprecated since v${d.since}; use \`noir ${d.newArgv.join(" ")}\`.`,
1496
+ opts
1497
+ );
1498
+ }
1499
+ }
1500
+ }
1269
1501
  function createProgram() {
1270
1502
  const program2 = new Command();
1271
- program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION3, "-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>")).exitOverride((err) => {
1503
+ program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION3, "-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(
1504
+ new Option("--tui", "prefer the interactive home menu for bare `noir` (advisory; TTY-only)")
1505
+ ).addOption(
1506
+ new Option(
1507
+ "--no-tui",
1508
+ "route bare `noir` to the non-interactive `status` path even in a TTY"
1509
+ )
1510
+ ).addOption(new Option("--no-tips", "suppress redirect / deprecation hints on stderr")).exitOverride((err) => {
1272
1511
  throw err;
1273
1512
  }).configureOutput({
1274
1513
  // Stream discipline: --help/--version → stdout (conventional; users pipe
@@ -1294,6 +1533,7 @@ function createProgram() {
1294
1533
  const nonInteractive = opts.json === true || opts.input === false;
1295
1534
  if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
1296
1535
  else delete process.env.NOIR_NON_INTERACTIVE;
1536
+ emitDeprecationHintsFor(actionCmd, toCliOptions(opts));
1297
1537
  });
1298
1538
  program2.command("init").description("scaffold Noir in the current project (.noir/, .mcp.json, CLAUDE.md, skills)").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").option(
1299
1539
  "--upgrade",
@@ -1306,39 +1546,45 @@ function createProgram() {
1306
1546
  new Option(
1307
1547
  "--host <id>",
1308
1548
  "target agentic CLI (default: claude) \u2014 drives host-side emission"
1309
- ).choices(SUPPORTED_HOSTS2)
1549
+ ).choices(SUPPORTED_HOSTS)
1310
1550
  ).action(
1311
- async (opts) => {
1551
+ async (opts, cmd) => {
1312
1552
  const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
1313
1553
  const upgrade = opts.upgrade === true;
1314
1554
  const force = opts.force === true;
1315
1555
  const host = parseHost(opts.host);
1316
- await init(process.cwd(), {
1556
+ const result = await init(process.cwd(), {
1317
1557
  transport,
1318
1558
  url: opts.url,
1319
1559
  ...upgrade ? { upgrade } : {},
1320
1560
  ...force ? { force } : {},
1321
1561
  ...host !== void 0 ? { host } : {}
1322
1562
  });
1563
+ if (cmd.optsWithGlobals().json === true && result !== void 0) {
1564
+ json({ ok: true, data: result });
1565
+ }
1323
1566
  }
1324
1567
  );
1325
1568
  program2.command("create [dir]").description("bootstrap the Noir AI layer in a new or empty directory").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").addOption(
1326
1569
  new Option(
1327
1570
  "--host <id>",
1328
1571
  "target agentic CLI (default: claude) \u2014 drives host-side emission"
1329
- ).choices(SUPPORTED_HOSTS2)
1572
+ ).choices(SUPPORTED_HOSTS)
1330
1573
  ).action(
1331
- async (dir, opts) => {
1574
+ async (dir, opts, cmd) => {
1332
1575
  const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
1333
1576
  const force = opts.force === true;
1334
1577
  const host = parseHost(opts.host);
1335
- const { create } = await import("./create-VK7JX6IB.js");
1336
- await create(dir, {
1578
+ const { create } = await import("./create-7ERBL42K.js");
1579
+ const result = await create(dir, {
1337
1580
  transport,
1338
1581
  url: opts.url,
1339
1582
  ...force ? { force } : {},
1340
1583
  ...host !== void 0 ? { host } : {}
1341
1584
  });
1585
+ if (cmd.optsWithGlobals().json === true && result !== void 0) {
1586
+ json({ ok: true, data: result });
1587
+ }
1342
1588
  }
1343
1589
  );
1344
1590
  program2.command("sync").description(
@@ -1348,7 +1594,15 @@ function createProgram() {
1348
1594
  "overwrite differing regenerated files without prompting (bypasses the conflict menu)"
1349
1595
  ).option(
1350
1596
  "--merge",
1351
- "three-way merge managed regions (preserve hand-edits inside <!-- noir:* --> markers)"
1597
+ "three-way merge managed regions (default since 1.3.0; flag kept for compatibility)"
1598
+ ).addOption(
1599
+ // Opt OUT of managed-region merge (restore strip-replace). Commander
1600
+ // stores `--no-merge-regions` under `mergeRegions` (default true; flag →
1601
+ // false). Bare `noir sync` keeps the merge default (true).
1602
+ new Option(
1603
+ "--no-merge-regions",
1604
+ "strip-replace managed regions (discard hand-edits inside <!-- noir:* --> markers)"
1605
+ )
1352
1606
  ).addOption(
1353
1607
  // S10: optional `--host` override. When omitted, sync reads host from
1354
1608
  // `.noir/config.yml` (whatever `noir init --host <id>` persisted). The
@@ -1356,22 +1610,25 @@ function createProgram() {
1356
1610
  new Option(
1357
1611
  "--host <id>",
1358
1612
  "override the configured host (advanced; default reads .noir/config.yml)"
1359
- ).choices(SUPPORTED_HOSTS2)
1360
- ).action(async (opts) => {
1361
- const { sync } = await import("./sync-7XFGCRJN.js");
1362
- const host = parseHost(opts.host);
1363
- const force = opts.force === true;
1364
- const merge = opts.merge === true;
1365
- if (host === void 0 && !force && !merge) {
1366
- await sync(process.cwd());
1367
- } else {
1368
- await sync(process.cwd(), {
1613
+ ).choices(SUPPORTED_HOSTS)
1614
+ ).action(
1615
+ async (opts, cmd) => {
1616
+ const { sync } = await import("./sync-SCAF6Z5H.js");
1617
+ const host = parseHost(opts.host);
1618
+ const force = opts.force === true;
1619
+ const merge = opts.merge === true;
1620
+ const noMergeRegions = opts.mergeRegions === false;
1621
+ const result = host === void 0 && !force && !merge && !noMergeRegions ? await sync(process.cwd()) : await sync(process.cwd(), {
1369
1622
  ...host !== void 0 ? { host } : {},
1370
1623
  ...force ? { force } : {},
1371
- ...merge ? { merge } : {}
1624
+ ...merge ? { merge } : {},
1625
+ ...noMergeRegions ? { mergeManagedRegions: false } : {}
1372
1626
  });
1627
+ if (cmd.optsWithGlobals().json === true) {
1628
+ json({ ok: true, data: result });
1629
+ }
1373
1630
  }
1374
- });
1631
+ );
1375
1632
  const mcpCmd = program2.command("mcp").description("MCP server control");
1376
1633
  mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").action(async (opts) => {
1377
1634
  await serve({ stdio: opts.stdio === true });
@@ -1443,7 +1700,7 @@ function createProgram() {
1443
1700
  contextGrp.action(() => {
1444
1701
  throw new NoirCliError(EXIT.USAGE, "Usage: noir context search|index|status");
1445
1702
  });
1446
- const memoryGrp = program2.command("memory").description("memory engine (S7)");
1703
+ const memoryGrp = program2.command("memory").description("memory engine");
1447
1704
  memoryGrp.command("recall").description("recall memories for a query").argument("<query>", "recall query").option("--limit <n>", "max results", "10").action(async (...args) => {
1448
1705
  const cmd = trailingCmd(args);
1449
1706
  const g = cmd.optsWithGlobals();
@@ -1508,7 +1765,7 @@ function createProgram() {
1508
1765
  skillsGrp.action(() => {
1509
1766
  throw new NoirCliError(EXIT.USAGE, "Usage: noir skills list|sync");
1510
1767
  });
1511
- const taskGrp = program2.command("task").description("workflow task control (S4)");
1768
+ const taskGrp = program2.command("task").description("workflow task control");
1512
1769
  taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").action(async (...args) => {
1513
1770
  const cmd = trailingCmd(args);
1514
1771
  const g = cmd.optsWithGlobals();
@@ -1547,6 +1804,34 @@ function createProgram() {
1547
1804
  taskGrp.action(() => {
1548
1805
  throw new NoirCliError(EXIT.USAGE, "Usage: noir task new|status|advance|next");
1549
1806
  });
1807
+ function buildHandoffOptions(g) {
1808
+ return { ...toCliOptions(g), ...g.write === true ? { write: true } : {} };
1809
+ }
1810
+ program2.command("handoff").description("emit a ready-to-paste host handoff prompt").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
1811
+ await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
1812
+ });
1813
+ program2.command("wrap").description("session-end alias for `noir handoff`").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
1814
+ await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
1815
+ });
1816
+ program2.command("tui").description("interactive Ink dashboard (host \xB7 phase \xB7 daemon + /command dispatch)").action(async (...args) => {
1817
+ const g = actionGlobals(args);
1818
+ const opts = toCliOptions(g);
1819
+ requireInteractive(opts, "`noir tui`");
1820
+ const tuiUrl = new URL("./tui/index.js", import.meta.url).href;
1821
+ const { runTui } = await import(
1822
+ /* @vite-ignore */
1823
+ tuiUrl
1824
+ );
1825
+ const dispatch = async (argv) => {
1826
+ const sub = createProgram();
1827
+ try {
1828
+ await sub.parseAsync([...argv], { from: "user" });
1829
+ } catch (err) {
1830
+ handleError(err);
1831
+ }
1832
+ };
1833
+ await runTui(opts, dispatch);
1834
+ });
1550
1835
  const homeDeps = {
1551
1836
  dispatch: async (argv) => {
1552
1837
  const sub = createProgram();
@@ -1593,9 +1878,11 @@ if (isMainModule) {
1593
1878
  void run(process.argv.slice(2));
1594
1879
  }
1595
1880
  export {
1881
+ DEPRECATIONS,
1596
1882
  EXIT,
1597
1883
  NoirCliError,
1598
1884
  createProgram,
1885
+ emitDeprecationHintsFor,
1599
1886
  fail,
1600
1887
  inferExitCode,
1601
1888
  program,