@noir-ai/cli 1.12.0-beta.1 → 1.13.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.
package/dist/index.js CHANGED
@@ -540,12 +540,326 @@ var init_context = __esm({
540
540
  }
541
541
  });
542
542
 
543
+ // src/workspace-mcp.ts
544
+ import { existsSync, readFileSync } from "fs";
545
+ import { join } from "path";
546
+ import { resolveAdapter } from "@noir-ai/adapters";
547
+ import { atomicWriteFile, resolveNoirCommand } from "@noir-ai/core";
548
+ function mcpPathFor(root, host) {
549
+ return resolveAdapter(host).mcpConfigPath?.({ root }) ?? join(root, ".mcp.json");
550
+ }
551
+ function readJson(path) {
552
+ if (!existsSync(path)) return null;
553
+ return JSON.parse(readFileSync(path, "utf8"));
554
+ }
555
+ function loadExisting(path, opts) {
556
+ let existing;
557
+ try {
558
+ existing = readJson(path);
559
+ } catch {
560
+ if (opts.force !== true) {
561
+ fail(
562
+ EXIT.ERROR,
563
+ `Refusing to rewrite ${path}: it is not valid JSON (add --force to overwrite).`,
564
+ opts
565
+ );
566
+ }
567
+ existing = null;
568
+ }
569
+ if (existing !== null && !isNoirEmitted(existing.mcpServers) && opts.force !== true) {
570
+ fail(
571
+ EXIT.ERROR,
572
+ `Refusing to rewrite ${path}: it does not look like a Noir-emitted config (add --force to overwrite).`,
573
+ opts
574
+ );
575
+ }
576
+ return existing;
577
+ }
578
+ function isNoirEmitted(mcpServers) {
579
+ const noir = mcpServers?.noir;
580
+ if (!noir) return false;
581
+ if (typeof noir.command === "string") return true;
582
+ return noir.type === "http";
583
+ }
584
+ function writeWorkspaceHttpEntry(root, host, url, projectId, opts) {
585
+ const path = mcpPathFor(root, host);
586
+ const existing = loadExisting(path, opts);
587
+ const next = {
588
+ ...existing ?? {},
589
+ mcpServers: {
590
+ ...existing?.mcpServers ?? {},
591
+ noir: { type: "http", url: `${url}?p=${projectId}` }
592
+ }
593
+ };
594
+ atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
595
+ `);
596
+ }
597
+ function writeStdioEntry(root, host, opts) {
598
+ const path = mcpPathFor(root, host);
599
+ const existing = loadExisting(path, opts);
600
+ const noir = { command: resolveNoirCommand(), args: ["mcp", "serve", "--stdio"] };
601
+ const next = { ...existing ?? {}, mcpServers: { ...existing?.mcpServers ?? {}, noir } };
602
+ atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
603
+ `);
604
+ }
605
+ var init_workspace_mcp = __esm({
606
+ "src/workspace-mcp.ts"() {
607
+ "use strict";
608
+ init_output();
609
+ }
610
+ });
611
+
612
+ // src/commands/workspace.ts
613
+ import { readdirSync } from "fs";
614
+ import {
615
+ clearWorkspaceMarker,
616
+ ensureWorkspaceRegistry,
617
+ isValidWorkspaceName,
618
+ isWorkspaceMember,
619
+ loadProjectInfo,
620
+ readWorkspaceMarker,
621
+ readWorkspaceRegistry,
622
+ removeWorkspaceMember,
623
+ upsertWorkspaceMember,
624
+ workspaceHomeDir,
625
+ writeWorkspaceMarker
626
+ } from "@noir-ai/core";
627
+ import {
628
+ clearWorkspaceDaemonRecord,
629
+ ensureWorkspaceDaemonRunning,
630
+ pidAlive,
631
+ readWorkspaceDaemonRecord,
632
+ spawnDetachedWorkspaceDaemon
633
+ } from "@noir-ai/daemon";
634
+ function loadProjectOrFail(opts) {
635
+ try {
636
+ return loadProjectInfo(process.cwd());
637
+ } catch {
638
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
639
+ }
640
+ }
641
+ function finishJoin(project, name, url, opts) {
642
+ writeWorkspaceMarker(project.root, name);
643
+ writeWorkspaceHttpEntry(project.root, project.config.host, url, project.id, opts);
644
+ }
645
+ async function daemonStartWorkspace(opts) {
646
+ const project = loadProjectOrFail(opts);
647
+ if (!isValidWorkspaceName(opts.name)) {
648
+ fail(
649
+ EXIT.USAGE,
650
+ `invalid workspace name ${JSON.stringify(opts.name)} (lowercase letters, digits, and dashes).`,
651
+ opts
652
+ );
653
+ }
654
+ upsertWorkspaceMember(ensureWorkspaceRegistry(opts.name), {
655
+ projectId: project.id,
656
+ root: project.root,
657
+ joinedAt: Date.now()
658
+ });
659
+ if (opts.detachChild === true) {
660
+ const ensured2 = await ensureWorkspaceDaemonRunning({
661
+ name: opts.name,
662
+ project,
663
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
664
+ });
665
+ if (opts.json === true) {
666
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { mode: "detached" } })}
667
+ `);
668
+ return;
669
+ }
670
+ info("noir workspace daemon: detached foreground. Ctrl+C to stop.", opts);
671
+ log(`workspace daemon listening at ${ensured2.url}`, opts);
672
+ return;
673
+ }
674
+ if (opts.detach === true) {
675
+ const spawned = await spawnDetachedWorkspaceDaemon({ name: opts.name, project });
676
+ finishJoin(project, opts.name, `http://127.0.0.1:${spawned.port}/mcp`, opts);
677
+ if (opts.json === true) {
678
+ process.stdout.write(
679
+ `${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
680
+ `
681
+ );
682
+ return;
683
+ }
684
+ info(
685
+ `workspace "${opts.name}" daemon started (pid ${spawned.pid}, port ${spawned.port}).`,
686
+ opts
687
+ );
688
+ return;
689
+ }
690
+ const ensured = await ensureWorkspaceDaemonRunning({
691
+ name: opts.name,
692
+ project,
693
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
694
+ });
695
+ finishJoin(project, opts.name, ensured.url, opts);
696
+ if (opts.json === true) {
697
+ process.stdout.write(
698
+ `${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: !ensured.started } })}
699
+ `
700
+ );
701
+ return;
702
+ }
703
+ info("noir workspace daemon: foreground mode. Ctrl+C to stop.", opts);
704
+ log(`workspace daemon listening at ${ensured.url}`, opts);
705
+ }
706
+ async function daemonJoin(opts) {
707
+ const project = loadProjectOrFail(opts);
708
+ if (!isValidWorkspaceName(opts.name)) {
709
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(opts.name)}.`, opts);
710
+ }
711
+ const reg = readWorkspaceRegistry(opts.name);
712
+ if (!reg) {
713
+ fail(
714
+ EXIT.ERROR,
715
+ `workspace ${JSON.stringify(opts.name)} not found \u2014 start it from a member repo with \`noir daemon start --workspace ${opts.name}\`.`,
716
+ opts
717
+ );
718
+ }
719
+ const ensured = await ensureWorkspaceDaemonRunning({
720
+ name: opts.name,
721
+ project,
722
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
723
+ });
724
+ upsertWorkspaceMember(reg, { projectId: project.id, root: project.root, joinedAt: Date.now() });
725
+ finishJoin(project, opts.name, ensured.url, opts);
726
+ if (opts.json === true) {
727
+ process.stdout.write(
728
+ `${JSON.stringify({ ok: true, data: { joined: true, url: ensured.url, port: ensured.port } })}
729
+ `
730
+ );
731
+ return;
732
+ }
733
+ info(`joined workspace ${JSON.stringify(opts.name)} (${ensured.url}).`, opts);
734
+ }
735
+ async function workspaceList(opts) {
736
+ let names = [];
737
+ try {
738
+ names = readdirSync(workspaceHomeDir(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
739
+ } catch {
740
+ names = [];
741
+ }
742
+ const rows = names.map((n) => {
743
+ const reg = readWorkspaceRegistry(n);
744
+ return {
745
+ name: n,
746
+ members: reg?.members.length ?? 0,
747
+ // A record file whose pid is dead (daemon killed without cleanup) must not
748
+ // read as "running" — mirror the pidAlive liveness `workspace status` uses.
749
+ running: (() => {
750
+ const rec = readWorkspaceDaemonRecord(n);
751
+ return rec !== null && pidAlive(rec.pid);
752
+ })()
753
+ };
754
+ });
755
+ if (opts.json === true) {
756
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { workspaces: rows } })}
757
+ `);
758
+ return;
759
+ }
760
+ if (rows.length === 0) {
761
+ info("No workspaces yet.", opts);
762
+ return;
763
+ }
764
+ for (const r of rows) {
765
+ log(`${r.name} \u2014 ${r.members} member(s)${r.running ? ", running" : ""}`, opts);
766
+ }
767
+ }
768
+ async function workspaceStatus(opts) {
769
+ const name = opts.name ?? readWorkspaceMarker(process.cwd()) ?? void 0;
770
+ if (name === void 0) {
771
+ fail(EXIT.USAGE, "workspace status requires a name (or run from a joined repo).", opts);
772
+ }
773
+ if (!isValidWorkspaceName(name)) {
774
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
775
+ }
776
+ const reg = readWorkspaceRegistry(name);
777
+ const rec = readWorkspaceDaemonRecord(name);
778
+ const running = rec !== null && pidAlive(rec.pid);
779
+ const data = {
780
+ name,
781
+ members: reg?.members.map((m) => m.projectId) ?? [],
782
+ running,
783
+ ...rec ? { pid: rec.pid, port: rec.port } : {}
784
+ };
785
+ if (opts.json === true) {
786
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
787
+ `);
788
+ return;
789
+ }
790
+ log(
791
+ `workspace ${name}: ${running ? `running (pid ${rec?.pid}, port ${rec?.port})` : "not running"}`,
792
+ opts
793
+ );
794
+ for (const m of data.members) log(` member: ${m}`, opts);
795
+ }
796
+ async function workspaceLeave(opts) {
797
+ const project = loadProjectOrFail(opts);
798
+ const name = readWorkspaceMarker(project.root);
799
+ if (name === null) {
800
+ fail(EXIT.USAGE, "this repo is not joined to a workspace.", opts);
801
+ }
802
+ writeStdioEntry(project.root, project.config.host, opts);
803
+ const reg = readWorkspaceRegistry(name);
804
+ if (reg && isWorkspaceMember(reg, project.id)) {
805
+ removeWorkspaceMember(reg, project.id);
806
+ }
807
+ clearWorkspaceMarker(project.root);
808
+ if (opts.json === true) {
809
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { left: name } })}
810
+ `);
811
+ return;
812
+ }
813
+ info(`left workspace ${JSON.stringify(name)}.`, opts);
814
+ }
815
+ async function workspaceStop(opts) {
816
+ const name = opts.name ?? readWorkspaceMarker(process.cwd()) ?? void 0;
817
+ if (name === void 0) {
818
+ fail(EXIT.USAGE, "workspace stop requires a name (or run from a joined repo).", opts);
819
+ }
820
+ if (!isValidWorkspaceName(name)) {
821
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
822
+ }
823
+ const rec = readWorkspaceDaemonRecord(name);
824
+ if (rec === null || !pidAlive(rec.pid)) {
825
+ if (opts.json === true) {
826
+ process.stdout.write(
827
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
828
+ `
829
+ );
830
+ return;
831
+ }
832
+ info("workspace daemon is not running.", opts);
833
+ return;
834
+ }
835
+ try {
836
+ process.kill(rec.pid, "SIGTERM");
837
+ } catch {
838
+ }
839
+ clearWorkspaceDaemonRecord(name);
840
+ if (opts.json === true) {
841
+ process.stdout.write(
842
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: true, pid: rec.pid } })}
843
+ `
844
+ );
845
+ return;
846
+ }
847
+ info(`stopped workspace daemon (pid ${rec.pid}).`, opts);
848
+ }
849
+ var init_workspace = __esm({
850
+ "src/commands/workspace.ts"() {
851
+ "use strict";
852
+ init_output();
853
+ init_workspace_mcp();
854
+ }
855
+ });
856
+
543
857
  // src/commands/daemon.ts
544
- import { loadProjectInfo } from "@noir-ai/core";
858
+ import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
545
859
  import {
546
860
  clearDaemonRecord,
547
861
  ensureDaemonRunning,
548
- pidAlive,
862
+ pidAlive as pidAlive2,
549
863
  readDaemonRecord,
550
864
  spawnDetachedDaemon
551
865
  } from "@noir-ai/daemon";
@@ -557,7 +871,7 @@ function formatUptime(sec) {
557
871
  }
558
872
  function resolveCallerProjectId() {
559
873
  try {
560
- return loadProjectInfo(process.cwd()).id;
874
+ return loadProjectInfo2(process.cwd()).id;
561
875
  } catch {
562
876
  return void 0;
563
877
  }
@@ -578,9 +892,18 @@ async function isHealthy(port, expectedPid, expectedProjectId) {
578
892
  }
579
893
  }
580
894
  async function daemonStart(opts) {
895
+ if (opts.workspace !== void 0) {
896
+ await daemonStartWorkspace({
897
+ ...opts,
898
+ name: opts.workspace,
899
+ ...opts.detach === true ? { detach: true } : {},
900
+ ...opts.detachChild === true ? { detachChild: true } : {}
901
+ });
902
+ return;
903
+ }
581
904
  let project;
582
905
  try {
583
- project = loadProjectInfo(process.cwd());
906
+ project = loadProjectInfo2(process.cwd());
584
907
  } catch {
585
908
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
586
909
  }
@@ -611,7 +934,7 @@ async function daemonStart(opts) {
611
934
  }
612
935
  if (opts.detach === true) {
613
936
  const existing = readDaemonRecord();
614
- if (existing && pidAlive(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
937
+ if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
615
938
  if (opts.json === true) {
616
939
  process.stdout.write(
617
940
  `${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
@@ -710,7 +1033,7 @@ async function daemonStop(opts) {
710
1033
  let errMsg2;
711
1034
  const callerProject = resolveCallerProjectId();
712
1035
  const wrongProject = rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject;
713
- const healthy = !wrongProject && pidAlive(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
1036
+ const healthy = !wrongProject && pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
714
1037
  if (healthy) {
715
1038
  try {
716
1039
  process.kill(rec.pid, "SIGTERM");
@@ -756,7 +1079,7 @@ async function daemonStatus(opts) {
756
1079
  opts
757
1080
  );
758
1081
  }
759
- if (!pidAlive(rec.pid)) {
1082
+ if (!pidAlive2(rec.pid)) {
760
1083
  clearDaemonRecord();
761
1084
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
762
1085
  }
@@ -814,6 +1137,7 @@ var init_daemon = __esm({
814
1137
  "use strict";
815
1138
  init_daemon_client();
816
1139
  init_output();
1140
+ init_workspace();
817
1141
  MODE = "foreground";
818
1142
  }
819
1143
  });
@@ -826,13 +1150,13 @@ __export(install_exports, {
826
1150
  installManagedNode: () => installManagedNode
827
1151
  });
828
1152
  import { chmodSync, mkdirSync } from "fs";
829
- import { join } from "path";
1153
+ import { join as join2 } from "path";
830
1154
  import {
831
- atomicWriteFile,
1155
+ atomicWriteFile as atomicWriteFile2,
832
1156
  detectActiveMethod,
833
1157
  detectInstallMethods,
834
1158
  ensureShimExecutable,
835
- loadProjectInfo as loadProjectInfo2,
1159
+ loadProjectInfo as loadProjectInfo3,
836
1160
  MANAGED_NODE_VERSION,
837
1161
  NOIR_VERSION,
838
1162
  noirHome,
@@ -878,8 +1202,8 @@ function buildMigrationPlan(opts) {
878
1202
  async function installManagedNode(opts) {
879
1203
  const spec = opts.version ?? opts.channel ?? "latest";
880
1204
  const home2 = noirHome();
881
- const cliDir = join(home2, "cli");
882
- const binDir = join(home2, "bin");
1205
+ const cliDir = join2(home2, "cli");
1206
+ const binDir = join2(home2, "bin");
883
1207
  mkdirSync(cliDir, { recursive: true });
884
1208
  mkdirSync(binDir, { recursive: true });
885
1209
  let runtime;
@@ -905,17 +1229,17 @@ async function installManagedNode(opts) {
905
1229
  if (code !== 0) {
906
1230
  return { ok: false, version: null, error: `npm install failed: ${stderr.slice(0, 300)}` };
907
1231
  }
908
- const shim = join(binDir, "noir");
1232
+ const shim = join2(binDir, "noir");
909
1233
  const shimBody = `#!/usr/bin/env bash
910
- "${nodeBin}" "${join(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js")}" "$@"
1234
+ "${nodeBin}" "${join2(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js")}" "$@"
911
1235
  `;
912
- atomicWriteFile(shim, shimBody);
1236
+ atomicWriteFile2(shim, shimBody);
913
1237
  chmodSync(shim, 493);
914
1238
  ensureShimExecutable();
915
1239
  const ver = await runManagerCmd(
916
1240
  [
917
1241
  nodeBin,
918
- join(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js"),
1242
+ join2(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js"),
919
1243
  "--version"
920
1244
  ],
921
1245
  { env: opts.env, timeoutMs: 3e4 }
@@ -971,7 +1295,7 @@ async function install(opts = {}) {
971
1295
  const installedRecord = readInstallRecord();
972
1296
  let minVersion = "1.6.0";
973
1297
  try {
974
- minVersion = loadProjectInfo2(process.cwd()).config.update.minVersion;
1298
+ minVersion = loadProjectInfo3(process.cwd()).config.update.minVersion;
975
1299
  } catch {
976
1300
  }
977
1301
  const plan = buildMigrationPlan({
@@ -1048,7 +1372,7 @@ import {
1048
1372
  detectActiveMethod as detectActiveMethod2,
1049
1373
  ensureShimExecutable as ensureShimExecutable2,
1050
1374
  fetchLatestVersion,
1051
- loadProjectInfo as loadProjectInfo3,
1375
+ loadProjectInfo as loadProjectInfo4,
1052
1376
  readInstallRecord as readInstallRecord2,
1053
1377
  readUpdateCache,
1054
1378
  runManagerCmd as runManagerCmd2,
@@ -1128,7 +1452,7 @@ async function update(opts = {}) {
1128
1452
  fetchSpin.succeed(`Latest: ${latest}`);
1129
1453
  let minVersion = DEFAULT_UPDATE_CONFIG.minVersion;
1130
1454
  try {
1131
- minVersion = loadProjectInfo3(process.cwd()).config.update.minVersion;
1455
+ minVersion = loadProjectInfo4(process.cwd()).config.update.minVersion;
1132
1456
  } catch {
1133
1457
  }
1134
1458
  const target = buildUpdateTarget({
@@ -1278,7 +1602,7 @@ var init_update = __esm({
1278
1602
  });
1279
1603
 
1280
1604
  // src/commands/status.ts
1281
- import { loadProjectInfo as loadProjectInfo4, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
1605
+ import { loadProjectInfo as loadProjectInfo5, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
1282
1606
  async function tryTool(caller, name, args) {
1283
1607
  try {
1284
1608
  return await caller.callTool(name, args);
@@ -1399,7 +1723,7 @@ function renderHuman(p, opts) {
1399
1723
  async function gatherStatusPayload(opts) {
1400
1724
  let project;
1401
1725
  try {
1402
- project = loadProjectInfo4(process.cwd());
1726
+ project = loadProjectInfo5(process.cwd());
1403
1727
  } catch {
1404
1728
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
1405
1729
  }
@@ -1429,7 +1753,7 @@ async function status(opts) {
1429
1753
  env: process.env,
1430
1754
  configUpdate: (() => {
1431
1755
  try {
1432
- const info2 = loadProjectInfo4(process.cwd());
1756
+ const info2 = loadProjectInfo5(process.cwd());
1433
1757
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
1434
1758
  } catch {
1435
1759
  return DEFAULT_UPDATE_CONFIG;
@@ -1458,7 +1782,7 @@ var init_status = __esm({
1458
1782
  // src/commands/task.ts
1459
1783
  import { spawnSync } from "child_process";
1460
1784
  import { writeFileSync } from "fs";
1461
- import { loadProjectInfo as loadProjectInfo5, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
1785
+ import { loadProjectInfo as loadProjectInfo6, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
1462
1786
  import {
1463
1787
  PHASES,
1464
1788
  TASK_CLASSES,
@@ -1471,7 +1795,7 @@ function skillFor(phase) {
1471
1795
  }
1472
1796
  function writeDoneArtifacts(opts, taskId) {
1473
1797
  try {
1474
- const project = loadProjectInfo5(process.cwd());
1798
+ const project = loadProjectInfo6(process.cwd());
1475
1799
  const conflict = { conflictPolicy: "preserve", interactive: false };
1476
1800
  writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
1477
1801
  const nextN = nextArtifactSequence(project.root, "adr");
@@ -1794,7 +2118,7 @@ function runCheck(cmd) {
1794
2118
  async function taskVerify(opts) {
1795
2119
  let project;
1796
2120
  try {
1797
- project = loadProjectInfo5(process.cwd());
2121
+ project = loadProjectInfo6(process.cwd());
1798
2122
  } catch {
1799
2123
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
1800
2124
  }
@@ -2000,13 +2324,13 @@ var init_task = __esm({
2000
2324
 
2001
2325
  // src/commands/handoff.ts
2002
2326
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2003
- import { join as join2 } from "path";
2327
+ import { join as join3 } from "path";
2004
2328
  import {
2005
2329
  defaultHandoffBlock,
2006
2330
  hostLaunchDirective,
2007
- resolveAdapter
2331
+ resolveAdapter as resolveAdapter2
2008
2332
  } from "@noir-ai/adapters";
2009
- import { loadProjectInfo as loadProjectInfo6, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
2333
+ import { loadProjectInfo as loadProjectInfo7, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
2010
2334
  function heading(name, id) {
2011
2335
  return `# Noir handoff \u2014 ${name} (${id})`;
2012
2336
  }
@@ -2102,7 +2426,7 @@ function toHostPayload(p) {
2102
2426
  }
2103
2427
  function resolveDirective(root, p) {
2104
2428
  const ctx = { root };
2105
- const adapter = resolveAdapter(p.host);
2429
+ const adapter = resolveAdapter2(p.host);
2106
2430
  const hostPayload = toHostPayload(p);
2107
2431
  if (typeof adapter.emitHandoff === "function") {
2108
2432
  return adapter.emitHandoff(ctx, hostPayload);
@@ -2145,7 +2469,7 @@ async function probeOnly(opts) {
2145
2469
  async function handoff(opts) {
2146
2470
  let project;
2147
2471
  try {
2148
- project = loadProjectInfo6(process.cwd());
2472
+ project = loadProjectInfo7(process.cwd());
2149
2473
  } catch {
2150
2474
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2151
2475
  }
@@ -2177,7 +2501,7 @@ async function handoff(opts) {
2177
2501
  if (opts.write === true) {
2178
2502
  const md = renderMarkdown(full);
2179
2503
  const path = handoffFilePath(project.root, task?.taskId, project.id);
2180
- mkdirSync2(join2(project.root, NOIR_DIR, "handoff"), { recursive: true });
2504
+ mkdirSync2(join3(project.root, NOIR_DIR, "handoff"), { recursive: true });
2181
2505
  writeFileSync2(path, md, "utf8");
2182
2506
  info(`handoff written to ${path}`, opts);
2183
2507
  }
@@ -2509,14 +2833,14 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
2509
2833
  import {
2510
2834
  detectActiveMethod as detectActiveMethod3,
2511
2835
  latestVersionFromCache,
2512
- loadProjectInfo as loadProjectInfo7,
2836
+ loadProjectInfo as loadProjectInfo8,
2513
2837
  NOIR_VERSION as NOIR_VERSION3,
2514
2838
  readInstallRecord as readInstallRecord3,
2515
2839
  readUpdateCache as readUpdateCache2
2516
2840
  } from "@noir-ai/core";
2517
2841
  function tryProject() {
2518
2842
  try {
2519
- const info2 = loadProjectInfo7(process.cwd());
2843
+ const info2 = loadProjectInfo8(process.cwd());
2520
2844
  return { id: info2.id, name: info2.name, host: info2.config.host };
2521
2845
  } catch {
2522
2846
  return null;
@@ -2679,7 +3003,7 @@ async function runMenu(opts, deps) {
2679
3003
  env: process.env,
2680
3004
  configUpdate: (() => {
2681
3005
  try {
2682
- const info2 = loadProjectInfo7(process.cwd());
3006
+ const info2 = loadProjectInfo8(process.cwd());
2683
3007
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
2684
3008
  } catch {
2685
3009
  return DEFAULT_UPDATE_CONFIG;
@@ -2741,6 +3065,27 @@ var init_home = __esm({
2741
3065
  });
2742
3066
 
2743
3067
  // src/commands/memory.ts
3068
+ import { readFileSync as readFileSync2 } from "fs";
3069
+ import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
3070
+ function resolveWorkspace() {
3071
+ const routing = workspaceRoutingTarget(process.cwd());
3072
+ if (routing === null) return null;
3073
+ try {
3074
+ return { routing, project: loadProjectInfo9(process.cwd()) };
3075
+ } catch {
3076
+ return null;
3077
+ }
3078
+ }
3079
+ async function callMemory(opts, name, args = {}) {
3080
+ const ws = resolveWorkspace();
3081
+ if (ws === null) return callDaemonTool(opts, name, args);
3082
+ return withWorkspaceDaemon(opts, ws.routing, (caller) => caller.callTool(name, args));
3083
+ }
3084
+ async function withMemoryDaemon(opts, fn) {
3085
+ const ws = resolveWorkspace();
3086
+ if (ws === null) return withDaemon(opts, fn);
3087
+ return withWorkspaceDaemon(opts, ws.routing, fn);
3088
+ }
2744
3089
  function parseLimit2(raw, label, opts) {
2745
3090
  if (raw === void 0) return void 0;
2746
3091
  const n = Number(raw);
@@ -2779,6 +3124,39 @@ function stamp(ts) {
2779
3124
  }
2780
3125
  async function memoryRecall(opts) {
2781
3126
  const limit = parseLimit2(opts.limit, "memory recall", opts);
3127
+ const ws = resolveWorkspace();
3128
+ if (ws !== null) {
3129
+ const probe2 = await probeWorkspaceDaemon(ws.routing);
3130
+ if (!probe2.running) {
3131
+ await withWorkspaceMemoryRead(opts, ws.routing, ws.project, async (memory) => {
3132
+ const { hits: hits3, degraded, mode } = await memory.recallWithMeta(opts.query, {
3133
+ ...limit === void 0 ? {} : { limit }
3134
+ });
3135
+ const data3 = { query: opts.query, hits: hits3, degraded, mode };
3136
+ if (opts.json === true) {
3137
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data3 })}
3138
+ `);
3139
+ return;
3140
+ }
3141
+ renderRecall(data3.query, data3.hits, data3.degraded, opts);
3142
+ });
3143
+ return;
3144
+ }
3145
+ const res2 = await callMemory(opts, "memory_recall", {
3146
+ query: opts.query,
3147
+ ...limit === void 0 ? {} : { limit }
3148
+ });
3149
+ if (res2.ok !== true) failTool2("memory recall", res2, opts);
3150
+ const hits2 = Array.isArray(res2.results) ? res2.results.map(toHit2) : [];
3151
+ const data2 = { query: opts.query, hits: hits2, degraded: res2.degraded === true };
3152
+ if (opts.json === true) {
3153
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
3154
+ `);
3155
+ return;
3156
+ }
3157
+ renderRecall(data2.query, hits2, data2.degraded, opts);
3158
+ return;
3159
+ }
2782
3160
  let probe;
2783
3161
  try {
2784
3162
  probe = await probeDaemon(opts);
@@ -2840,7 +3218,7 @@ async function memorySave(opts) {
2840
3218
  const args = { content };
2841
3219
  if (typeof opts.type === "string" && opts.type.length > 0) args.type = opts.type;
2842
3220
  if (files !== void 0) args.files = files;
2843
- const res = await callDaemonTool(opts, "memory_save", args);
3221
+ const res = await callMemory(opts, "memory_save", args);
2844
3222
  if (res.ok !== true) failTool2("memory save", res, opts);
2845
3223
  const id = typeof res.id === "string" ? res.id : "";
2846
3224
  const observation = res.observation ?? {};
@@ -2884,7 +3262,98 @@ function renderObservation(obs, opts) {
2884
3262
  if (typeof content === "string") log(`
2885
3263
  ${content}`, opts);
2886
3264
  }
3265
+ async function memoryCapture(opts) {
3266
+ const content = await resolveCaptureContent(opts);
3267
+ const args = { content };
3268
+ if (typeof opts.eventType === "string" && opts.eventType.length > 0)
3269
+ args.eventType = opts.eventType;
3270
+ const res = await callMemory(opts, "memory_capture", args);
3271
+ if (res.ok !== true) failTool2("memory capture", res, opts);
3272
+ const id = typeof res.id === "string" ? res.id : "";
3273
+ const observation = res.observation ?? {};
3274
+ if (opts.json === true) {
3275
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { id, observation } })}
3276
+ `);
3277
+ return;
3278
+ }
3279
+ log(`Captured memory ${id}.`, opts);
3280
+ renderObservation(observation, opts);
3281
+ }
3282
+ async function resolveCaptureContent(opts) {
3283
+ if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
3284
+ if (typeof opts.file === "string" && opts.file.length > 0) {
3285
+ try {
3286
+ return readFileSync2(opts.file, "utf8");
3287
+ } catch {
3288
+ fail(EXIT.USAGE, `memory capture could not read --file ${opts.file}`, opts);
3289
+ }
3290
+ }
3291
+ if (!process.stdin.isTTY) {
3292
+ try {
3293
+ return readFileSync2(0, "utf8");
3294
+ } catch {
3295
+ fail(
3296
+ EXIT.USAGE,
3297
+ "memory capture could not read piped stdin \u2014 pass --content <text> or --file <path>.",
3298
+ opts
3299
+ );
3300
+ }
3301
+ }
3302
+ if (!isInteractive(opts)) {
3303
+ fail(
3304
+ EXIT.USAGE,
3305
+ "memory capture requires --content <text> or --file <path> (or piped stdin).",
3306
+ opts
3307
+ );
3308
+ }
3309
+ const clack = await import("@clack/prompts");
3310
+ const value = await clack.text({ message: "Captured memory content:" });
3311
+ if (clack.isCancel(value)) {
3312
+ clack.cancel("Cancelled.");
3313
+ fail(EXIT.CANCELLED, "cancelled", opts);
3314
+ }
3315
+ return String(value);
3316
+ }
2887
3317
  async function memorySessions(opts) {
3318
+ const ws = resolveWorkspace();
3319
+ if (ws !== null) {
3320
+ const probe2 = await probeWorkspaceDaemon(ws.routing);
3321
+ if (!probe2.running) {
3322
+ await withWorkspaceMemoryRead(opts, ws.routing, ws.project, async (memory) => {
3323
+ const sessions3 = memory.sessions().map((s) => ({
3324
+ id: s.id,
3325
+ count: s.count,
3326
+ lastTs: s.lastTs
3327
+ }));
3328
+ const data3 = { sessions: sessions3 };
3329
+ if (opts.json === true) {
3330
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data3 })}
3331
+ `);
3332
+ return;
3333
+ }
3334
+ renderSessions(sessions3, opts);
3335
+ });
3336
+ return;
3337
+ }
3338
+ const res2 = await callMemory(opts, "memory_sessions");
3339
+ if (res2.ok !== true) failTool2("memory sessions", res2, opts);
3340
+ const sessions2 = Array.isArray(res2.sessions) ? res2.sessions.map((raw) => {
3341
+ const s = raw ?? {};
3342
+ return {
3343
+ id: typeof s.id === "string" ? s.id : "",
3344
+ count: typeof s.count === "number" ? s.count : 0,
3345
+ lastTs: typeof s.lastTs === "number" ? s.lastTs : 0
3346
+ };
3347
+ }) : [];
3348
+ const data2 = { sessions: sessions2 };
3349
+ if (opts.json === true) {
3350
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
3351
+ `);
3352
+ return;
3353
+ }
3354
+ renderSessions(sessions2, opts);
3355
+ return;
3356
+ }
2888
3357
  let probe;
2889
3358
  try {
2890
3359
  probe = await probeDaemon(opts);
@@ -2939,7 +3408,7 @@ async function memoryForget(opts) {
2939
3408
  if (opts.ids.length === 0) {
2940
3409
  fail(EXIT.USAGE, "memory forget requires at least one <id>", opts);
2941
3410
  }
2942
- const res = await callDaemonTool(opts, "memory_forget", {
3411
+ const res = await callMemory(opts, "memory_forget", {
2943
3412
  ids: opts.ids
2944
3413
  });
2945
3414
  if (res.ok !== true) failTool2("memory forget", res, opts);
@@ -2953,11 +3422,11 @@ async function memoryForget(opts) {
2953
3422
  log(`Forgot ${deleted} observation${deleted === 1 ? "" : "s"}.`, opts);
2954
3423
  }
2955
3424
  async function memoryConsolidate(opts) {
2956
- const exposed = await withDaemon(opts, async (caller) => caller.listTools());
3425
+ const exposed = await withMemoryDaemon(opts, async (caller) => caller.listTools());
2957
3426
  if (!exposed.includes("memory_consolidate")) {
2958
3427
  fail(
2959
3428
  EXIT.ERROR,
2960
- "memory consolidate: the daemon does not expose the memory_consolidate tool. Enable it in .noir/config under memory.consolidation (enabled: true + a provider + model), then restart the daemon.",
3429
+ resolveWorkspace() !== null ? "memory consolidate is not supported on shared workspaces (the workspace memory store has no consolidation model)." : "memory consolidate: the daemon does not expose the memory_consolidate tool. Enable it in .noir/config under memory.consolidation (enabled: true + a provider + model), then restart the daemon.",
2961
3430
  opts
2962
3431
  );
2963
3432
  }
@@ -2966,7 +3435,7 @@ async function memoryConsolidate(opts) {
2966
3435
  const args = {};
2967
3436
  if (types !== void 0) args.types = types;
2968
3437
  if (limit !== void 0) args.limit = limit;
2969
- const res = await callDaemonTool(
3438
+ const res = await callMemory(
2970
3439
  opts,
2971
3440
  "memory_consolidate",
2972
3441
  args
@@ -3485,10 +3954,10 @@ var init_orchestrator = __esm({
3485
3954
  });
3486
3955
 
3487
3956
  // src/run-profiles.ts
3488
- import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
3957
+ import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
3489
3958
  function loadRunConfig(root) {
3490
3959
  try {
3491
- return loadProjectInfo8(root).config;
3960
+ return loadProjectInfo10(root).config;
3492
3961
  } catch {
3493
3962
  return null;
3494
3963
  }
@@ -3542,7 +4011,7 @@ var init_run_profiles = __esm({
3542
4011
 
3543
4012
  // src/commands/run.ts
3544
4013
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
3545
- import { join as join3 } from "path";
4014
+ import { join as join4 } from "path";
3546
4015
  import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
3547
4016
  import { parseConfig } from "@noir-ai/core";
3548
4017
  function formatUsage(u) {
@@ -3636,8 +4105,8 @@ function mergeEnv(base, overlay) {
3636
4105
  }
3637
4106
  function writeTranscript(host, lines) {
3638
4107
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3639
- const dir = join3(process.cwd(), ".noir", "transcripts");
3640
- const file = join3(dir, `${host}-${ts}.jsonl`);
4108
+ const dir = join4(process.cwd(), ".noir", "transcripts");
4109
+ const file = join4(dir, `${host}-${ts}.jsonl`);
3641
4110
  try {
3642
4111
  mkdirSync3(dir, { recursive: true, mode: 448 });
3643
4112
  writeFileSync3(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
@@ -3656,8 +4125,8 @@ var init_run = __esm({
3656
4125
  });
3657
4126
 
3658
4127
  // src/commands/skills.ts
3659
- import { resolveAdapter as resolveAdapter2 } from "@noir-ai/adapters";
3660
- import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
4128
+ import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
4129
+ import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
3661
4130
  import {
3662
4131
  buildRegistry,
3663
4132
  discoverAll,
@@ -3725,11 +4194,11 @@ async function skillsSync(opts) {
3725
4194
  const root = process.cwd();
3726
4195
  let project;
3727
4196
  try {
3728
- project = loadProjectInfo9(root);
4197
+ project = loadProjectInfo11(root);
3729
4198
  } catch {
3730
4199
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
3731
4200
  }
3732
- const adapter = resolveAdapter2(project.config.host);
4201
+ const adapter = resolveAdapter3(project.config.host);
3733
4202
  if (!adapter.skillsDir) {
3734
4203
  const data2 = {
3735
4204
  emitted: [],
@@ -4001,8 +4470,8 @@ var init_conflict = __esm({
4001
4470
  });
4002
4471
 
4003
4472
  // src/dedup-write.ts
4004
- import { mkdirSync as mkdirSync4, readFileSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
4005
- import { join as join4 } from "path";
4473
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync4 } from "fs";
4474
+ import { join as join5 } from "path";
4006
4475
  import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
4007
4476
  async function checkWritePathDedup(root, res, opts) {
4008
4477
  const result = { found: [], conflicts: [] };
@@ -4013,10 +4482,10 @@ async function checkWritePathDedup(root, res, opts) {
4013
4482
  if (embed === void 0) return result;
4014
4483
  const cachedEmbed = makeCachedEmbed(cache, embed);
4015
4484
  for (const proposedRel of writtenHostFiles) {
4016
- const proposedAbs = join4(root, proposedRel);
4485
+ const proposedAbs = join5(root, proposedRel);
4017
4486
  let proposedText;
4018
4487
  try {
4019
- proposedText = readFileSync(proposedAbs, "utf8");
4488
+ proposedText = readFileSync3(proposedAbs, "utf8");
4020
4489
  } catch {
4021
4490
  continue;
4022
4491
  }
@@ -4060,7 +4529,7 @@ async function checkWritePathDedup(root, res, opts) {
4060
4529
  `
4061
4530
  );
4062
4531
  }
4063
- const matchedText = safeRead(join4(root, matchedRel));
4532
+ const matchedText = safeRead(join5(root, matchedRel));
4064
4533
  result.conflicts.push({
4065
4534
  path: proposedRel,
4066
4535
  mode: "artifact",
@@ -4144,21 +4613,21 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
4144
4613
  if (clack.isCancel(choice)) return "preserve";
4145
4614
  if (choice === "skip") {
4146
4615
  try {
4147
- rmSync(join4(root, proposedRel), { force: true });
4616
+ rmSync(join5(root, proposedRel), { force: true });
4148
4617
  } catch {
4149
4618
  }
4150
4619
  return "preserve";
4151
4620
  } else if (choice === "replace") {
4152
4621
  try {
4153
- writeFileSync4(join4(root, matchedRel), proposedText, "utf8");
4622
+ writeFileSync4(join5(root, matchedRel), proposedText, "utf8");
4154
4623
  } catch {
4155
4624
  }
4156
4625
  return "replace";
4157
4626
  } else if (choice === "mirror") {
4158
4627
  try {
4159
- const existing = readFileSync(join4(root, matchedRel), "utf8");
4628
+ const existing = readFileSync3(join5(root, matchedRel), "utf8");
4160
4629
  writeFileSync4(
4161
- join4(root, matchedRel),
4630
+ join5(root, matchedRel),
4162
4631
  `${existing}
4163
4632
 
4164
4633
  <!-- mirrored from ${proposedRel} -->
@@ -4176,11 +4645,11 @@ function emptyCache() {
4176
4645
  return { version: DEDUP_CACHE_VERSION, model: "", entries: {} };
4177
4646
  }
4178
4647
  function cachePath(root) {
4179
- return join4(root, ".noir", "dedup-cache.json");
4648
+ return join5(root, ".noir", "dedup-cache.json");
4180
4649
  }
4181
4650
  function loadCache(root) {
4182
4651
  try {
4183
- const raw = readFileSync(cachePath(root), "utf8");
4652
+ const raw = readFileSync3(cachePath(root), "utf8");
4184
4653
  const parsed = JSON.parse(raw);
4185
4654
  if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
4186
4655
  if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
@@ -4195,7 +4664,7 @@ function loadCache(root) {
4195
4664
  }
4196
4665
  function saveCache(root, cache) {
4197
4666
  try {
4198
- mkdirSync4(join4(root, ".noir"), { recursive: true });
4667
+ mkdirSync4(join5(root, ".noir"), { recursive: true });
4199
4668
  writeFileSync4(cachePath(root), JSON.stringify(cache), "utf8");
4200
4669
  } catch {
4201
4670
  }
@@ -4212,7 +4681,7 @@ function makeCachedEmbed(cache, embed) {
4212
4681
  }
4213
4682
  function safeRead(abs) {
4214
4683
  try {
4215
- return readFileSync(abs, "utf8");
4684
+ return readFileSync3(abs, "utf8");
4216
4685
  } catch {
4217
4686
  return void 0;
4218
4687
  }
@@ -4250,8 +4719,8 @@ var init_dedup_write = __esm({
4250
4719
  });
4251
4720
 
4252
4721
  // src/init.ts
4253
- import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
4254
- import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
4722
+ import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
4723
+ import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
4255
4724
  import { scaffold } from "@noir-ai/create";
4256
4725
  import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
4257
4726
  async function init(root, opts) {
@@ -4280,7 +4749,7 @@ async function init(root, opts) {
4280
4749
  await emitHostSkills(root, host, conflictOpts, interactive);
4281
4750
  let projectInfo;
4282
4751
  try {
4283
- projectInfo = loadProjectInfo10(root);
4752
+ projectInfo = loadProjectInfo12(root);
4284
4753
  } catch {
4285
4754
  projectInfo = void 0;
4286
4755
  }
@@ -4311,7 +4780,7 @@ function reportPlannedWrites(res) {
4311
4780
  }
4312
4781
  }
4313
4782
  async function emitHostSkills(root, host, conflictOpts, interactive) {
4314
- const adapter = resolveAdapter3(host);
4783
+ const adapter = resolveAdapter4(host);
4315
4784
  const skillsDir = adapter.skillsDir?.({ root });
4316
4785
  if (skillsDir === void 0) {
4317
4786
  process.stderr.write(`host '${host}' has no skill emitter; skipping skills
@@ -4367,11 +4836,11 @@ var init_init = __esm({
4367
4836
  });
4368
4837
 
4369
4838
  // src/serve.ts
4370
- import { applyNoirEnv, loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
4839
+ import { applyNoirEnv, loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
4371
4840
  import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
4372
4841
  async function serve(opts) {
4373
4842
  applyNoirEnv(process.cwd());
4374
- const project = loadProjectInfo11(process.cwd());
4843
+ const project = loadProjectInfo13(process.cwd());
4375
4844
  if (opts.stdio) {
4376
4845
  await startStdioServer({ project, transport: "stdio", daemon: false });
4377
4846
  return;
@@ -4405,8 +4874,8 @@ __export(create_exports, {
4405
4874
  create: () => create
4406
4875
  });
4407
4876
  import { resolve } from "path";
4408
- import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
4409
- import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
4877
+ import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
4878
+ import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
4410
4879
  import { scaffold as scaffold2 } from "@noir-ai/create";
4411
4880
  import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
4412
4881
  async function create(dir, opts) {
@@ -4432,7 +4901,7 @@ async function create(dir, opts) {
4432
4901
  return res;
4433
4902
  }
4434
4903
  if (res.noop) return res;
4435
- const adapter = resolveAdapter4(host);
4904
+ const adapter = resolveAdapter5(host);
4436
4905
  const skillsDir = adapter.skillsDir?.({ root });
4437
4906
  if (skillsDir === void 0) {
4438
4907
  process.stderr.write(`host '${host}' has no skill emitter; skipping skills
@@ -4457,7 +4926,7 @@ async function create(dir, opts) {
4457
4926
  }
4458
4927
  let projectInfo;
4459
4928
  try {
4460
- projectInfo = loadProjectInfo12(root);
4929
+ projectInfo = loadProjectInfo14(root);
4461
4930
  } catch {
4462
4931
  projectInfo = void 0;
4463
4932
  }
@@ -4483,9 +4952,9 @@ var sync_exports = {};
4483
4952
  __export(sync_exports, {
4484
4953
  sync: () => sync
4485
4954
  });
4486
- import { existsSync } from "fs";
4487
- import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
4488
- import { loadProjectInfo as loadProjectInfo13, paths } from "@noir-ai/core";
4955
+ import { existsSync as existsSync2 } from "fs";
4956
+ import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
4957
+ import { loadProjectInfo as loadProjectInfo15, paths } from "@noir-ai/core";
4489
4958
  import { scaffold as scaffold3 } from "@noir-ai/create";
4490
4959
  import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
4491
4960
  async function sync(root, opts = {}) {
@@ -4494,7 +4963,7 @@ async function sync(root, opts = {}) {
4494
4963
  const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
4495
4964
  let projectInfo;
4496
4965
  try {
4497
- projectInfo = loadProjectInfo13(root);
4966
+ projectInfo = loadProjectInfo15(root);
4498
4967
  } catch {
4499
4968
  projectInfo = void 0;
4500
4969
  }
@@ -4516,7 +4985,7 @@ async function sync(root, opts = {}) {
4516
4985
  if (res.written.length === 0 && res.identical.length > 0) {
4517
4986
  process.stderr.write("Nothing to sync (Noir-managed files are up to date).\n");
4518
4987
  }
4519
- const adapter = resolveAdapter5(host);
4988
+ const adapter = resolveAdapter6(host);
4520
4989
  const skillsDir = adapter.skillsDir?.({ root });
4521
4990
  if (skillsDir === void 0) {
4522
4991
  process.stderr.write(`host '${host}' has no skill emitter; skipping skills
@@ -4552,9 +5021,9 @@ async function sync(root, opts = {}) {
4552
5021
  }
4553
5022
  function resolveSyncHost(root, opts) {
4554
5023
  if (opts.host !== void 0) return opts.host;
4555
- if (!existsSync(paths.config(root))) return "claude";
5024
+ if (!existsSync2(paths.config(root))) return "claude";
4556
5025
  try {
4557
- return loadProjectInfo13(root).config.host;
5026
+ return loadProjectInfo15(root).config.host;
4558
5027
  } catch {
4559
5028
  return "claude";
4560
5029
  }
@@ -4785,30 +5254,75 @@ function createProgram() {
4785
5254
  throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
4786
5255
  });
4787
5256
  const daemonGrp = program2.command("daemon").description("control the Noir daemon");
4788
- daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").addOption(
5257
+ daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").option("--workspace <name>", "start a shared cross-repo workspace daemon instead").option("--force", "overwrite a non-Noir .mcp.json entry (--workspace only)").addOption(
4789
5258
  new Option("--_detached-child", "reserved: detached daemon child (internal)").hideHelp()
4790
5259
  ).action(async (...args) => {
4791
5260
  const cmd = trailingCmd(args);
4792
5261
  const g = cmd.optsWithGlobals();
4793
5262
  const detach = g.detach === true;
4794
5263
  const detachChild = g._detachedChild === true;
5264
+ const workspace = typeof g.workspace === "string" ? g.workspace : void 0;
4795
5265
  await daemonStart({
4796
5266
  ...toCliOptions(g),
4797
5267
  ...detach ? { detach } : {},
4798
- ...detachChild ? { detachChild } : {}
5268
+ ...detachChild ? { detachChild } : {},
5269
+ ...workspace !== void 0 ? { workspace } : {},
5270
+ ...g.force === true ? { force: true } : {}
4799
5271
  });
4800
5272
  });
5273
+ daemonGrp.command("join").description("join a shared workspace from this repo").argument("<name>", "workspace name").option("--force", "overwrite a non-Noir .mcp.json entry").action(async (...args) => {
5274
+ const cmd = trailingCmd(args);
5275
+ const g = cmd.optsWithGlobals();
5276
+ const name = typeof args[0] === "string" ? args[0] : "";
5277
+ await daemonJoin({ ...toCliOptions(g), name, ...g.force === true ? { force: true } : {} });
5278
+ });
4801
5279
  daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
4802
5280
  await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
4803
5281
  });
4804
5282
  daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
4805
5283
  await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
4806
5284
  });
4807
- daemonGrp.command("restart").description("stop then start the daemon").action(async (...args) => {
4808
- await daemonRestart(toCliOptions(trailingCmd(args).optsWithGlobals()));
5285
+ daemonGrp.command("restart").description("stop then start the daemon").option("--detach", "run the daemon in the background and exit").action(async (...args) => {
5286
+ const cmd = trailingCmd(args);
5287
+ const g = cmd.optsWithGlobals();
5288
+ await daemonRestart({
5289
+ ...toCliOptions(g),
5290
+ ...g.detach === true ? { detach: true } : {}
5291
+ });
4809
5292
  });
4810
5293
  daemonGrp.action(() => {
4811
- throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|stop|status|restart");
5294
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart");
5295
+ });
5296
+ const workspaceGrp = program2.command("workspace").description("shared cross-repo workspaces");
5297
+ workspaceGrp.command("list").description("list workspaces on this machine").action(async (...args) => {
5298
+ await workspaceList(toCliOptions(trailingCmd(args).optsWithGlobals()));
5299
+ });
5300
+ workspaceGrp.command("status").description("members + daemon liveness for a workspace").argument("[name]", "workspace name (defaults to this repo's joined workspace)").action(async (...args) => {
5301
+ const cmd = trailingCmd(args);
5302
+ const name = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
5303
+ await workspaceStatus({
5304
+ ...toCliOptions(cmd.optsWithGlobals()),
5305
+ ...name !== void 0 ? { name } : {}
5306
+ });
5307
+ });
5308
+ workspaceGrp.command("leave").description("leave the workspace this repo joined (restore stdio)").option("--force", "overwrite a non-Noir .mcp.json entry").action(async (...args) => {
5309
+ const cmd = trailingCmd(args);
5310
+ const g = cmd.optsWithGlobals();
5311
+ await workspaceLeave({
5312
+ ...toCliOptions(g),
5313
+ ...g.force === true ? { force: true } : {}
5314
+ });
5315
+ });
5316
+ workspaceGrp.command("stop").description("stop a workspace daemon (membership retained)").argument("[name]", "workspace name (defaults to this repo's joined workspace)").action(async (...args) => {
5317
+ const cmd = trailingCmd(args);
5318
+ const name = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
5319
+ await workspaceStop({
5320
+ ...toCliOptions(cmd.optsWithGlobals()),
5321
+ ...name !== void 0 ? { name } : {}
5322
+ });
5323
+ });
5324
+ workspaceGrp.action(() => {
5325
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir workspace list|status|leave|stop");
4812
5326
  });
4813
5327
  program2.command("doctor").description("environment + project health").option(
4814
5328
  "--dedup",
@@ -4904,10 +5418,23 @@ function createProgram() {
4904
5418
  ...limit === void 0 ? {} : { limit }
4905
5419
  });
4906
5420
  });
5421
+ memoryGrp.command("capture").description("distill a transcript/notes file (or stdin) into a memory (manual)").argument("[file]", "transcript/notes file (or pipe stdin)").option("--content <text>", "inline distilled content").option("--event-type <type>", "capture hook label (defaults to Stop)").action(async (...args) => {
5422
+ const cmd = trailingCmd(args);
5423
+ const g = cmd.optsWithGlobals();
5424
+ const file = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
5425
+ const content = typeof g.content === "string" ? g.content : void 0;
5426
+ const eventType = typeof g.eventType === "string" ? g.eventType : void 0;
5427
+ await memoryCapture({
5428
+ ...toCliOptions(g),
5429
+ ...file === void 0 ? {} : { file },
5430
+ ...content === void 0 ? {} : { content },
5431
+ ...eventType === void 0 ? {} : { eventType }
5432
+ });
5433
+ });
4907
5434
  memoryGrp.action(() => {
4908
5435
  throw new NoirCliError(
4909
5436
  EXIT.USAGE,
4910
- "Usage: noir memory recall|save|sessions|forget|consolidate"
5437
+ "Usage: noir memory recall|save|capture|sessions|forget|consolidate"
4911
5438
  );
4912
5439
  });
4913
5440
  const skillsGrp = program2.command("skills").description("builtin skills");
@@ -5201,6 +5728,7 @@ var init_bin = __esm({
5201
5728
  init_skills();
5202
5729
  init_status();
5203
5730
  init_task();
5731
+ init_workspace();
5204
5732
  init_init();
5205
5733
  init_output();
5206
5734
  init_run_profiles();
@@ -5240,11 +5768,17 @@ var init_bin = __esm({
5240
5768
  // src/daemon-client.ts
5241
5769
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
5242
5770
  import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
5243
- import { loadProjectInfo as loadProjectInfo14, NOIR_VERSION as NOIR_VERSION5 } from "@noir-ai/core";
5771
+ import {
5772
+ loadProjectInfo as loadProjectInfo16,
5773
+ NOIR_VERSION as NOIR_VERSION5,
5774
+ readWorkspaceMarker as readWorkspaceMarker2,
5775
+ workspaceStoreDbPath
5776
+ } from "@noir-ai/core";
5244
5777
  import {
5245
5778
  ensureDaemonRunning as ensureDaemonRunning3,
5246
- pidAlive as pidAlive2,
5779
+ pidAlive as pidAlive3,
5247
5780
  readDaemonRecord as readDaemonRecord2,
5781
+ readWorkspaceDaemonRecord as readWorkspaceDaemonRecord2,
5248
5782
  resolveGateConfig
5249
5783
  } from "@noir-ai/daemon";
5250
5784
  import { createMemoryEngine } from "@noir-ai/memory";
@@ -5271,7 +5805,7 @@ async function probeDaemon(opts = {}) {
5271
5805
  if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
5272
5806
  return { running: false };
5273
5807
  }
5274
- if (!pidAlive2(rec.pid)) {
5808
+ if (!pidAlive3(rec.pid)) {
5275
5809
  if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
5276
5810
  `);
5277
5811
  return { running: false };
@@ -5289,7 +5823,7 @@ async function probeDaemon(opts = {}) {
5289
5823
  const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
5290
5824
  const expectedProject = opts.project?.id ?? (() => {
5291
5825
  try {
5292
- return loadProjectInfo14(process.cwd()).id;
5826
+ return loadProjectInfo16(process.cwd()).id;
5293
5827
  } catch {
5294
5828
  return void 0;
5295
5829
  }
@@ -5319,7 +5853,7 @@ async function probeDaemon(opts = {}) {
5319
5853
  async function resolveDaemon(opts) {
5320
5854
  let project;
5321
5855
  try {
5322
- project = opts.project ?? loadProjectInfo14(process.cwd());
5856
+ project = opts.project ?? loadProjectInfo16(process.cwd());
5323
5857
  } catch {
5324
5858
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5325
5859
  }
@@ -5415,7 +5949,7 @@ async function callDaemonTool(opts, name, args = {}) {
5415
5949
  async function withInProcessRead(opts, fn) {
5416
5950
  let project;
5417
5951
  try {
5418
- project = opts.project ?? loadProjectInfo14(process.cwd());
5952
+ project = opts.project ?? loadProjectInfo16(process.cwd());
5419
5953
  } catch {
5420
5954
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5421
5955
  }
@@ -5458,6 +5992,91 @@ async function withInProcessRead(opts, fn) {
5458
5992
  });
5459
5993
  }
5460
5994
  }
5995
+ function workspaceRoutingTarget(root) {
5996
+ const name = readWorkspaceMarker2(root);
5997
+ if (name === null) return null;
5998
+ try {
5999
+ return { name, projectId: loadProjectInfo16(root).id };
6000
+ } catch {
6001
+ return null;
6002
+ }
6003
+ }
6004
+ async function probeWorkspaceDaemon(routing) {
6005
+ const rec = readWorkspaceDaemonRecord2(routing.name);
6006
+ if (rec === null || !pidAlive3(rec.pid)) return { running: false };
6007
+ try {
6008
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
6009
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
6010
+ });
6011
+ if (!res.ok) return { running: false };
6012
+ const body = await res.json().catch(() => null);
6013
+ if (body?.ok !== true || body.workspace !== routing.name || body.pid !== rec.pid) {
6014
+ return { running: false };
6015
+ }
6016
+ return { running: true, pid: rec.pid, port: rec.port };
6017
+ } catch {
6018
+ return { running: false };
6019
+ }
6020
+ }
6021
+ async function withWorkspaceDaemon(opts, routing, fn) {
6022
+ const probe = await probeWorkspaceDaemon(routing);
6023
+ if (!probe.running || probe.port === void 0) {
6024
+ fail(
6025
+ EXIT.DAEMON_DOWN,
6026
+ `workspace ${JSON.stringify(routing.name)} daemon is not running \u2014 start it with \`noir daemon start --workspace ${routing.name}\` (or run \`noir workspace status\`).`,
6027
+ opts
6028
+ );
6029
+ }
6030
+ const url = `http://127.0.0.1:${probe.port}/mcp?p=${routing.projectId}`;
6031
+ let client;
6032
+ try {
6033
+ client = new Client(
6034
+ { name: "noir-cli", version: NOIR_VERSION5 },
6035
+ { versionNegotiation: { mode: "auto" } }
6036
+ );
6037
+ const connected = client;
6038
+ await connectClient(connected, url, opts);
6039
+ return await fn(buildCaller(connected, opts));
6040
+ } finally {
6041
+ if (client) {
6042
+ await client.close().catch(() => {
6043
+ });
6044
+ }
6045
+ }
6046
+ }
6047
+ async function withWorkspaceMemoryRead(opts, routing, project, fn) {
6048
+ let store;
6049
+ try {
6050
+ store = await openStore({
6051
+ projectId: `ws-${routing.name}`,
6052
+ root: project.root,
6053
+ dbPath: workspaceStoreDbPath(routing.name),
6054
+ readonly: true
6055
+ });
6056
+ } catch (err) {
6057
+ fail(
6058
+ EXIT.DAEMON_DOWN,
6059
+ `workspace ${JSON.stringify(routing.name)} store is unavailable (${err instanceof Error ? err.message : String(err)}) \u2014 start its daemon with \`noir daemon start --workspace ${routing.name}\` (or run \`noir workspace status\`).`,
6060
+ opts
6061
+ );
6062
+ }
6063
+ const embedderCfg = resolveEmbedderConfig(project.config.context);
6064
+ const embed = createEmbedFn(embedderCfg).embed;
6065
+ const memory = createMemoryEngine({
6066
+ store,
6067
+ root: project.root,
6068
+ projectId: `ws-${routing.name}`,
6069
+ embed,
6070
+ storeDegraded: true,
6071
+ softForget: true
6072
+ });
6073
+ try {
6074
+ return await fn(memory);
6075
+ } finally {
6076
+ await store.close().catch(() => {
6077
+ });
6078
+ }
6079
+ }
5461
6080
  var DAEMON_DOWN_HINT, PROBE_TIMEOUT_MS;
5462
6081
  var init_daemon_client = __esm({
5463
6082
  "src/daemon-client.ts"() {
@@ -5470,21 +6089,21 @@ var init_daemon_client = __esm({
5470
6089
 
5471
6090
  // src/commands/doctor.ts
5472
6091
  import { spawnSync as spawnSync3 } from "child_process";
5473
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
5474
- import { dirname, join as join5, relative } from "path";
6092
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
6093
+ import { dirname, join as join6, relative } from "path";
5475
6094
  import { fileURLToPath } from "url";
5476
- import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
6095
+ import { resolveAdapter as resolveAdapter7 } from "@noir-ai/adapters";
5477
6096
  import {
5478
6097
  detectActiveMethod as detectActiveMethod4,
5479
6098
  latestVersionFromCache as latestVersionFromCache2,
5480
- loadProjectInfo as loadProjectInfo15,
6099
+ loadProjectInfo as loadProjectInfo17,
5481
6100
  NOIR_VERSION as NOIR_VERSION6,
5482
6101
  paths as paths2,
5483
6102
  readInstallRecord as readInstallRecord4,
5484
6103
  readUpdateCache as readUpdateCache3
5485
6104
  } from "@noir-ai/core";
5486
6105
  import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
5487
- import { pidAlive as pidAlive3, readDaemonRecord as readDaemonRecord3 } from "@noir-ai/daemon";
6106
+ import { pidAlive as pidAlive4, readDaemonRecord as readDaemonRecord3 } from "@noir-ai/daemon";
5488
6107
  import { resolveModelConfig } from "@noir-ai/model";
5489
6108
  async function probeOnnx() {
5490
6109
  try {
@@ -5521,7 +6140,7 @@ async function checkRuntime(checks) {
5521
6140
  }
5522
6141
  function checkConfig(root) {
5523
6142
  const configPath = paths2.config(root);
5524
- if (!existsSync2(configPath)) {
6143
+ if (!existsSync3(configPath)) {
5525
6144
  return {
5526
6145
  project: void 0,
5527
6146
  result: {
@@ -5532,7 +6151,7 @@ function checkConfig(root) {
5532
6151
  };
5533
6152
  }
5534
6153
  try {
5535
- const project = loadProjectInfo15(root);
6154
+ const project = loadProjectInfo17(root);
5536
6155
  return {
5537
6156
  project,
5538
6157
  result: {
@@ -5558,7 +6177,7 @@ async function checkDaemon(checks) {
5558
6177
  });
5559
6178
  return;
5560
6179
  }
5561
- if (!pidAlive3(rec.pid)) {
6180
+ if (!pidAlive4(rec.pid)) {
5562
6181
  checks.push({
5563
6182
  name: "daemon",
5564
6183
  status: "warn",
@@ -5613,7 +6232,7 @@ async function checkStore(checks, project, root) {
5613
6232
  return;
5614
6233
  }
5615
6234
  const dbPath = paths2.storeDb(root, project.id);
5616
- if (!existsSync2(dbPath)) {
6235
+ if (!existsSync3(dbPath)) {
5617
6236
  checks.push({
5618
6237
  name: "store",
5619
6238
  status: "warn",
@@ -5642,7 +6261,7 @@ async function checkStore(checks, project, root) {
5642
6261
  }
5643
6262
  }
5644
6263
  function checkNoirEnv(checks, root) {
5645
- const path = join5(paths2.noirDir(root), ".env");
6264
+ const path = join6(paths2.noirDir(root), ".env");
5646
6265
  let st;
5647
6266
  try {
5648
6267
  st = statSync(path);
@@ -5752,7 +6371,7 @@ function checkRulesMdBudget(checks, root, project) {
5752
6371
  return null;
5753
6372
  }
5754
6373
  const rulesPath = paths2.rulesMd(root);
5755
- if (!existsSync2(rulesPath)) {
6374
+ if (!existsSync3(rulesPath)) {
5756
6375
  checks.push({
5757
6376
  name: "rules budget",
5758
6377
  status: "ok",
@@ -5762,7 +6381,7 @@ function checkRulesMdBudget(checks, root, project) {
5762
6381
  }
5763
6382
  let content;
5764
6383
  try {
5765
- content = readFileSync2(rulesPath, "utf8");
6384
+ content = readFileSync4(rulesPath, "utf8");
5766
6385
  } catch (e) {
5767
6386
  checks.push({
5768
6387
  name: "rules budget",
@@ -5806,7 +6425,7 @@ function checkHostArtifacts(checks, root, project) {
5806
6425
  return null;
5807
6426
  }
5808
6427
  const host = project.config.host;
5809
- const adapter = resolveAdapter6(host);
6428
+ const adapter = resolveAdapter7(host);
5810
6429
  const ectx = { root };
5811
6430
  const expected = [];
5812
6431
  const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
@@ -5816,7 +6435,7 @@ function checkHostArtifacts(checks, root, project) {
5816
6435
  if (host === "claude") expected.push("CLAUDE.md");
5817
6436
  else if (host === "gemini") expected.push("GEMINI.md");
5818
6437
  expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
5819
- const missing = expected.filter((p) => !existsSync2(joinRoot(root, p)));
6438
+ const missing = expected.filter((p) => !existsSync3(joinRoot(root, p)));
5820
6439
  const status2 = missing.length === 0 ? "ok" : "warn";
5821
6440
  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)`;
5822
6441
  checks.push({ name: "host artifacts", status: status2, detail });
@@ -5833,7 +6452,7 @@ function checkNestedNoir(checks, root) {
5833
6452
  ".noir/.npmignore",
5834
6453
  ".noir/.prettierignore"
5835
6454
  ];
5836
- const found = candidates.filter((rel) => existsSync2(join5(root, rel)));
6455
+ const found = candidates.filter((rel) => existsSync3(join6(root, rel)));
5837
6456
  const detected = found.length > 0;
5838
6457
  checks.push({
5839
6458
  name: "nested .noir",
@@ -5846,11 +6465,11 @@ function collectDedupCandidates(root) {
5846
6465
  const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
5847
6466
  const out = [];
5848
6467
  for (const rel of rels) {
5849
- const abs = join5(root, rel);
5850
- if (!existsSync2(abs)) continue;
6468
+ const abs = join6(root, rel);
6469
+ if (!existsSync3(abs)) continue;
5851
6470
  let text;
5852
6471
  try {
5853
- text = readFileSync2(abs, "utf8");
6472
+ text = readFileSync4(abs, "utf8");
5854
6473
  } catch {
5855
6474
  continue;
5856
6475
  }
@@ -5907,13 +6526,13 @@ async function checkSemanticDupDoctor(checks, root, project, opts = {}) {
5907
6526
  function resolveWorkspacePackagesDir() {
5908
6527
  let here = dirname(fileURLToPath(import.meta.url));
5909
6528
  for (let i = 0; i < 8; i++) {
5910
- const pkgPath = join5(here, "package.json");
5911
- if (existsSync2(pkgPath)) {
6529
+ const pkgPath = join6(here, "package.json");
6530
+ if (existsSync3(pkgPath)) {
5912
6531
  try {
5913
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
6532
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5914
6533
  if (pkg.name === "@noir-ai/cli") {
5915
6534
  const packagesDir = dirname(here);
5916
- if (existsSync2(join5(dirname(packagesDir), "pnpm-workspace.yaml"))) {
6535
+ if (existsSync3(join6(dirname(packagesDir), "pnpm-workspace.yaml"))) {
5917
6536
  return packagesDir;
5918
6537
  }
5919
6538
  return null;
@@ -5958,7 +6577,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
5958
6577
  }
5959
6578
  let entries = [];
5960
6579
  try {
5961
- entries = readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
6580
+ entries = readdirSync2(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
5962
6581
  } catch (e) {
5963
6582
  checks.push({
5964
6583
  name: "publish",
@@ -5971,11 +6590,11 @@ function checkPublish(checks, packagesDir, opts = {}) {
5971
6590
  let checked = 0;
5972
6591
  let cliDir = null;
5973
6592
  for (const name of entries) {
5974
- const pkgPath = join5(packagesDir, name, "package.json");
5975
- if (!existsSync2(pkgPath)) continue;
6593
+ const pkgPath = join6(packagesDir, name, "package.json");
6594
+ if (!existsSync3(pkgPath)) continue;
5976
6595
  let pkg;
5977
6596
  try {
5978
- pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
6597
+ pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5979
6598
  } catch (e) {
5980
6599
  issues.push(`${name}/package.json: unparseable (${msg(e)})`);
5981
6600
  checked++;
@@ -5997,7 +6616,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
5997
6616
  if (pkg.bin == null || typeof pkg.bin === "object" && Object.keys(pkg.bin).length === 0) {
5998
6617
  issues.push(`${name}/package.json: bin missing (cli package must declare a bin)`);
5999
6618
  }
6000
- cliDir = join5(packagesDir, name);
6619
+ cliDir = join6(packagesDir, name);
6001
6620
  }
6002
6621
  }
6003
6622
  let packNote = "";