@noir-ai/cli 1.14.0-beta.1 → 1.15.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/bin.js CHANGED
@@ -635,16 +635,36 @@ function spinner(text = "", opts = {}) {
635
635
  }
636
636
  return new OraSpinner(text);
637
637
  }
638
+ function failureEnvelope(exitCode, message) {
639
+ return `${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
640
+ `;
641
+ }
638
642
  function fail(exitCode, message, opts = {}) {
639
643
  if (isJsonMode(opts)) {
640
- process.stdout.write(`${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
641
- `);
644
+ process.stdout.write(failureEnvelope(exitCode, message));
642
645
  } else if (message.length > 0) {
643
646
  process.stderr.write(`${message}
644
647
  `);
645
648
  }
646
649
  throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);
647
650
  }
651
+ function failAndExit(exitCode, message, opts = {}) {
652
+ process.exitCode = exitCode;
653
+ const jsonMode = isJsonMode(opts);
654
+ const text = jsonMode ? failureEnvelope(exitCode, message) : message.length > 0 ? `${message}
655
+ ` : "";
656
+ if (text.length === 0) {
657
+ process.exit(exitCode);
658
+ }
659
+ let left = false;
660
+ const leave = () => {
661
+ if (left) return;
662
+ left = true;
663
+ process.exit(exitCode);
664
+ };
665
+ (jsonMode ? process.stdout : process.stderr).write(text, leave);
666
+ setTimeout(leave, EXIT_FLUSH_MS);
667
+ }
648
668
  function requireInteractive(opts, hint) {
649
669
  if (!isInteractive(opts)) {
650
670
  fail(
@@ -685,7 +705,7 @@ function handleError(err) {
685
705
  }
686
706
  process.exitCode = inferExitCode(err);
687
707
  }
688
- var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner;
708
+ var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner, EXIT_FLUSH_MS;
689
709
  var init_output = __esm({
690
710
  "src/output.ts"() {
691
711
  "use strict";
@@ -764,6 +784,7 @@ var init_output = __esm({
764
784
  this.handle.info(text);
765
785
  }
766
786
  };
787
+ EXIT_FLUSH_MS = 250;
767
788
  }
768
789
  });
769
790
 
@@ -882,8 +903,8 @@ async function contextIndex(opts) {
882
903
  }
883
904
  renderIndex(data, opts.paths, opts);
884
905
  }
885
- function renderIndex(data, paths3, opts) {
886
- const scope = paths3 && paths3.length > 0 ? paths3.join(", ") : ".";
906
+ function renderIndex(data, paths5, opts) {
907
+ const scope = paths5 && paths5.length > 0 ? paths5.join(", ") : ".";
887
908
  const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
888
909
  log(`context index \u2014 ${scope}${flag}`, opts);
889
910
  definitionList(
@@ -1604,7 +1625,13 @@ import {
1604
1625
  readInstallRecord,
1605
1626
  readUpdateCache
1606
1627
  } from "@noir-ai/core";
1607
- import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
1628
+ import {
1629
+ CURRENT_SCAFFOLD_VERSION,
1630
+ isStaleSeed,
1631
+ loadTemplate,
1632
+ readScaffoldVersion,
1633
+ render
1634
+ } from "@noir-ai/create";
1608
1635
  import { pidAlive as pidAlive4, readProjectDaemonRecord as readProjectDaemonRecord3 } from "@noir-ai/daemon";
1609
1636
  import { resolveModelConfig } from "@noir-ai/model";
1610
1637
  async function probeOnnx() {
@@ -1850,9 +1877,10 @@ function checkProvider(checks, project, sources) {
1850
1877
  for (const name of names) {
1851
1878
  const p = resolved.providers[name];
1852
1879
  if (!p) continue;
1853
- const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file";
1854
- const key = p.hasKey ? fromFile ? "key present (from .noir/.env)" : "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
1855
- if (!p.hasKey && p.apiKeyEnv) missing = true;
1880
+ const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file" || p.authTokenEnv !== void 0 && sources[p.authTokenEnv] === "file";
1881
+ const credentialName = p.apiKeyEnv ?? p.authTokenEnv;
1882
+ const key = p.hasKey ? fromFile ? "key present (from .noir/.env)" : "key present" : credentialName ? `missing ${credentialName}` : "anonymous";
1883
+ if (!p.hasKey && credentialName) missing = true;
1856
1884
  parts.push(`${name}/${p.model ?? "?"} (${key})`);
1857
1885
  }
1858
1886
  checks.push({
@@ -1888,6 +1916,23 @@ function checkScaffoldVersion(checks, root) {
1888
1916
  checks.push({ name: "scaffold version", status: status2, detail });
1889
1917
  return { onDisk, current, drift };
1890
1918
  }
1919
+ function checkSeedDrift(checks, root) {
1920
+ const seedPath = join2(root, ".noir", ".env.example");
1921
+ let bytes;
1922
+ try {
1923
+ bytes = readFileSync2(seedPath, "utf8");
1924
+ } catch {
1925
+ return;
1926
+ }
1927
+ const currentRender = render(loadTemplate("env.example.tmpl"), {});
1928
+ if (isStaleSeed("envExample", bytes, currentRender)) {
1929
+ checks.push({
1930
+ name: "doc seed",
1931
+ status: "warn",
1932
+ detail: `.noir/.env.example is an unedited copy of an older Noir \u2014 run \`noir init --upgrade\` to refresh it`
1933
+ });
1934
+ }
1935
+ }
1891
1936
  function checkRulesMdBudget(checks, root, project) {
1892
1937
  if (!project) {
1893
1938
  checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
@@ -2222,6 +2267,7 @@ async function doctor(opts = {}) {
2222
2267
  checkProvider(checks, project, noirEnv.sources);
2223
2268
  checkNoirEnv(checks, root, noirEnv);
2224
2269
  const scaffold4 = checkScaffoldVersion(checks, root);
2270
+ checkSeedDrift(checks, root);
2225
2271
  const rules = checkRulesMdBudget(checks, root, project);
2226
2272
  const host = checkHostArtifacts(checks, root, project);
2227
2273
  checkNestedNoir(checks, root);
@@ -2329,12 +2375,12 @@ function describeSource(row) {
2329
2375
  return row.shadowed === true ? ".noir/.env (shadows environment)" : ".noir/.env";
2330
2376
  }
2331
2377
  async function env(opts = {}) {
2332
- const report = resolveEnv(process.cwd(), ambientBaseline ?? process.env);
2378
+ const report2 = resolveEnv(process.cwd(), ambientBaseline ?? process.env);
2333
2379
  if (opts.json === true) {
2334
- json({ ok: true, data: { vars: report.vars, warnings: report.warnings } });
2380
+ json({ ok: true, data: { vars: report2.vars, warnings: report2.warnings } });
2335
2381
  return;
2336
2382
  }
2337
- if (report.vars.length === 0) {
2383
+ if (report2.vars.length === 0) {
2338
2384
  info(
2339
2385
  "nothing to report \u2014 .noir/.env defines no keys in effect and no Noir-relevant environment variable is set",
2340
2386
  opts
@@ -2342,10 +2388,10 @@ async function env(opts = {}) {
2342
2388
  return;
2343
2389
  }
2344
2390
  table(
2345
- report.vars.map((row) => ({
2391
+ report2.vars.map((row) => ({
2346
2392
  KEY: row.key,
2347
2393
  SOURCE: describeSource(row),
2348
- VALUE: report.shapes[row.key] ?? ""
2394
+ VALUE: report2.shapes[row.key] ?? ""
2349
2395
  })),
2350
2396
  ["KEY", "SOURCE", "VALUE"],
2351
2397
  opts
@@ -2363,6 +2409,9 @@ var init_env = __esm({
2363
2409
  "COHERE_API_KEY",
2364
2410
  "OLLAMA_BASE_URL",
2365
2411
  "ANTHROPIC_API_KEY",
2412
+ "ANTHROPIC_AUTH_TOKEN",
2413
+ "ANTHROPIC_BASE_URL",
2414
+ "API_TIMEOUT_MS",
2366
2415
  "NOIR_PROFILE"
2367
2416
  ];
2368
2417
  SHORT_VALUE_MAX = 8;
@@ -3417,7 +3466,7 @@ async function taskVerify(opts) {
3417
3466
  `${JSON.stringify({
3418
3467
  ok: false,
3419
3468
  error: { code: EXIT.ERROR, message: `task verify: ${detail}` },
3420
- // S9 `{ok:false, error}` + a payload — recovery context + evidence nest
3469
+ // `{ok:false, error}` + a payload — recovery context + evidence nest
3421
3470
  // under `data`, not as top-level siblings (same envelope shape as the
3422
3471
  // success path; consumers read `data` for the full picture).
3423
3472
  data: {
@@ -3535,7 +3584,7 @@ var init_task = __esm({
3535
3584
  init_output();
3536
3585
  init_theme();
3537
3586
  PHASE_SKILL = {
3538
- // C3 curation (2026-08-10): intake+clarify merged into brainstorming;
3587
+ // Skill curation: intake+clarify merged into brainstorming;
3539
3588
  // execute → executing-plans; verify → verifying; document → wrap.
3540
3589
  intake: "noir-brainstorming",
3541
3590
  clarify: "noir-brainstorming",
@@ -3804,6 +3853,10 @@ function capitalizeFirst(s) {
3804
3853
  function hasAction(cmd) {
3805
3854
  return cmd._actionHandler != null;
3806
3855
  }
3856
+ function requiredArgLabel(cmd) {
3857
+ const required = cmd.registeredArguments.find((arg) => arg.required);
3858
+ return required?.name();
3859
+ }
3807
3860
  function collectLeaves(cmd, path, out) {
3808
3861
  const children = cmd.commands;
3809
3862
  const isLeaf = hasAction(cmd) && children.length === 0;
@@ -3818,7 +3871,8 @@ function collectLeaves(cmd, path, out) {
3818
3871
  description,
3819
3872
  category: argv[0] ?? "",
3820
3873
  keywords: argv,
3821
- destructive: isDestructive(argv)
3874
+ destructive: isDestructive(argv),
3875
+ needsArg: requiredArgLabel(cmd)
3822
3876
  });
3823
3877
  return;
3824
3878
  }
@@ -3931,7 +3985,15 @@ var init_sections = __esm({
3931
3985
  label: "Save memory",
3932
3986
  hint: "save an observation \xB7 \u26A0 destructive",
3933
3987
  destructive: true,
3934
- needsArg: { prompt: "Memory content:", placeholder: "what you want to remember" }
3988
+ // The value travels as `--content <text>` (memory save declares no
3989
+ // positional), so the flag rides in the dispatch prefix and the
3990
+ // collected value is appended after it.
3991
+ dispatch: ["memory", "save", "--content"],
3992
+ needsArg: {
3993
+ label: "content",
3994
+ prompt: "Memory content:",
3995
+ placeholder: "what you want to remember"
3996
+ }
3935
3997
  },
3936
3998
  {
3937
3999
  id: "memory sessions",
@@ -3955,8 +4017,21 @@ var init_sections = __esm({
3955
4017
  {
3956
4018
  id: "workflow",
3957
4019
  label: "Workflow",
3958
- hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
4020
+ hint: "run \xB7 next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
3959
4021
  items: [
4022
+ {
4023
+ id: "run",
4024
+ label: "Ask the host",
4025
+ hint: "ask the host agent a question \xB7 \u26A0 destructive",
4026
+ destructive: true,
4027
+ // The prompt is not a required argument (the command also takes flags
4028
+ // like --list-profiles), so the palette only learns to ask for it here.
4029
+ needsArg: {
4030
+ label: "prompt",
4031
+ prompt: "Prompt for the host:",
4032
+ placeholder: "e.g. summarise the auth flow"
4033
+ }
4034
+ },
3960
4035
  {
3961
4036
  id: "task next",
3962
4037
  label: "Next task",
@@ -4808,6 +4883,15 @@ var init_release = __esm({
4808
4883
  }
4809
4884
  });
4810
4885
 
4886
+ // src/run-timing.ts
4887
+ var INTERRUPT_GRACE_MS;
4888
+ var init_run_timing = __esm({
4889
+ "src/run-timing.ts"() {
4890
+ "use strict";
4891
+ INTERRUPT_GRACE_MS = 5e3;
4892
+ }
4893
+ });
4894
+
4811
4895
  // src/shell-bridge.ts
4812
4896
  import { spawn } from "child_process";
4813
4897
  import { basename } from "path";
@@ -4842,7 +4926,7 @@ function shellKind(shell) {
4842
4926
  function probeCommand(name, shell, opts) {
4843
4927
  const kind = shellKind(shell);
4844
4928
  const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS2;
4845
- return new Promise((resolve2) => {
4929
+ return new Promise((resolve3) => {
4846
4930
  const script = kind ? PROBE_SCRIPTS[kind] : "";
4847
4931
  const flags = kind ? PROBE_FLAGS[kind] : "-lic";
4848
4932
  const probeArgs = kind === "fish" ? [name] : ["noirbridge", name];
@@ -4865,15 +4949,15 @@ function probeCommand(name, shell, opts) {
4865
4949
  } catch {
4866
4950
  }
4867
4951
  }
4868
- resolve2({ stdout, timedOut: true });
4952
+ resolve3({ stdout, timedOut: true });
4869
4953
  }, timeoutMs);
4870
4954
  child.on("error", () => {
4871
4955
  clearTimeout(timer);
4872
- resolve2({ stdout, timedOut: false });
4956
+ resolve3({ stdout, timedOut: false });
4873
4957
  });
4874
4958
  child.on("close", () => {
4875
4959
  clearTimeout(timer);
4876
- resolve2({ stdout, timedOut: false });
4960
+ resolve3({ stdout, timedOut: false });
4877
4961
  });
4878
4962
  });
4879
4963
  }
@@ -4969,18 +5053,23 @@ function usageFrom(v) {
4969
5053
  if (input === void 0 && output === void 0 && cache === void 0) return void 0;
4970
5054
  return { inputTokens: input, outputTokens: output, cacheReadTokens: cache };
4971
5055
  }
4972
- function messageText(message) {
4973
- if (!message) return void 0;
5056
+ function messageContent(message) {
5057
+ if (!message) return {};
4974
5058
  const content = message.content;
4975
- if (typeof content === "string") return content;
4976
- if (Array.isArray(content)) {
4977
- const parts = content.map((block) => {
4978
- const b = asObj(block);
4979
- return b && typeof b.text === "string" ? b.text : null;
4980
- }).filter((t) => t !== null);
4981
- if (parts.length > 0) return parts.join("");
5059
+ if (typeof content === "string") return { text: content };
5060
+ if (!Array.isArray(content)) return {};
5061
+ const parts = [];
5062
+ const tools = [];
5063
+ for (const block of content) {
5064
+ const b = asObj(block);
5065
+ if (!b) continue;
5066
+ if (typeof b.text === "string") parts.push(b.text);
5067
+ else if (b.type === "tool_use" && typeof b.name === "string") tools.push(b.name);
4982
5068
  }
4983
- return void 0;
5069
+ return {
5070
+ ...parts.length > 0 ? { text: parts.join("") } : {},
5071
+ ...tools.length > 0 ? { tools } : {}
5072
+ };
4984
5073
  }
4985
5074
  function parseStreamLine(line) {
4986
5075
  const trimmed = line.trim();
@@ -4999,15 +5088,14 @@ function normalizeStreamEvent(raw) {
4999
5088
  if (r.subtype === "init") {
5000
5089
  return { kind: "init", sessionId: str(r.session_id), model: str(r.model) };
5001
5090
  }
5002
- return { kind: "other" };
5091
+ return { kind: "other", subtype: str(r.subtype) ?? "system" };
5003
5092
  }
5004
5093
  if (type === "assistant") {
5005
5094
  const message = asObj(r.message);
5006
- return {
5007
- kind: "assistant",
5008
- messageId: message ? str(message.id) : void 0,
5009
- text: messageText(message),
5010
- usage: message ? usageFrom(message.usage) : void 0,
5095
+ const content = messageContent(message);
5096
+ const messageId = message ? str(message.id) : void 0;
5097
+ const usage = message ? usageFrom(message.usage) : void 0;
5098
+ const error2 = {
5011
5099
  // An API error message is a diagnostic, not the answer — flag it so the
5012
5100
  // caller never streams it as assistant text (optional-when-true keeps
5013
5101
  // exact-shape consumers of the existing union intact).
@@ -5016,6 +5104,20 @@ function normalizeStreamEvent(raw) {
5016
5104
  ...typeof r.error === "string" ? { errorCategory: r.error } : {}
5017
5105
  } : {}
5018
5106
  };
5107
+ if (content.text !== void 0) {
5108
+ return {
5109
+ kind: "assistant",
5110
+ messageId,
5111
+ text: content.text,
5112
+ usage,
5113
+ ...content.tools === void 0 ? {} : { tools: content.tools },
5114
+ ...error2
5115
+ };
5116
+ }
5117
+ if (content.tools !== void 0) {
5118
+ return { kind: "tool", name: content.tools[0], messageId, usage };
5119
+ }
5120
+ return { kind: "assistant", messageId, usage, ...error2 };
5019
5121
  }
5020
5122
  if (type === "result") {
5021
5123
  return {
@@ -5027,7 +5129,33 @@ function normalizeStreamEvent(raw) {
5027
5129
  usage: usageFrom(r.usage)
5028
5130
  };
5029
5131
  }
5030
- return { kind: "other" };
5132
+ if (type === "stream_event") {
5133
+ return normalizeStreamEventInner(asObj(r.event));
5134
+ }
5135
+ return { kind: "other", ...typeof type === "string" ? { subtype: type } : {} };
5136
+ }
5137
+ function normalizeStreamEventInner(inner) {
5138
+ if (!inner) return { kind: "other", subtype: "stream_event" };
5139
+ const innerType = str(inner.type);
5140
+ if (innerType === "content_block_start") {
5141
+ const block = asObj(inner.content_block);
5142
+ const name = block ? str(block.name) : void 0;
5143
+ if (block && str(block.type) === "tool_use" && name !== void 0) {
5144
+ return { kind: "tool", name };
5145
+ }
5146
+ }
5147
+ if (innerType === "content_block_delta") {
5148
+ const delta = asObj(inner.delta);
5149
+ const text = delta ? str(delta.text) : void 0;
5150
+ if (delta && delta.type === "text_delta" && text !== void 0) {
5151
+ return { kind: "delta", text };
5152
+ }
5153
+ }
5154
+ return { kind: "other", subtype: innerType ?? "stream_event" };
5155
+ }
5156
+ function signalChild(child, signal) {
5157
+ if (child.pid === void 0) return;
5158
+ child.kill(signal);
5031
5159
  }
5032
5160
  function runHost(opts) {
5033
5161
  const spec = resolveHostRun(opts.host, opts.customBinary);
@@ -5043,12 +5171,13 @@ function runHost(opts) {
5043
5171
  false
5044
5172
  );
5045
5173
  }
5046
- function spawnAndConsume(binary, args, opts, shellRun) {
5047
- return new Promise((resolve2, reject) => {
5174
+ function spawnAndConsume(binary, args, opts, shellRun, bridged = false) {
5175
+ return new Promise((resolve3, reject) => {
5048
5176
  const child = spawn2(binary, [...args], {
5049
5177
  stdio: ["ignore", "pipe", "pipe"],
5050
5178
  ...opts.env ? { env: opts.env } : {}
5051
5179
  });
5180
+ opts.onChild?.(child);
5052
5181
  const reducer = new UsageReducer();
5053
5182
  let eventCount = 0;
5054
5183
  let stderrBuf = "";
@@ -5056,10 +5185,40 @@ function spawnAndConsume(binary, args, opts, shellRun) {
5056
5185
  let errorText;
5057
5186
  let errorCategory;
5058
5187
  let spawnError = null;
5188
+ const { signal } = opts;
5189
+ let resend;
5190
+ let offersLeft = Math.ceil(BRIDGE_POLITE_WINDOW_MS / BRIDGE_POLITE_RESEND_MS);
5191
+ const stopResend = () => {
5192
+ if (resend === void 0) return;
5193
+ clearTimeout(resend);
5194
+ resend = void 0;
5195
+ };
5196
+ const offerAgain = () => {
5197
+ if (offersLeft <= 0) return;
5198
+ offersLeft -= 1;
5199
+ resend = setTimeout(() => {
5200
+ resend = void 0;
5201
+ signalChild(child, "SIGTERM");
5202
+ offerAgain();
5203
+ }, BRIDGE_POLITE_RESEND_MS);
5204
+ };
5205
+ const onAbort = () => {
5206
+ signalChild(child, "SIGTERM");
5207
+ if (bridged) offerAgain();
5208
+ };
5209
+ if (signal) {
5210
+ if (signal.aborted) onAbort();
5211
+ else signal.addEventListener("abort", onAbort, { once: true });
5212
+ }
5213
+ const detachAbort = () => {
5214
+ signal?.removeEventListener("abort", onAbort);
5215
+ stopResend();
5216
+ };
5059
5217
  child.on("error", (err) => {
5060
5218
  spawnError = err;
5219
+ detachAbort();
5061
5220
  if (!shellRun && err.code === "ENOENT") {
5062
- void shellFallback(binary, args, opts, resolve2, reject, err);
5221
+ void shellFallback(binary, args, opts, resolve3, reject, err);
5063
5222
  return;
5064
5223
  }
5065
5224
  reject(err);
@@ -5086,41 +5245,43 @@ function spawnAndConsume(binary, args, opts, shellRun) {
5086
5245
  child.stderr.on("data", (chunk) => {
5087
5246
  stderrBuf += chunk;
5088
5247
  });
5089
- child.on("close", (code, signal) => {
5248
+ child.on("close", (code, signal2) => {
5249
+ detachAbort();
5090
5250
  if (spawnError !== null) return;
5091
- const killed = code === null && signal != null;
5092
- resolve2({
5093
- exitCode: killed ? 128 + (signalNumber(signal) ?? 0) : code ?? 0,
5251
+ const killed = code === null && signal2 != null;
5252
+ resolve3({
5253
+ exitCode: killed ? 128 + (signalNumber(signal2) ?? 0) : code ?? 0,
5094
5254
  usage: reducer.snapshot(),
5095
5255
  eventCount,
5096
5256
  stderr: stderrBuf,
5097
5257
  isError: errored || killed,
5098
- errorText: killed && errorText === void 0 ? `terminated by signal ${signal}` : errorText,
5258
+ errorText: killed && errorText === void 0 ? `terminated by signal ${signal2}` : errorText,
5099
5259
  errorCategory
5100
5260
  });
5101
5261
  });
5102
5262
  });
5103
5263
  }
5104
- async function shellFallback(binary, args, opts, resolve2, reject, originalErr) {
5264
+ async function shellFallback(binary, args, opts, resolve3, reject, originalErr) {
5105
5265
  try {
5106
5266
  const res = await resolveCommandViaShell(binary, { env: opts.env ?? process.env });
5107
5267
  if (res.kind === "path") {
5108
- resolve2(await spawnAndConsume(res.path, args, opts, true));
5268
+ resolve3(await spawnAndConsume(res.path, args, opts, true));
5109
5269
  return;
5110
5270
  }
5111
5271
  if (res.kind === "alias" || res.kind === "function") {
5112
5272
  const bridge = buildBridgeArgs(binary, args, res.shell);
5113
- resolve2(await spawnAndConsume(bridge.binary, bridge.args, opts, true));
5273
+ resolve3(await spawnAndConsume(bridge.binary, bridge.args, opts, true, true));
5114
5274
  return;
5115
5275
  }
5116
5276
  } catch {
5117
5277
  }
5118
5278
  reject(originalErr);
5119
5279
  }
5120
- var HOST_BINARY, HOST_FLAGS, UsageReducer;
5280
+ var HOST_BINARY, HOST_FLAGS, UsageReducer, BRIDGE_POLITE_RESEND_MS, BRIDGE_POLITE_WINDOW_MS;
5121
5281
  var init_orchestrator = __esm({
5122
5282
  "src/orchestrator.ts"() {
5123
5283
  "use strict";
5284
+ init_run_timing();
5124
5285
  init_shell_bridge();
5125
5286
  HOST_BINARY = {
5126
5287
  claude: "claude",
@@ -5131,7 +5292,7 @@ var init_orchestrator = __esm({
5131
5292
  // emits AGENTS.md — a file host, not a spawnable CLI
5132
5293
  };
5133
5294
  HOST_FLAGS = {
5134
- claude: ["-p", "--output-format", "stream-json", "--verbose"],
5295
+ claude: ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages"],
5135
5296
  gemini: ["-p", "--output-format", "stream-json"],
5136
5297
  opencode: ["run"],
5137
5298
  cursor: ["-p"],
@@ -5145,27 +5306,29 @@ var init_orchestrator = __esm({
5145
5306
  turns = 0;
5146
5307
  /** Feed one normalized event into the reducer. */
5147
5308
  add(event) {
5148
- if (event.kind === "assistant" && event.messageId && event.usage) {
5149
- const key = event.messageId;
5150
- const input = event.usage.inputTokens ?? 0;
5151
- const output = event.usage.outputTokens ?? 0;
5152
- const prev = this.maxByMessage.get(key);
5153
- if (!prev) {
5154
- this.maxByMessage.set(key, { input, output });
5155
- this.input += input;
5156
- this.output += output;
5157
- } else {
5158
- this.input += Math.max(0, input - prev.input);
5159
- this.output += Math.max(0, output - prev.output);
5160
- this.maxByMessage.set(key, {
5161
- input: Math.max(prev.input, input),
5162
- output: Math.max(prev.output, output)
5163
- });
5164
- }
5165
- } else if (event.kind === "result") {
5309
+ if (event.kind === "result") {
5166
5310
  if (event.totalCostUsd !== void 0) this.costUsd = event.totalCostUsd;
5167
5311
  if (event.numTurns !== void 0) this.turns = event.numTurns;
5312
+ return;
5168
5313
  }
5314
+ const usage = event.kind === "assistant" || event.kind === "tool" ? event.usage : void 0;
5315
+ const key = event.kind === "assistant" || event.kind === "tool" ? event.messageId : void 0;
5316
+ if (!key || !usage) return;
5317
+ const input = usage.inputTokens ?? 0;
5318
+ const output = usage.outputTokens ?? 0;
5319
+ const prev = this.maxByMessage.get(key);
5320
+ if (!prev) {
5321
+ this.maxByMessage.set(key, { input, output });
5322
+ this.input += input;
5323
+ this.output += output;
5324
+ return;
5325
+ }
5326
+ this.input += Math.max(0, input - prev.input);
5327
+ this.output += Math.max(0, output - prev.output);
5328
+ this.maxByMessage.set(key, {
5329
+ input: Math.max(prev.input, input),
5330
+ output: Math.max(prev.output, output)
5331
+ });
5169
5332
  }
5170
5333
  /** The current accumulator snapshot. */
5171
5334
  snapshot() {
@@ -5177,11 +5340,362 @@ var init_orchestrator = __esm({
5177
5340
  };
5178
5341
  }
5179
5342
  };
5343
+ BRIDGE_POLITE_RESEND_MS = 250;
5344
+ BRIDGE_POLITE_WINDOW_MS = INTERRUPT_GRACE_MS;
5345
+ }
5346
+ });
5347
+
5348
+ // src/run-actions.ts
5349
+ import { writeFileSync as writeFileSync3 } from "fs";
5350
+ import { resolve } from "path";
5351
+ import { CommanderError as CommanderError2 } from "commander";
5352
+ function postRunActions(input) {
5353
+ const answer = input.answer.trim();
5354
+ const canContinue = input.canContinue ?? (input.sessionId !== void 0 && input.resume !== void 0);
5355
+ const options = [];
5356
+ if (answer.length > 0) {
5357
+ options.push({
5358
+ value: "memory",
5359
+ label: "Save answer to memory",
5360
+ hint: "distil it into this project's memory (needs the daemon)"
5361
+ });
5362
+ options.push({
5363
+ value: "research",
5364
+ label: "Add as task research",
5365
+ hint: "record it as a finding on the active task"
5366
+ });
5367
+ }
5368
+ options.push({
5369
+ value: "handoff",
5370
+ label: "Write a handoff artifact",
5371
+ hint: "snapshot the task, the next step and a seed for your host"
5372
+ });
5373
+ if (canContinue) {
5374
+ options.push({
5375
+ value: "resume",
5376
+ label: "Continue this session",
5377
+ hint: `ask ${input.host} a follow-up in the same session`,
5378
+ needsValue: {
5379
+ label: "follow-up prompt",
5380
+ placeholder: "e.g. now write the tests for that"
5381
+ }
5382
+ });
5383
+ }
5384
+ if (answer.length > 0) {
5385
+ options.push({
5386
+ value: "save",
5387
+ label: "Save answer to a file",
5388
+ hint: "write the answer text to a path you choose",
5389
+ needsValue: { label: "file path", placeholder: "e.g. notes/answer.md" }
5390
+ });
5391
+ }
5392
+ options.push({
5393
+ value: "dismiss",
5394
+ label: "Dismiss",
5395
+ hint: "nothing further \u2014 the run is already complete"
5396
+ });
5397
+ return options;
5398
+ }
5399
+ async function performPostRunAction(choice, input, value) {
5400
+ const label = ACTION_LABEL[choice];
5401
+ try {
5402
+ switch (choice) {
5403
+ case "memory":
5404
+ await saveToMemory(input);
5405
+ return { ok: true, message: "" };
5406
+ case "research":
5407
+ await recordAsResearch(input);
5408
+ return { ok: true, message: "" };
5409
+ case "handoff":
5410
+ await writeHandoff(input);
5411
+ return { ok: true, message: "" };
5412
+ case "resume":
5413
+ return await continueSession(input, value);
5414
+ case "save":
5415
+ return saveAnswerToFile(input, value);
5416
+ case "dismiss":
5417
+ return { ok: true, message: "" };
5418
+ }
5419
+ } catch (err) {
5420
+ const quoted = err instanceof CommanderError2 || err instanceof NoirCliError ? void 0 : `${label}: ${err instanceof Error ? err.message : String(err)}`;
5421
+ return {
5422
+ ok: false,
5423
+ message: `${label} did not complete \u2014 the run's result stands.`,
5424
+ ...quoted === void 0 ? {} : { detail: quoted }
5425
+ };
5426
+ }
5427
+ }
5428
+ async function offerPostRunActions(input) {
5429
+ if (!isInteractive(input.opts)) return;
5430
+ const options = postRunActions(input);
5431
+ if (options.length <= 1) return;
5432
+ const menu = await openMenu(options);
5433
+ if (menu === void 0) return;
5434
+ const chosen = options.find((o) => o.value === menu.choice);
5435
+ if (chosen === void 0) return;
5436
+ const value = chosen.needsValue === void 0 ? void 0 : await askForValue(menu.clack, chosen, input);
5437
+ if (chosen.needsValue !== void 0 && value === void 0) return;
5438
+ report(await performPostRunAction(menu.choice, input, value), input.opts);
5439
+ }
5440
+ function report(outcome, opts) {
5441
+ const caution = outcome.tone === "caution" || !outcome.ok && outcome.tone !== "confirm";
5442
+ if (outcome.detail !== void 0) warn(outcome.detail, opts);
5443
+ if (outcome.message.length === 0) return;
5444
+ if (caution) warn(outcome.message, opts);
5445
+ else success(outcome.message, opts);
5446
+ }
5447
+ async function openMenu(options) {
5448
+ try {
5449
+ const clack = await import("@clack/prompts");
5450
+ const choice = await clack.select({
5451
+ message: "The run finished. What next?",
5452
+ initialValue: "dismiss",
5453
+ // Spread because the prompt library takes a mutable list; the rows
5454
+ // themselves are the shared descriptors and are never rewritten.
5455
+ options: [...options]
5456
+ });
5457
+ if (clack.isCancel(choice)) return void 0;
5458
+ return { clack, choice };
5459
+ } catch {
5460
+ return void 0;
5461
+ }
5462
+ }
5463
+ async function askForValue(clack, option, input) {
5464
+ const needs = option.needsValue;
5465
+ if (needs === void 0) return void 0;
5466
+ const message = option.value === "save" ? `Write the answer to a file (the raw transcript is at ${input.transcript})` : `What should ${input.host} do next?`;
5467
+ try {
5468
+ const written = await clack.text({ message, placeholder: needs.placeholder });
5469
+ if (clack.isCancel(written)) return void 0;
5470
+ return String(written ?? "");
5471
+ } catch {
5472
+ return void 0;
5473
+ }
5474
+ }
5475
+ async function saveToMemory(input) {
5476
+ await memoryCapture({
5477
+ ...input.opts,
5478
+ content: input.answer,
5479
+ eventType: CAPTURE_EVENT
5480
+ });
5481
+ }
5482
+ async function recordAsResearch(input) {
5483
+ await taskResearchRecord({
5484
+ ...input.opts,
5485
+ type: "discovery",
5486
+ text: researchText(input.answer),
5487
+ source: RESEARCH_SOURCE
5488
+ });
5489
+ }
5490
+ function researchText(answer) {
5491
+ const condensed = answer.replace(/\s+/g, " ").trim();
5492
+ if (condensed.length <= RESEARCH_TEXT_LIMIT) return condensed;
5493
+ return `${condensed.slice(0, RESEARCH_TEXT_LIMIT - 1)}\u2026`;
5494
+ }
5495
+ async function writeHandoff(input) {
5496
+ await handoff({ ...input.opts, write: true });
5497
+ }
5498
+ async function continueSession(input, value) {
5499
+ const { sessionId, resume } = input;
5500
+ if (sessionId === void 0 || resume === void 0) return { ok: true, message: "" };
5501
+ const prompt = (value ?? "").trim();
5502
+ if (prompt.length === 0) {
5503
+ return {
5504
+ ok: true,
5505
+ tone: "caution",
5506
+ message: "No prompt given \u2014 the session was not continued."
5507
+ };
5508
+ }
5509
+ await resume(prompt, sessionId);
5510
+ return { ok: true, message: "" };
5511
+ }
5512
+ function saveAnswerToFile(input, value) {
5513
+ const fallback = `noir-run-${stamp2()}.md`;
5514
+ const chosen = (value ?? "").trim();
5515
+ const target = resolve(chosen.length > 0 ? chosen : fallback);
5516
+ const body = input.answer.endsWith("\n") ? input.answer : `${input.answer}
5517
+ `;
5518
+ try {
5519
+ writeFileSync3(target, body, { mode: 384 });
5520
+ } catch (err) {
5521
+ return {
5522
+ ok: false,
5523
+ tone: "caution",
5524
+ message: `could not write ${target}: ${err instanceof Error ? err.message : String(err)}`
5525
+ };
5526
+ }
5527
+ return { ok: true, message: `Answer written to ${target}` };
5528
+ }
5529
+ function stamp2() {
5530
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5531
+ }
5532
+ var CAPTURE_EVENT, RESEARCH_SOURCE, RESEARCH_TEXT_LIMIT, ACTION_LABEL;
5533
+ var init_run_actions = __esm({
5534
+ "src/run-actions.ts"() {
5535
+ "use strict";
5536
+ init_handoff();
5537
+ init_memory();
5538
+ init_task();
5539
+ init_output();
5540
+ CAPTURE_EVENT = "capture";
5541
+ RESEARCH_SOURCE = "noir run";
5542
+ RESEARCH_TEXT_LIMIT = 220;
5543
+ ACTION_LABEL = {
5544
+ memory: "Saving to memory",
5545
+ research: "Recording the finding",
5546
+ handoff: "Writing the handoff",
5547
+ resume: "Continuing the session",
5548
+ save: "Writing the file",
5549
+ dismiss: "Dismissing"
5550
+ };
5551
+ }
5552
+ });
5553
+
5554
+ // src/run-interrupt.ts
5555
+ import { constants as constants2 } from "os";
5556
+ function exitCodeForSignal(signal) {
5557
+ return 128 + (constants2.signals[signal] ?? 0);
5558
+ }
5559
+ function interruptedNotice(transcript) {
5560
+ return `interrupted \xB7 transcript: ${transcript}`;
5561
+ }
5562
+ var INTERRUPT_SIGNALS, RunInterrupt;
5563
+ var init_run_interrupt = __esm({
5564
+ "src/run-interrupt.ts"() {
5565
+ "use strict";
5566
+ init_orchestrator();
5567
+ init_run_timing();
5568
+ INTERRUPT_SIGNALS = ["SIGINT", "SIGTERM"];
5569
+ RunInterrupt = class {
5570
+ graceMs;
5571
+ exitNow;
5572
+ controller = new AbortController();
5573
+ /** The live child, or undefined between spawns. */
5574
+ child;
5575
+ /** The child the ladder already ran for — a repeat call is not a new ladder. */
5576
+ stopping;
5577
+ grace;
5578
+ signalName;
5579
+ stopRequested = false;
5580
+ forced = false;
5581
+ watching = false;
5582
+ /** The installed listeners, keyed by the signal each answers. */
5583
+ handlers = /* @__PURE__ */ new Map();
5584
+ constructor(opts = {}) {
5585
+ this.graceMs = opts.graceMs ?? INTERRUPT_GRACE_MS;
5586
+ this.exitNow = opts.exitNow ?? ((code) => {
5587
+ if (this.child !== void 0) signalChild(this.child, "SIGKILL");
5588
+ process.exit(code);
5589
+ });
5590
+ }
5591
+ /**
5592
+ * The cancel signal for the spawn. Aborting it is what a caller holding only
5593
+ * the signal can do; the ladder itself does not depend on it, which matters
5594
+ * because the child a run ends up driving is not always the first one spawned.
5595
+ */
5596
+ get signal() {
5597
+ return this.controller.signal;
5598
+ }
5599
+ /** The signal that stopped the run, or undefined when nothing did. */
5600
+ get interruptedBy() {
5601
+ return this.signalName;
5602
+ }
5603
+ /** True once a child had to be forced — it did not honour the polite signal. */
5604
+ get escalated() {
5605
+ return this.forced;
5606
+ }
5607
+ /**
5608
+ * Hand over the child a spawn produced. Called for every spawn of one run: a
5609
+ * command resolved through the shell bridge is its own child, and a stop that
5610
+ * arrived before that child existed has to land on it too.
5611
+ */
5612
+ track(child) {
5613
+ this.child = child;
5614
+ child.once("exit", () => this.forget(child));
5615
+ if (this.stopRequested) this.terminate();
5616
+ }
5617
+ /**
5618
+ * Stop the host. The polite signal goes to the child itself, and a child still
5619
+ * alive after the grace is forced. Repeat calls are the same call: the ladder
5620
+ * runs once per child, so a keystroke held down is not a stream of signals.
5621
+ */
5622
+ terminate() {
5623
+ this.stopRequested = true;
5624
+ if (!this.controller.signal.aborted) this.controller.abort();
5625
+ const child = this.child;
5626
+ if (child === void 0 || child.pid === void 0 || child === this.stopping) return;
5627
+ this.stopping = child;
5628
+ this.clearGrace();
5629
+ signalChild(child, "SIGTERM");
5630
+ this.grace = setTimeout(() => {
5631
+ this.grace = void 0;
5632
+ if (this.child !== child) return;
5633
+ this.forced = true;
5634
+ signalChild(child, "SIGKILL");
5635
+ }, this.graceMs);
5636
+ }
5637
+ /**
5638
+ * Skip the grace and force the host now. The "you asked twice" path: the
5639
+ * terminal leaves at once on a second signal, and the run screen forces and
5640
+ * returns to the dashboard rather than holding a keystroke while the grace
5641
+ * runs out. Does nothing when no stop is in flight.
5642
+ */
5643
+ forceNow() {
5644
+ const child = this.child;
5645
+ if (child === void 0 || child.pid === void 0 || child !== this.stopping) return false;
5646
+ this.forced = true;
5647
+ this.clearGrace();
5648
+ signalChild(child, "SIGKILL");
5649
+ return true;
5650
+ }
5651
+ /**
5652
+ * Install the terminal interrupt handlers. Only a run installs these, and only
5653
+ * for as long as it is driving the host: the process's own handling has to
5654
+ * come back afterwards, or a later command inherits an interrupt contract with
5655
+ * nothing left to interrupt.
5656
+ */
5657
+ watch() {
5658
+ if (this.watching) return;
5659
+ this.watching = true;
5660
+ for (const signal of INTERRUPT_SIGNALS) {
5661
+ const listener = () => this.onSignal(signal);
5662
+ this.handlers.set(signal, listener);
5663
+ process.on(signal, listener);
5664
+ }
5665
+ }
5666
+ /** Put the process's own handling back, whatever the run left behind. */
5667
+ unwatch() {
5668
+ if (!this.watching) return;
5669
+ this.watching = false;
5670
+ for (const [signal, listener] of this.handlers) process.off(signal, listener);
5671
+ this.handlers.clear();
5672
+ }
5673
+ onSignal(signal) {
5674
+ if (this.signalName !== void 0) {
5675
+ this.exitNow(exitCodeForSignal(signal));
5676
+ return;
5677
+ }
5678
+ this.signalName = signal;
5679
+ this.terminate();
5680
+ }
5681
+ /** The child is gone: stop tracking it and disarm its ladder. */
5682
+ forget(child) {
5683
+ if (this.child !== child) return;
5684
+ this.child = void 0;
5685
+ this.stopping = void 0;
5686
+ this.clearGrace();
5687
+ }
5688
+ clearGrace() {
5689
+ if (this.grace === void 0) return;
5690
+ clearTimeout(this.grace);
5691
+ this.grace = void 0;
5692
+ }
5693
+ };
5180
5694
  }
5181
5695
  });
5182
5696
 
5183
5697
  // src/run-profiles.ts
5184
- import { loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
5698
+ import { isDeniedEnvKey, loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
5185
5699
  function loadRunConfig(root) {
5186
5700
  try {
5187
5701
  return loadProjectInfo13(root).config;
@@ -5216,6 +5730,14 @@ function resolveRunProfile(requested, config, env2) {
5216
5730
  envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env2);
5217
5731
  }
5218
5732
  }
5733
+ for (const key of Object.keys(envExpanded)) {
5734
+ if (isDeniedEnvKey(key)) {
5735
+ return {
5736
+ ok: false,
5737
+ message: `run profile "${name}" refuses process-injection key "${key}" \u2014 a profile env block in .noir/config.yml must not set ${key} (it can inject into the spawned host process). Remove "${key}" from the profile.`
5738
+ };
5739
+ }
5740
+ }
5219
5741
  return {
5220
5742
  ok: true,
5221
5743
  profile: { binary: profile.binary, env: envExpanded, args: profile.args, profileName: name }
@@ -5236,11 +5758,195 @@ var init_run_profiles = __esm({
5236
5758
  }
5237
5759
  });
5238
5760
 
5761
+ // src/run-status.ts
5762
+ function humanizeElapsed(ms) {
5763
+ const total = Math.max(0, Math.floor(ms / 1e3));
5764
+ const seconds = total % 60;
5765
+ const minutes = Math.floor(total / 60) % 60;
5766
+ const hours = Math.floor(total / 3600);
5767
+ if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
5768
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
5769
+ return `${seconds}s`;
5770
+ }
5771
+ function hostStderrTail(stderr, maxLines = HOST_STDERR_TAIL_LINES) {
5772
+ if (maxLines <= 0) return "";
5773
+ const lines = stderr.split("\n").map((line) => line.trimEnd());
5774
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
5775
+ if (lines.length === 0) return "";
5776
+ if (lines.length <= maxLines) return lines.join("\n");
5777
+ const omitted = lines.length - maxLines;
5778
+ return [`\u2026 (${omitted} earlier lines omitted)`, ...lines.slice(-maxLines)].join("\n");
5779
+ }
5780
+ var HOST_STDERR_TAIL_LINES, RunStatusLine;
5781
+ var init_run_status = __esm({
5782
+ "src/run-status.ts"() {
5783
+ "use strict";
5784
+ init_orchestrator();
5785
+ HOST_STDERR_TAIL_LINES = 20;
5786
+ RunStatusLine = class {
5787
+ host;
5788
+ enabled;
5789
+ animated;
5790
+ sharesCursor;
5791
+ write;
5792
+ now;
5793
+ /**
5794
+ * Running token totals for the live line. This is a SECOND accumulator: the
5795
+ * snapshot the run reports at the end is reduced independently from the full
5796
+ * event stream, and both apply the same `max usage per message id` rule, so
5797
+ * the number on screen and the number in the summary agree.
5798
+ */
5799
+ usage = new UsageReducer();
5800
+ startedAt;
5801
+ model;
5802
+ streaming = false;
5803
+ begun = false;
5804
+ ended = false;
5805
+ /** True while the status text is sitting on the row the cursor is currently at. */
5806
+ onRow = false;
5807
+ /** True when the last stdout write ended a line — the only safe moment to move the cursor. */
5808
+ stdoutAtRowStart = true;
5809
+ /** The text currently on the row (empty when the row is not ours). */
5810
+ rendered = "";
5811
+ constructor(opts) {
5812
+ this.host = opts.host;
5813
+ this.enabled = opts.json !== true && opts.quiet !== true;
5814
+ this.animated = this.enabled && opts.stderrIsTty === true;
5815
+ this.sharesCursor = opts.stdoutSharesCursor !== false;
5816
+ this.write = opts.write ?? ((chunk) => void process.stderr.write(chunk));
5817
+ this.now = opts.now ?? (() => performance.now());
5818
+ this.startedAt = this.now();
5819
+ }
5820
+ /**
5821
+ * Claim the row: show the seeded "waiting" state immediately, before the host
5822
+ * has said anything, so the spawn itself is never blank. Calling this twice
5823
+ * is a no-op.
5824
+ */
5825
+ begin() {
5826
+ if (!this.enabled || this.begun) return;
5827
+ this.begun = true;
5828
+ this.startedAt = this.now();
5829
+ if (this.animated) {
5830
+ this.draw();
5831
+ return;
5832
+ }
5833
+ this.write(`${this.render()}
5834
+ `);
5835
+ }
5836
+ /** Feed one normalized host event. Anything unrenderable is ignored. */
5837
+ event(e) {
5838
+ if (!this.enabled || this.ended || !this.begun) return;
5839
+ if (e.kind === "result") {
5840
+ this.end();
5841
+ return;
5842
+ }
5843
+ if (e.kind === "init") {
5844
+ if (e.model !== void 0) this.model = e.model;
5845
+ } else if (e.kind === "assistant" || e.kind === "tool") {
5846
+ this.streaming = true;
5847
+ this.usage.add(e);
5848
+ } else if (e.kind === "delta") {
5849
+ this.streaming = true;
5850
+ } else {
5851
+ return;
5852
+ }
5853
+ this.draw();
5854
+ }
5855
+ /**
5856
+ * Announce the chunk the caller is about to write to STDOUT — before writing
5857
+ * it. Warning the line first lets it finish its row: the answer would
5858
+ * otherwise start glued to the tail of the status text. It also records
5859
+ * whether that write ended a line, which is the only moment the cursor can be
5860
+ * moved without landing inside the answer.
5861
+ */
5862
+ beforeStdout(chunk) {
5863
+ if (chunk.length === 0) return;
5864
+ if (this.animated && this.sharesCursor && this.onRow) {
5865
+ this.write("\n");
5866
+ this.onRow = false;
5867
+ }
5868
+ this.stdoutAtRowStart = chunk.endsWith("\n");
5869
+ }
5870
+ /**
5871
+ * Finish the row the host's answer left open. A failure message printed onto
5872
+ * a half-written answer reads as part of the answer, so a caller about to
5873
+ * report a failure asks for the row to be terminated first. A no-op unless
5874
+ * this line is drawing on a terminal stdout also writes to and the answer
5875
+ * stopped mid-line — the only state where the cursor is inside the answer.
5876
+ */
5877
+ finishRow() {
5878
+ if (!this.animated || !this.sharesCursor || this.stdoutAtRowStart) return;
5879
+ this.write("\n");
5880
+ this.stdoutAtRowStart = true;
5881
+ }
5882
+ /**
5883
+ * Close the line out. On a terminal the line is erased so the run summary
5884
+ * takes its place — unless the answer has since moved the cursor off it, in
5885
+ * which case the text stays as the record of how long the wait was. On a pipe
5886
+ * it becomes the final marker. Safe to call twice.
5887
+ */
5888
+ end() {
5889
+ if (!this.enabled || this.ended) return;
5890
+ this.ended = true;
5891
+ if (!this.animated) {
5892
+ this.write(`${this.render(true)}
5893
+ `);
5894
+ return;
5895
+ }
5896
+ if (!this.onRow || !this.cursorFree()) return;
5897
+ this.write("\r\x1B[K");
5898
+ this.rendered = "";
5899
+ this.onRow = false;
5900
+ }
5901
+ /** Rewrite the current row in place, skipping no-op rewrites and unsafe moments. */
5902
+ draw() {
5903
+ if (!this.animated || !this.cursorFree()) return;
5904
+ const text = this.render();
5905
+ if (text === this.rendered) return;
5906
+ this.write(`\r\x1B[K${text}`);
5907
+ this.rendered = text;
5908
+ this.onRow = true;
5909
+ }
5910
+ /**
5911
+ * Whether the cursor is at the start of a row nobody else is using. On stdout
5912
+ * that is not this terminal there is no shared cursor to disturb; when it is,
5913
+ * only a write that ended a line leaves one free.
5914
+ */
5915
+ cursorFree() {
5916
+ return !this.sharesCursor || this.stdoutAtRowStart;
5917
+ }
5918
+ /**
5919
+ * `final` marks the line the run ends on: a run that never got past the spawn
5920
+ * is over, so saying it is still waiting — directly above whatever the run
5921
+ * reported — would be a lie.
5922
+ */
5923
+ render(final = false) {
5924
+ const parts = [];
5925
+ if (this.streaming) {
5926
+ const u = this.usage.snapshot();
5927
+ parts.push(
5928
+ this.model ?? this.host,
5929
+ this.elapsed(),
5930
+ `\u2193${u.inputTokens.toLocaleString()}\u2191${u.outputTokens.toLocaleString()} tokens`
5931
+ );
5932
+ return `\u25CF ${parts.join(" \xB7 ")}`;
5933
+ }
5934
+ if (this.model !== void 0) return `\u25B6 ${[this.host, this.model, this.elapsed()].join(" \xB7 ")}`;
5935
+ if (final) return `\u25B6 ${this.host} \xB7 no output after ${this.elapsed()}`;
5936
+ return `\u25B6 ${this.host} \xB7 waiting for first event\u2026`;
5937
+ }
5938
+ elapsed() {
5939
+ return humanizeElapsed(this.now() - this.startedAt);
5940
+ }
5941
+ };
5942
+ }
5943
+ });
5944
+
5239
5945
  // src/commands/run.ts
5240
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
5946
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
5241
5947
  import { join as join5 } from "path";
5242
5948
  import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
5243
- import { loadNoirEnv as loadNoirEnv3, NOIR_DIR as NOIR_DIR2, parseConfig } from "@noir-ai/core";
5949
+ import { isDeniedEnvKey as isDeniedEnvKey2, loadNoirEnv as loadNoirEnv3, NOIR_DIR as NOIR_DIR2, parseConfig, paths as paths2 } from "@noir-ai/core";
5244
5950
  function credentialNote(env2, sources) {
5245
5951
  const named = CREDENTIAL_ENV_VARS.filter((name) => {
5246
5952
  const value = env2[name];
@@ -5260,13 +5966,24 @@ function formatUsage(u) {
5260
5966
  const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
5261
5967
  return `${u.inputTokens.toLocaleString()} in / ${u.outputTokens.toLocaleString()} out${cost}${turns}`;
5262
5968
  }
5263
- function streamEvent(event, opts) {
5264
- if (opts.json === true) return;
5969
+ function answerText(event) {
5265
5970
  if (event.kind === "assistant" && event.text && event.text.length > 0 && event.isError !== true) {
5266
- process.stdout.write(event.text);
5971
+ return event.text;
5972
+ }
5973
+ return void 0;
5974
+ }
5975
+ function streamEvent(event, opts, status2) {
5976
+ if (opts.json === true) return;
5977
+ const text = answerText(event);
5978
+ if (text !== void 0) {
5979
+ status2.beforeStdout(text);
5980
+ process.stdout.write(text);
5267
5981
  }
5268
5982
  }
5269
5983
  async function run(prompt, opts) {
5984
+ await runOnce(prompt, opts, void 0);
5985
+ }
5986
+ async function runOnce(prompt, opts, resumeSessionId) {
5270
5987
  const host = opts.host ?? "claude";
5271
5988
  if (!SUPPORTED_HOSTS.includes(host)) {
5272
5989
  fail(EXIT.USAGE, `unknown host '${host}' (supported: ${SUPPORTED_HOSTS.join(", ")})`, opts);
@@ -5285,7 +6002,7 @@ async function run(prompt, opts) {
5285
6002
  if (!resolved.ok) fail(EXIT.USAGE, resolved.message, opts);
5286
6003
  const profile = resolved.profile;
5287
6004
  const customBinary = opts.command ?? profile.binary;
5288
- const extraArgs = profile.args;
6005
+ const extraArgs = resumeSessionId === void 0 ? profile.args : [...profile.args ?? [], "--resume", resumeSessionId];
5289
6006
  const env2 = profile.env ? mergeEnv(process.env, profile.env) : void 0;
5290
6007
  if (!existsSync3(join5(root, NOIR_DIR2, ".env"))) {
5291
6008
  log(
@@ -5294,6 +6011,37 @@ async function run(prompt, opts) {
5294
6011
  );
5295
6012
  }
5296
6013
  const transcriptLines = [];
6014
+ const answer = [];
6015
+ let sessionId;
6016
+ const binary = customBinary && customBinary.length > 0 ? customBinary : host;
6017
+ const status2 = new RunStatusLine({
6018
+ host: binary,
6019
+ json: opts.json,
6020
+ quiet: opts.quiet,
6021
+ stderrIsTty: process.stderr.isTTY === true,
6022
+ stdoutSharesCursor: process.stdout.isTTY === true
6023
+ });
6024
+ status2.begin();
6025
+ let verdictWritten = false;
6026
+ const claimVerdict = () => {
6027
+ if (verdictWritten) return false;
6028
+ verdictWritten = true;
6029
+ return true;
6030
+ };
6031
+ const interrupt = new RunInterrupt({
6032
+ // A second interrupt leaves at once, and leaves the same verdict behind as
6033
+ // the first: the run owes its consumer an envelope under --json, and this is
6034
+ // the one path where `fail()` cannot write it (there is no caller left to
6035
+ // catch its throw). The child is forced first, so leaving does not leave a
6036
+ // host behind.
6037
+ exitNow: (code) => {
6038
+ interrupt.forceNow();
6039
+ if (claimVerdict()) {
6040
+ failAndExit(code, interruptedNotice(safeTranscript(host, transcriptLines)), opts);
6041
+ }
6042
+ }
6043
+ });
6044
+ interrupt.watch();
5297
6045
  let result;
5298
6046
  try {
5299
6047
  result = await runHost({
@@ -5303,28 +6051,61 @@ async function run(prompt, opts) {
5303
6051
  extraArgs,
5304
6052
  env: env2,
5305
6053
  onLine: (line) => transcriptLines.push(line),
5306
- onEvent: (event) => streamEvent(event, opts)
6054
+ onEvent: (event) => {
6055
+ if (event.kind === "init" && sessionId === void 0) sessionId = event.sessionId;
6056
+ status2.event(event);
6057
+ streamEvent(event, opts, status2);
6058
+ const text = answerText(event);
6059
+ if (text !== void 0) answer.push(text);
6060
+ },
6061
+ signal: interrupt.signal,
6062
+ onChild: (child) => interrupt.track(child)
5307
6063
  });
5308
6064
  } catch (err) {
5309
- const binary = customBinary && customBinary.length > 0 ? customBinary : host;
6065
+ status2.end();
6066
+ status2.finishRow();
6067
+ if (interrupt.interruptedBy !== void 0) {
6068
+ if (claimVerdict()) {
6069
+ failInterrupted(interrupt.interruptedBy, host, transcriptLines, opts);
6070
+ }
6071
+ return;
6072
+ }
5310
6073
  const detail = err instanceof Error ? err.message : String(err);
5311
6074
  const enoent = err?.code === "ENOENT";
5312
6075
  const guidance = enoent ? ` No executable '${binary}' was found. Shell aliases and functions (e.g. from .zshrc) are invisible to noir \u2014 use an executable on PATH, an absolute path, or a launcher script such as ~/.local/bin/${binary}.` : "";
5313
6076
  const subject = customBinary && customBinary.length > 0 ? `custom command '${customBinary}'` : `host '${host}'`;
5314
6077
  fail(EXIT.ERROR, `failed to run ${subject}: ${detail}.${guidance}`, opts);
6078
+ } finally {
6079
+ interrupt.unwatch();
5315
6080
  }
5316
- const transcript = writeTranscript(host, transcriptLines);
6081
+ if (interrupt.interruptedBy !== void 0) {
6082
+ status2.end();
6083
+ status2.finishRow();
6084
+ if (claimVerdict()) {
6085
+ failInterrupted(interrupt.interruptedBy, host, transcriptLines, opts);
6086
+ }
6087
+ return;
6088
+ }
6089
+ const transcript = safeTranscript(host, transcriptLines);
5317
6090
  const failed = result.exitCode !== 0 || result.isError;
5318
6091
  if (failed) {
5319
- const binary = customBinary && customBinary.length > 0 ? customBinary : host;
5320
- const reason = result.errorText && result.errorText.trim().length > 0 ? result.errorText.trim() : result.stderr.trim() || `exit code ${result.exitCode}`;
5321
- const isAuth = result.errorCategory === "authentication_failed" || result.errorCategory === "oauth_org_not_allowed" || /not logged|login|authenticate|invalid api key/i.test(reason);
6092
+ status2.end();
6093
+ status2.finishRow();
6094
+ const stderrTail = hostStderrTail(result.stderr);
6095
+ const reason = result.errorText && result.errorText.trim().length > 0 ? result.errorText.trim() : `exit code ${result.exitCode}`;
6096
+ const isAuth = result.errorCategory === "authentication_failed" || result.errorCategory === "oauth_org_not_allowed" || /not logged|login|authenticate|invalid api key/i.test(`${reason}
6097
+ ${stderrTail}`);
5322
6098
  let message = `host '${binary}' failed (exit ${result.exitCode}): ${reason}`;
5323
6099
  if (isAuth) {
5324
6100
  message += ` Open a terminal and run \`${binary} /login\` (interactive-only \u2014 it cannot run inside \`noir run\`), then retry.`;
5325
6101
  message += credentialNote(env2 ?? process.env, envSources);
5326
6102
  }
5327
6103
  message += ` If you use another profile, pass \`--command <binary>\` or define a run profile under run.profiles. transcript: ${transcript}`;
6104
+ if (opts.json !== true && stderrTail.length > 0) {
6105
+ message += `
6106
+ ${binary} stderr (last ${HOST_STDERR_TAIL_LINES} lines):
6107
+ ${stderrTail}`;
6108
+ }
5328
6109
  fail(EXIT.ERROR, message, opts);
5329
6110
  }
5330
6111
  if (opts.json === true) {
@@ -5337,26 +6118,59 @@ async function run(prompt, opts) {
5337
6118
  usage: result.usage,
5338
6119
  numTurns: result.usage.numTurns,
5339
6120
  events: result.eventCount,
5340
- transcript
6121
+ transcript,
6122
+ // The answer as plain words, so a consumer does not have to reassemble
6123
+ // it from the stream-json transcript.
6124
+ answerText: answer.join(""),
6125
+ ...sessionId === void 0 ? {} : { sessionId }
5341
6126
  }
5342
6127
  });
5343
6128
  return;
5344
6129
  }
6130
+ status2.end();
5345
6131
  process.stdout.write("\n");
5346
6132
  success(`usage: ${formatUsage(result.usage)} (API-equivalent estimate, not billed)`, opts);
5347
6133
  log(`transcript: ${transcript}`, opts);
6134
+ await offerPostRunActions({
6135
+ answer: answer.join(""),
6136
+ transcript,
6137
+ sessionId,
6138
+ host: binary,
6139
+ opts,
6140
+ // Only the first run of a session can be continued: a resumed run is
6141
+ // itself the continuation, so the menu never nests more than one deep.
6142
+ ...resumeSessionId === void 0 && sessionId !== void 0 ? { resume: (nextPrompt, id) => runOnce(nextPrompt, opts, id) } : {}
6143
+ });
5348
6144
  }
5349
6145
  function mergeEnv(base, overlay) {
6146
+ for (const key of Object.keys(overlay)) {
6147
+ if (isDeniedEnvKey2(key)) {
6148
+ fail(
6149
+ EXIT.ERROR,
6150
+ `refusing to run the host: the profile env defines process-injection key "${key}" (it can inject into the spawned host process)`
6151
+ );
6152
+ }
6153
+ }
5350
6154
  const merged = { ...base, ...overlay };
5351
6155
  return Object.fromEntries(Object.entries(merged).filter(([, v]) => v !== void 0));
5352
6156
  }
5353
- function writeTranscript(host, lines) {
6157
+ function failInterrupted(signal, host, lines, opts) {
6158
+ return fail(exitCodeForSignal(signal), interruptedNotice(safeTranscript(host, lines)), opts);
6159
+ }
6160
+ function safeTranscript(host, lines) {
6161
+ try {
6162
+ return writeTranscript(host, lines);
6163
+ } catch {
6164
+ return "(not persisted)";
6165
+ }
6166
+ }
6167
+ function writeTranscript(host, lines, root = process.cwd()) {
5354
6168
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5355
- const dir = join5(process.cwd(), ".noir", "transcripts");
6169
+ const dir = paths2.transcriptsDir(root);
5356
6170
  const file = join5(dir, `${host}-${ts}.jsonl`);
5357
6171
  try {
5358
6172
  mkdirSync3(dir, { recursive: true, mode: 448 });
5359
- writeFileSync3(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
6173
+ writeFileSync4(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
5360
6174
  } catch {
5361
6175
  return "(not persisted)";
5362
6176
  }
@@ -5368,7 +6182,10 @@ var init_run = __esm({
5368
6182
  "use strict";
5369
6183
  init_orchestrator();
5370
6184
  init_output();
6185
+ init_run_actions();
6186
+ init_run_interrupt();
5371
6187
  init_run_profiles();
6188
+ init_run_status();
5372
6189
  CREDENTIAL_ENV_VARS = [
5373
6190
  "ANTHROPIC_API_KEY",
5374
6191
  "ANTHROPIC_AUTH_TOKEN",
@@ -5377,254 +6194,17 @@ var init_run = __esm({
5377
6194
  }
5378
6195
  });
5379
6196
 
5380
- // src/commands/skills.ts
5381
- import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
5382
- import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
6197
+ // src/conflict.ts
5383
6198
  import {
5384
- buildRegistry,
5385
- discoverAll,
5386
- emitSkillsToDir,
5387
- lintSkill
5388
- } from "@noir-ai/skills";
5389
- function categoryOf(name) {
5390
- const mapped = CATEGORY[name];
5391
- if (mapped !== void 0) return mapped;
5392
- return name.replace(/^noir-/, "") || "general";
5393
- }
5394
- function toRow(s, kind = "builtin") {
5395
- const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
5396
- const status2 = s.skillMd.includes("> **Stub:**") ? "stub" : "full";
5397
- const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
5398
- return { name: s.name, category, description, kind, status: status2 };
5399
- }
5400
- function truncate(text, max) {
5401
- if (text.length <= max) return text;
5402
- const slice = text.slice(0, Math.max(0, max - 1));
5403
- const boundary = slice.lastIndexOf(" ");
5404
- const head = boundary > 0 ? slice.slice(0, boundary) : slice;
5405
- return `${head.trimEnd()}\u2026`;
6199
+ lineDiff
6200
+ } from "@noir-ai/create";
6201
+ function flaggedNonInteractive() {
6202
+ const v = process.env.NOIR_NON_INTERACTIVE;
6203
+ return v !== void 0 && v !== "";
5406
6204
  }
5407
- async function skillsList(opts) {
5408
- let builtins;
5409
- let integrations;
5410
- try {
5411
- const all = discoverAll();
5412
- builtins = all.builtins;
5413
- integrations = all.integrations;
5414
- } catch (err) {
5415
- fail(
5416
- EXIT.ERROR,
5417
- `skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
5418
- opts
5419
- );
5420
- }
5421
- const rows = [
5422
- ...builtins.map((b) => toRow(b, "builtin")),
5423
- ...integrations.map((i) => toRow(i, "integration"))
5424
- ];
5425
- const data = { count: rows.length, skills: rows };
5426
- if (opts.json === true) {
5427
- process.stdout.write(`${JSON.stringify({ ok: true, data })}
5428
- `);
5429
- return;
5430
- }
5431
- const intCount = integrations.length;
5432
- 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"}`;
5433
- log(banner, opts);
5434
- table(
5435
- rows.map((r) => ({
5436
- Skill: r.name,
5437
- Kind: r.kind,
5438
- Category: r.category,
5439
- Status: r.status,
5440
- Description: truncate(r.description, 80)
5441
- })),
5442
- ["Skill", "Kind", "Category", "Status", "Description"],
5443
- opts
5444
- );
5445
- }
5446
- async function skillsSync(opts) {
5447
- const root = process.cwd();
5448
- let project;
5449
- try {
5450
- project = loadProjectInfo14(root);
5451
- } catch {
5452
- fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5453
- }
5454
- const adapter = resolveAdapter4(project.config.host);
5455
- if (!adapter.skillsDir) {
5456
- const data2 = {
5457
- emitted: [],
5458
- references: 0,
5459
- host: project.config.host,
5460
- reason: "no-skill-emitter"
5461
- };
5462
- if (opts.json === true) {
5463
- process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
5464
- `);
5465
- return;
5466
- }
5467
- info("This host has no skill emitter; nothing to sync.", opts);
5468
- return;
5469
- }
5470
- const dir = adapter.skillsDir({ root });
5471
- const target = project.config.host;
5472
- const summary = await emitSkillsToDir(dir, { target });
5473
- const pruned = summary.pruned ?? [];
5474
- const data = {
5475
- emitted: summary.emitted,
5476
- references: summary.references,
5477
- dir,
5478
- pruned
5479
- };
5480
- if (opts.json === true) {
5481
- process.stdout.write(`${JSON.stringify({ ok: true, data })}
5482
- `);
5483
- return;
5484
- }
5485
- log(
5486
- `Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
5487
- opts
5488
- );
5489
- if (pruned.length > 0) {
5490
- warn(
5491
- `Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
5492
- opts
5493
- );
5494
- }
5495
- }
5496
- async function skillsLint(opts) {
5497
- let skills;
5498
- try {
5499
- const all = discoverAll();
5500
- skills = [...all.builtins, ...all.integrations].map(lintSkill);
5501
- } catch (err) {
5502
- fail(
5503
- EXIT.ERROR,
5504
- `skills lint: could not lint skills (${err instanceof Error ? err.message : String(err)})`,
5505
- opts
5506
- );
5507
- }
5508
- const errored = skills.filter((s) => s.errors.length > 0);
5509
- const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
5510
- const data = { count: skills.length, errored: errored.length, skills };
5511
- if (errored.length > 0) {
5512
- const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
5513
- if (opts.json === true) {
5514
- process.stdout.write(
5515
- `${JSON.stringify({ ok: false, error: { code: EXIT.ERROR, message }, data })}
5516
- `
5517
- );
5518
- process.exitCode = EXIT.ERROR;
5519
- return;
5520
- }
5521
- warn(
5522
- `noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
5523
- opts
5524
- );
5525
- for (const s of errored) {
5526
- for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
5527
- }
5528
- if (warned.length > 0) {
5529
- info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
5530
- for (const s of warned) {
5531
- for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
5532
- }
5533
- }
5534
- fail(EXIT.ERROR, message, opts);
5535
- }
5536
- if (opts.json === true) {
5537
- process.stdout.write(`${JSON.stringify({ ok: true, data })}
5538
- `);
5539
- return;
5540
- }
5541
- log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
5542
- if (warned.length > 0) {
5543
- info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
5544
- for (const s of warned) {
5545
- for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
5546
- }
5547
- }
5548
- }
5549
- async function skillsRegistry(opts) {
5550
- let reg;
5551
- try {
5552
- reg = buildRegistry();
5553
- } catch (err) {
5554
- fail(
5555
- EXIT.ERROR,
5556
- `skills registry: could not build registry (${err instanceof Error ? err.message : String(err)})`,
5557
- opts
5558
- );
5559
- }
5560
- const data = { count: reg.length, skills: reg };
5561
- if (opts.json === true) {
5562
- process.stdout.write(`${JSON.stringify({ ok: true, data })}
5563
- `);
5564
- return;
5565
- }
5566
- log(`noir skills registry \u2014 ${reg.length} skills.`, opts);
5567
- table(
5568
- reg.map((r) => ({
5569
- Skill: r.name,
5570
- Kind: r.kind,
5571
- Category: r.category,
5572
- Version: r.version,
5573
- Status: r.status,
5574
- Refs: String(r.referenceCount),
5575
- Lines: String(r.lines)
5576
- })),
5577
- ["Skill", "Kind", "Category", "Version", "Status", "Refs", "Lines"],
5578
- opts
5579
- );
5580
- }
5581
- var CATEGORY;
5582
- var init_skills = __esm({
5583
- "src/commands/skills.ts"() {
5584
- "use strict";
5585
- init_output();
5586
- CATEGORY = {
5587
- "noir-brainstorming": "discovery",
5588
- "noir-sync": "discovery",
5589
- "noir-checkpoint": "discovery",
5590
- "noir-exploring": "discovery",
5591
- "noir-spec": "spec",
5592
- "noir-planning": "plan",
5593
- "noir-prd": "plan",
5594
- "noir-executing-plans": "execute",
5595
- "noir-test-driven-development": "execute",
5596
- "noir-systematic-debugging": "execute",
5597
- "noir-subagent": "execute",
5598
- "noir-parallel": "execute",
5599
- "noir-verifying": "verify",
5600
- "noir-security": "verify",
5601
- "noir-wrap": "document",
5602
- "noir-readme": "document",
5603
- "noir-shipping": "git",
5604
- "noir-worktree": "git",
5605
- "noir-recall": "memory",
5606
- "noir-remember": "memory",
5607
- "noir-context": "context",
5608
- "noir-frontend": "domain",
5609
- "noir-backend": "domain",
5610
- "noir-doctor": "meta",
5611
- "noir-writing-skills": "meta",
5612
- "noir-rules": "meta"
5613
- };
5614
- }
5615
- });
5616
-
5617
- // src/conflict.ts
5618
- import {
5619
- lineDiff
5620
- } from "@noir-ai/create";
5621
- function flaggedNonInteractive() {
5622
- const v = process.env.NOIR_NON_INTERACTIVE;
5623
- return v !== void 0 && v !== "";
5624
- }
5625
- function buildConflictOpts(input = {}) {
5626
- if (input.force === true) {
5627
- return { conflictPolicy: "overwrite" };
6205
+ function buildConflictOpts(input = {}) {
6206
+ if (input.force === true) {
6207
+ return { conflictPolicy: "overwrite" };
5628
6208
  }
5629
6209
  if (input.interactive === false) {
5630
6210
  return { conflictPolicy: "preserve" };
@@ -5723,7 +6303,7 @@ var init_conflict = __esm({
5723
6303
  });
5724
6304
 
5725
6305
  // src/dedup-write.ts
5726
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
6306
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
5727
6307
  import { join as join6 } from "path";
5728
6308
  import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
5729
6309
  async function checkWritePathDedup(root, res, opts) {
@@ -5872,14 +6452,14 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
5872
6452
  return "preserve";
5873
6453
  } else if (choice === "replace") {
5874
6454
  try {
5875
- writeFileSync4(join6(root, matchedRel), proposedText, "utf8");
6455
+ writeFileSync5(join6(root, matchedRel), proposedText, "utf8");
5876
6456
  } catch {
5877
6457
  }
5878
6458
  return "replace";
5879
6459
  } else if (choice === "mirror") {
5880
6460
  try {
5881
6461
  const existing = readFileSync4(join6(root, matchedRel), "utf8");
5882
- writeFileSync4(
6462
+ writeFileSync5(
5883
6463
  join6(root, matchedRel),
5884
6464
  `${existing}
5885
6465
 
@@ -5918,7 +6498,7 @@ function loadCache(root) {
5918
6498
  function saveCache(root, cache) {
5919
6499
  try {
5920
6500
  mkdirSync4(join6(root, ".noir"), { recursive: true });
5921
- writeFileSync4(cachePath(root), JSON.stringify(cache), "utf8");
6501
+ writeFileSync5(cachePath(root), JSON.stringify(cache), "utf8");
5922
6502
  } catch {
5923
6503
  }
5924
6504
  }
@@ -5940,16 +6520,16 @@ function safeRead(abs) {
5940
6520
  }
5941
6521
  }
5942
6522
  function withTimeout(p, ms) {
5943
- return new Promise((resolve2) => {
5944
- const t = setTimeout(() => resolve2(void 0), ms);
6523
+ return new Promise((resolve3) => {
6524
+ const t = setTimeout(() => resolve3(void 0), ms);
5945
6525
  p.then(
5946
6526
  (v) => {
5947
6527
  clearTimeout(t);
5948
- resolve2(v);
6528
+ resolve3(v);
5949
6529
  },
5950
6530
  () => {
5951
6531
  clearTimeout(t);
5952
- resolve2(void 0);
6532
+ resolve3(void 0);
5953
6533
  }
5954
6534
  );
5955
6535
  });
@@ -5972,13 +6552,16 @@ var init_dedup_write = __esm({
5972
6552
  });
5973
6553
 
5974
6554
  // src/init.ts
5975
- import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
5976
- import { loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
6555
+ import { existsSync as existsSync4 } from "fs";
6556
+ import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
6557
+ import { loadProjectInfo as loadProjectInfo14, paths as paths3 } from "@noir-ai/core";
5977
6558
  import { scaffold } from "@noir-ai/create";
5978
- import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
6559
+ import {
6560
+ emitSkillsToDir
6561
+ } from "@noir-ai/skills";
5979
6562
  async function init(root, opts) {
5980
6563
  assertTransportUrl(opts);
5981
- const host = opts.host ?? "claude";
6564
+ const host = resolveInitHost(root, opts);
5982
6565
  const interactive = resolveInteractive();
5983
6566
  const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
5984
6567
  const dryRun = opts.dryRun === true || opts.preview === true;
@@ -5999,15 +6582,29 @@ async function init(root, opts) {
5999
6582
  return res;
6000
6583
  }
6001
6584
  if (res.noop) return res;
6002
- await emitHostSkills(root, host, conflictOpts, interactive);
6585
+ const skillSummary = await emitHostSkills(root, host, conflictOpts, interactive);
6586
+ res.skillConflicts = skillSummary?.conflicts ?? [];
6587
+ res.preservedSkills = skillSummary?.preserved ?? [];
6003
6588
  let projectInfo;
6004
6589
  try {
6005
- projectInfo = loadProjectInfo15(root);
6590
+ projectInfo = loadProjectInfo14(root);
6006
6591
  } catch {
6007
6592
  projectInfo = void 0;
6008
6593
  }
6009
6594
  const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
6010
6595
  if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
6596
+ if (res.refreshed.length > 0) {
6597
+ process.stderr.write(
6598
+ `Refreshed ${res.refreshed.length} unedited doc seed(s): ${res.refreshed.join(", ")}
6599
+ `
6600
+ );
6601
+ }
6602
+ if (res.migrationChanged.length > 0) {
6603
+ process.stderr.write(
6604
+ `Migrated ${res.migrationChanged.length} file(s): ${res.migrationChanged.join(", ")}
6605
+ `
6606
+ );
6607
+ }
6011
6608
  process.stderr.write(
6012
6609
  `Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).
6013
6610
  `
@@ -6017,6 +6614,19 @@ async function init(root, opts) {
6017
6614
  );
6018
6615
  return res;
6019
6616
  }
6617
+ function resolveInitHost(root, opts) {
6618
+ if (opts.host !== void 0) return opts.host;
6619
+ if (!existsSync4(paths3.config(root))) return "claude";
6620
+ try {
6621
+ return loadProjectInfo14(root).config.host;
6622
+ } catch {
6623
+ process.stderr.write(
6624
+ `Could not read the configured host from ${paths3.config(root)}; using 'claude'.
6625
+ `
6626
+ );
6627
+ return "claude";
6628
+ }
6629
+ }
6020
6630
  function reportPlannedWrites(res) {
6021
6631
  log("Dry run \u2014 no files were written.");
6022
6632
  if (res.written.length > 0) {
@@ -6026,6 +6636,10 @@ function reportPlannedWrites(res) {
6026
6636
  log(mode === void 0 ? ` ${p}` : ` ${p} (mode ${mode.toString(8).padStart(3, "0")})`);
6027
6637
  }
6028
6638
  }
6639
+ if (res.refreshed.length > 0) {
6640
+ log("Would refresh (unedited older copy):");
6641
+ for (const p of res.refreshed) log(` ${p}`);
6642
+ }
6029
6643
  if (res.skipped.length > 0) {
6030
6644
  log("Would leave as-is (already present):");
6031
6645
  for (const p of res.skipped) log(` ${p}`);
@@ -6036,12 +6650,12 @@ function reportPlannedWrites(res) {
6036
6650
  }
6037
6651
  }
6038
6652
  async function emitHostSkills(root, host, conflictOpts, interactive) {
6039
- const adapter = resolveAdapter5(host);
6653
+ const adapter = resolveAdapter4(host);
6040
6654
  const skillsDir = adapter.skillsDir?.({ root });
6041
6655
  if (skillsDir === void 0) {
6042
6656
  process.stderr.write(`host '${host}' has no skill emitter; skipping skills
6043
6657
  `);
6044
- return;
6658
+ return void 0;
6045
6659
  }
6046
6660
  const target = host;
6047
6661
  const skillOpts = {
@@ -6053,12 +6667,20 @@ async function emitHostSkills(root, host, conflictOpts, interactive) {
6053
6667
  if (conflictOpts.onConflict !== void 0) {
6054
6668
  skillOpts.onConflict = conflictOpts.onConflict;
6055
6669
  }
6056
- const summary = await emitSkillsToDir2(skillsDir, skillOpts);
6670
+ const summary = await emitSkillsToDir(skillsDir, skillOpts);
6057
6671
  const relDir = skillsDir.replace(`${root}/`, "");
6058
6672
  process.stderr.write(
6059
6673
  `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
6060
6674
  `
6061
6675
  );
6676
+ const stale = preservedStaleLine(summary.preserved ?? []);
6677
+ if (stale !== void 0) process.stderr.write(`${stale}
6678
+ `);
6679
+ return summary;
6680
+ }
6681
+ function preservedStaleLine(preserved) {
6682
+ if (preserved.length === 0) return void 0;
6683
+ return `${preserved.length} skill(s) preserved as stale (interactive TTY required to refresh): ${preserved.join(", ")}`;
6062
6684
  }
6063
6685
  function assertTransportUrl(opts) {
6064
6686
  if (opts.transport === "streamable-http" && opts.url === void 0) {
@@ -6091,6 +6713,251 @@ var init_init = __esm({
6091
6713
  }
6092
6714
  });
6093
6715
 
6716
+ // src/commands/skills.ts
6717
+ import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
6718
+ import { loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
6719
+ import {
6720
+ buildRegistry,
6721
+ discoverAll,
6722
+ emitSkillsToDir as emitSkillsToDir2,
6723
+ lintSkill
6724
+ } from "@noir-ai/skills";
6725
+ function categoryOf(name) {
6726
+ const mapped = CATEGORY[name];
6727
+ if (mapped !== void 0) return mapped;
6728
+ return name.replace(/^noir-/, "") || "general";
6729
+ }
6730
+ function toRow(s, kind = "builtin") {
6731
+ const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
6732
+ const status2 = s.skillMd.includes("> **Stub:**") ? "stub" : "full";
6733
+ const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
6734
+ return { name: s.name, category, description, kind, status: status2 };
6735
+ }
6736
+ function truncate(text, max) {
6737
+ if (text.length <= max) return text;
6738
+ const slice = text.slice(0, Math.max(0, max - 1));
6739
+ const boundary = slice.lastIndexOf(" ");
6740
+ const head = boundary > 0 ? slice.slice(0, boundary) : slice;
6741
+ return `${head.trimEnd()}\u2026`;
6742
+ }
6743
+ async function skillsList(opts) {
6744
+ let builtins;
6745
+ let integrations;
6746
+ try {
6747
+ const all = discoverAll();
6748
+ builtins = all.builtins;
6749
+ integrations = all.integrations;
6750
+ } catch (err) {
6751
+ fail(
6752
+ EXIT.ERROR,
6753
+ `skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
6754
+ opts
6755
+ );
6756
+ }
6757
+ const rows = [
6758
+ ...builtins.map((b) => toRow(b, "builtin")),
6759
+ ...integrations.map((i) => toRow(i, "integration"))
6760
+ ];
6761
+ const data = { count: rows.length, skills: rows };
6762
+ if (opts.json === true) {
6763
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
6764
+ `);
6765
+ return;
6766
+ }
6767
+ const intCount = integrations.length;
6768
+ 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"}`;
6769
+ log(banner, opts);
6770
+ table(
6771
+ rows.map((r) => ({
6772
+ Skill: r.name,
6773
+ Kind: r.kind,
6774
+ Category: r.category,
6775
+ Status: r.status,
6776
+ Description: truncate(r.description, 80)
6777
+ })),
6778
+ ["Skill", "Kind", "Category", "Status", "Description"],
6779
+ opts
6780
+ );
6781
+ }
6782
+ async function skillsSync(opts) {
6783
+ const root = process.cwd();
6784
+ let project;
6785
+ try {
6786
+ project = loadProjectInfo15(root);
6787
+ } catch {
6788
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
6789
+ }
6790
+ const adapter = resolveAdapter5(project.config.host);
6791
+ if (!adapter.skillsDir) {
6792
+ const data2 = {
6793
+ emitted: [],
6794
+ references: 0,
6795
+ host: project.config.host,
6796
+ reason: "no-skill-emitter"
6797
+ };
6798
+ if (opts.json === true) {
6799
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
6800
+ `);
6801
+ return;
6802
+ }
6803
+ info("This host has no skill emitter; nothing to sync.", opts);
6804
+ return;
6805
+ }
6806
+ const dir = adapter.skillsDir({ root });
6807
+ const target = project.config.host;
6808
+ const summary = await emitSkillsToDir2(dir, { target });
6809
+ const pruned = summary.pruned ?? [];
6810
+ const preserved = summary.preserved ?? [];
6811
+ const data = {
6812
+ emitted: summary.emitted,
6813
+ references: summary.references,
6814
+ dir,
6815
+ pruned,
6816
+ // What the count above does NOT cover: skills whose files were preserved
6817
+ // (stale on disk, by design, in a non-interactive run). Same field `noir
6818
+ // init --json` carries, so a consumer reads one shape from every emitter.
6819
+ preserved
6820
+ };
6821
+ if (opts.json === true) {
6822
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
6823
+ `);
6824
+ return;
6825
+ }
6826
+ log(
6827
+ `Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
6828
+ opts
6829
+ );
6830
+ if (pruned.length > 0) {
6831
+ warn(
6832
+ `Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
6833
+ opts
6834
+ );
6835
+ }
6836
+ const stale = preservedStaleLine(preserved);
6837
+ if (stale !== void 0) warn(stale, opts);
6838
+ }
6839
+ async function skillsLint(opts) {
6840
+ let skills;
6841
+ try {
6842
+ const all = discoverAll();
6843
+ skills = [...all.builtins, ...all.integrations].map(lintSkill);
6844
+ } catch (err) {
6845
+ fail(
6846
+ EXIT.ERROR,
6847
+ `skills lint: could not lint skills (${err instanceof Error ? err.message : String(err)})`,
6848
+ opts
6849
+ );
6850
+ }
6851
+ const errored = skills.filter((s) => s.errors.length > 0);
6852
+ const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
6853
+ const data = { count: skills.length, errored: errored.length, skills };
6854
+ if (errored.length > 0) {
6855
+ const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
6856
+ if (opts.json === true) {
6857
+ process.stdout.write(
6858
+ `${JSON.stringify({ ok: false, error: { code: EXIT.ERROR, message }, data })}
6859
+ `
6860
+ );
6861
+ process.exitCode = EXIT.ERROR;
6862
+ return;
6863
+ }
6864
+ warn(
6865
+ `noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
6866
+ opts
6867
+ );
6868
+ for (const s of errored) {
6869
+ for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
6870
+ }
6871
+ if (warned.length > 0) {
6872
+ info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
6873
+ for (const s of warned) {
6874
+ for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
6875
+ }
6876
+ }
6877
+ fail(EXIT.ERROR, message, opts);
6878
+ }
6879
+ if (opts.json === true) {
6880
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
6881
+ `);
6882
+ return;
6883
+ }
6884
+ log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
6885
+ if (warned.length > 0) {
6886
+ info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
6887
+ for (const s of warned) {
6888
+ for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
6889
+ }
6890
+ }
6891
+ }
6892
+ async function skillsRegistry(opts) {
6893
+ let reg;
6894
+ try {
6895
+ reg = buildRegistry();
6896
+ } catch (err) {
6897
+ fail(
6898
+ EXIT.ERROR,
6899
+ `skills registry: could not build registry (${err instanceof Error ? err.message : String(err)})`,
6900
+ opts
6901
+ );
6902
+ }
6903
+ const data = { count: reg.length, skills: reg };
6904
+ if (opts.json === true) {
6905
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
6906
+ `);
6907
+ return;
6908
+ }
6909
+ log(`noir skills registry \u2014 ${reg.length} skills.`, opts);
6910
+ table(
6911
+ reg.map((r) => ({
6912
+ Skill: r.name,
6913
+ Kind: r.kind,
6914
+ Category: r.category,
6915
+ Version: r.version,
6916
+ Status: r.status,
6917
+ Refs: String(r.referenceCount),
6918
+ Lines: String(r.lines)
6919
+ })),
6920
+ ["Skill", "Kind", "Category", "Version", "Status", "Refs", "Lines"],
6921
+ opts
6922
+ );
6923
+ }
6924
+ var CATEGORY;
6925
+ var init_skills = __esm({
6926
+ "src/commands/skills.ts"() {
6927
+ "use strict";
6928
+ init_init();
6929
+ init_output();
6930
+ CATEGORY = {
6931
+ "noir-brainstorming": "discovery",
6932
+ "noir-sync": "discovery",
6933
+ "noir-checkpoint": "discovery",
6934
+ "noir-exploring": "discovery",
6935
+ "noir-spec": "spec",
6936
+ "noir-planning": "plan",
6937
+ "noir-prd": "plan",
6938
+ "noir-executing-plans": "execute",
6939
+ "noir-test-driven-development": "execute",
6940
+ "noir-systematic-debugging": "execute",
6941
+ "noir-subagent": "execute",
6942
+ "noir-parallel": "execute",
6943
+ "noir-verifying": "verify",
6944
+ "noir-security": "verify",
6945
+ "noir-wrap": "document",
6946
+ "noir-readme": "document",
6947
+ "noir-shipping": "git",
6948
+ "noir-worktree": "git",
6949
+ "noir-recall": "memory",
6950
+ "noir-remember": "memory",
6951
+ "noir-context": "context",
6952
+ "noir-frontend": "domain",
6953
+ "noir-backend": "domain",
6954
+ "noir-doctor": "meta",
6955
+ "noir-writing-skills": "meta",
6956
+ "noir-rules": "meta"
6957
+ };
6958
+ }
6959
+ });
6960
+
6094
6961
  // src/serve.ts
6095
6962
  import { applyNoirEnv, loadProjectInfo as loadProjectInfo16 } from "@noir-ai/core";
6096
6963
  import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
@@ -6130,22 +6997,21 @@ var create_exports = {};
6130
6997
  __export(create_exports, {
6131
6998
  create: () => create
6132
6999
  });
6133
- import { resolve } from "path";
7000
+ import { resolve as resolve2 } from "path";
6134
7001
  import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
6135
7002
  import { loadProjectInfo as loadProjectInfo17 } from "@noir-ai/core";
6136
7003
  import { scaffold as scaffold2 } from "@noir-ai/create";
6137
7004
  import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
6138
7005
  async function create(dir, opts) {
6139
7006
  assertTransportUrl(opts);
6140
- const root = resolve(dir ?? process.cwd());
6141
- const host = opts.host ?? "claude";
7007
+ const root = resolve2(dir ?? process.cwd());
6142
7008
  const interactive = resolveInteractive();
6143
7009
  const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
6144
7010
  const dryRun = opts.dryRun === true || opts.preview === true;
6145
7011
  const res = await scaffold2({
6146
7012
  root,
6147
7013
  mode: "create",
6148
- host,
7014
+ ...opts.host !== void 0 ? { host: opts.host } : {},
6149
7015
  transport: opts.transport,
6150
7016
  interactive,
6151
7017
  ...opts.url !== void 0 ? { url: opts.url } : {},
@@ -6158,6 +7024,7 @@ async function create(dir, opts) {
6158
7024
  return res;
6159
7025
  }
6160
7026
  if (res.noop) return res;
7027
+ const host = res.host;
6161
7028
  const adapter = resolveAdapter6(host);
6162
7029
  const skillsDir = adapter.skillsDir?.({ root });
6163
7030
  if (skillsDir === void 0) {
@@ -6180,6 +7047,9 @@ async function create(dir, opts) {
6180
7047
  `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
6181
7048
  `
6182
7049
  );
7050
+ const stale = preservedStaleLine(summary.preserved ?? []);
7051
+ if (stale !== void 0) process.stderr.write(`${stale}
7052
+ `);
6183
7053
  }
6184
7054
  let projectInfo;
6185
7055
  try {
@@ -6209,9 +7079,9 @@ var sync_exports = {};
6209
7079
  __export(sync_exports, {
6210
7080
  sync: () => sync
6211
7081
  });
6212
- import { existsSync as existsSync4 } from "fs";
7082
+ import { existsSync as existsSync5 } from "fs";
6213
7083
  import { resolveAdapter as resolveAdapter7 } from "@noir-ai/adapters";
6214
- import { loadProjectInfo as loadProjectInfo18, paths as paths2 } from "@noir-ai/core";
7084
+ import { loadProjectInfo as loadProjectInfo18, paths as paths4 } from "@noir-ai/core";
6215
7085
  import { scaffold as scaffold3 } from "@noir-ai/create";
6216
7086
  import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
6217
7087
  async function sync(root, opts = {}) {
@@ -6271,6 +7141,9 @@ async function sync(root, opts = {}) {
6271
7141
  `
6272
7142
  );
6273
7143
  }
7144
+ const stale = preservedStaleLine(summary.preserved ?? []);
7145
+ if (stale !== void 0) process.stderr.write(`${stale}
7146
+ `);
6274
7147
  }
6275
7148
  const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
6276
7149
  if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
@@ -6278,7 +7151,7 @@ async function sync(root, opts = {}) {
6278
7151
  }
6279
7152
  function resolveSyncHost(root, opts) {
6280
7153
  if (opts.host !== void 0) return opts.host;
6281
- if (!existsSync4(paths2.config(root))) return "claude";
7154
+ if (!existsSync5(paths4.config(root))) return "claude";
6282
7155
  try {
6283
7156
  return loadProjectInfo18(root).config.host;
6284
7157
  } catch {
@@ -6411,13 +7284,15 @@ function createProgram() {
6411
7284
  "--upgrade",
6412
7285
  "run scaffold migrations before re-emitting (re-run on an existing project)"
6413
7286
  ).option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").option("--dry-run", "report planned writes without writing anything").option("--preview", "alias for --dry-run").addOption(
6414
- // S10: target host. Defaults to `'claude'` (the regression anchor). The
6415
- // chosen host is forwarded to scaffold() + skills emission via
6416
- // resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS` so a new host
6417
- // lands here automatically; commander rejects anything else as usage=2.
7287
+ // Target host. When the flag is absent the host is read from an existing
7288
+ // `.noir/config.yml`, falling back to `'claude'` (so a fresh project is
7289
+ // unaffected). The chosen host is forwarded to scaffold() + skills
7290
+ // emission via resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS`
7291
+ // so a new host lands here automatically; commander rejects anything else
7292
+ // as usage=2.
6418
7293
  new Option(
6419
7294
  "--host <id>",
6420
- "target agentic CLI (default: claude) \u2014 drives host-side emission"
7295
+ "target agentic CLI (default: the host in .noir/config.yml, else claude)"
6421
7296
  ).choices(SUPPORTED_HOSTS2)
6422
7297
  ).action(
6423
7298
  async (opts, cmd) => {
@@ -6480,7 +7355,7 @@ function createProgram() {
6480
7355
  "strip-replace managed regions (discard hand-edits inside <!-- noir:* --> markers)"
6481
7356
  )
6482
7357
  ).addOption(
6483
- // S10: optional `--host` override. When omitted, sync reads host from
7358
+ // Optional `--host` override. When omitted, sync reads host from
6484
7359
  // `.noir/config.yml` (whatever `noir init --host <id>` persisted). The
6485
7360
  // override is rarely needed — documented as advanced.
6486
7361
  new Option(
@@ -7035,7 +7910,7 @@ var init_bin = __esm({
7035
7910
  }
7036
7911
  },
7037
7912
  // The grouped home menu resolves its sections against the LIVE palette
7038
- // registry (home-consolidation S1/S2) — the same source the TUI palette
7913
+ // registry — the same source the TUI palette
7039
7914
  // uses — so the menu cannot drift from the commander tree.
7040
7915
  commands: buildPaletteCommands(createProgram())
7041
7916
  };