@noir-ai/cli 1.9.4 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui/index.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  log,
25
25
  requireInteractive,
26
26
  spinner,
27
+ success,
27
28
  table,
28
29
  tip,
29
30
  useColor,
@@ -31,7 +32,7 @@ import {
31
32
  } from "./chunk-MD6WXZBF.js";
32
33
 
33
34
  // src/tui/index.tsx
34
- import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
35
+ import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
35
36
  import { render } from "ink";
36
37
 
37
38
  // src/bin.ts
@@ -45,7 +46,12 @@ import { Command, Option } from "commander";
45
46
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
46
47
  import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
47
48
  import { loadProjectInfo, NOIR_VERSION } from "@noir-ai/core";
48
- import { ensureDaemonRunning, pidAlive, readDaemonRecord } from "@noir-ai/daemon";
49
+ import {
50
+ ensureDaemonRunning,
51
+ pidAlive,
52
+ readDaemonRecord,
53
+ resolveGateConfig
54
+ } from "@noir-ai/daemon";
49
55
  import { createMemoryEngine } from "@noir-ai/memory";
50
56
  import { openStore } from "@noir-ai/store";
51
57
  import { WorkflowEngine } from "@noir-ai/workflow";
@@ -215,7 +221,12 @@ async function withInProcessRead(opts, fn) {
215
221
  embed,
216
222
  storeDegraded: true
217
223
  });
218
- const workflow = new WorkflowEngine(store, project.root, project.id);
224
+ const workflow = new WorkflowEngine(
225
+ store,
226
+ project.root,
227
+ project.id,
228
+ resolveGateConfig(project.config)
229
+ );
219
230
  return await fn({ context, memory, workflow });
220
231
  } finally {
221
232
  await store.close().catch(() => {
@@ -606,13 +617,13 @@ async function daemonRestart(opts) {
606
617
 
607
618
  // src/commands/handoff.ts
608
619
  import { mkdirSync, writeFileSync } from "fs";
609
- import { join } from "path";
620
+ import { join as join2 } from "path";
610
621
  import {
611
622
  defaultHandoffBlock,
612
623
  hostLaunchDirective,
613
624
  resolveAdapter
614
625
  } from "@noir-ai/adapters";
615
- import { loadProjectInfo as loadProjectInfo4, NOIR_DIR } from "@noir-ai/core";
626
+ import { loadProjectInfo as loadProjectInfo5, NOIR_DIR } from "@noir-ai/core";
616
627
 
617
628
  // src/commands/status.ts
618
629
  import { loadProjectInfo as loadProjectInfo3, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
@@ -729,6 +740,9 @@ function renderHuman(p, opts) {
729
740
  ],
730
741
  opts
731
742
  );
743
+ if (p.workflow && p.workflow.state !== "done" && p.workflow.state !== "abandoned") {
744
+ tip("resume: `noir task resume`", opts);
745
+ }
732
746
  }
733
747
  async function gatherStatusPayload(opts) {
734
748
  const project = loadProjectInfo3(process.cwd());
@@ -776,7 +790,17 @@ async function status(opts) {
776
790
  }
777
791
 
778
792
  // src/commands/task.ts
779
- import { PHASES } from "@noir-ai/workflow";
793
+ import { spawnSync } from "child_process";
794
+ import { createHash } from "crypto";
795
+ import { readdirSync } from "fs";
796
+ import { join } from "path";
797
+ import { loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
798
+ import {
799
+ PHASES,
800
+ TASK_CLASSES,
801
+ writeChangelogStub,
802
+ writeDecisionStub
803
+ } from "@noir-ai/workflow";
780
804
  var PHASE_SKILL = {
781
805
  // C3 curation (2026-08-10): intake+clarify merged into brainstorming;
782
806
  // execute → executing-plans; verify → verifying; document → wrap.
@@ -792,6 +816,24 @@ function skillFor(phase) {
792
816
  if (typeof phase !== "string") return null;
793
817
  return PHASE_SKILL[phase] ?? null;
794
818
  }
819
+ function writeDoneArtifacts(opts, taskId) {
820
+ try {
821
+ const project = loadProjectInfo4(process.cwd());
822
+ const conflict = { conflictPolicy: "preserve", interactive: false };
823
+ writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
824
+ const decisionsDir = join(project.root, ".noir", "decisions");
825
+ let nextN = 1;
826
+ try {
827
+ const files = readdirSync(decisionsDir);
828
+ const nums = files.map((f) => Number.parseInt(f.replace(/\D.*$/, ""), 10)).filter((n) => Number.isFinite(n) && n > 0);
829
+ if (nums.length > 0) nextN = Math.max(...nums) + 1;
830
+ } catch {
831
+ }
832
+ writeDecisionStub(project.root, nextN, taskId, conflict);
833
+ } catch {
834
+ tip("document-phase artifacts could not be written (use --no-artifacts to skip)", opts);
835
+ }
836
+ }
795
837
  async function fetchStatus(opts, taskId) {
796
838
  const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
797
839
  const res = await callDaemonTool(
@@ -810,17 +852,23 @@ function formatStamp(ms) {
810
852
  function renderStatusRow(s, opts) {
811
853
  const flag = s.degraded === true ? ` ${badge("warn", "degraded")}` : "";
812
854
  log(`task status \u2014 ${s.taskId}${flag}`, opts);
813
- definitionList(
814
- [
815
- { label: "Task", value: s.taskId },
816
- { label: "Phase", value: s.phase },
817
- { label: "State", value: s.state },
818
- { label: "Mode", value: s.mode },
819
- { label: "Next gate", value: s.nextGate ?? "\u2014" },
820
- { label: "Updated", value: formatStamp(s.updatedAt) }
821
- ],
822
- opts
855
+ const rows = [
856
+ { label: "Task", value: s.taskId },
857
+ { label: "Phase", value: s.phase },
858
+ { label: "State", value: s.state },
859
+ { label: "Mode", value: s.mode }
860
+ ];
861
+ if (typeof s.taskClass === "string" && s.taskClass.length > 0) {
862
+ rows.push({ label: "Class", value: s.taskClass });
863
+ }
864
+ if (typeof s.blockReason === "string" && s.blockReason.length > 0) {
865
+ rows.push({ label: "Block reason", value: s.blockReason });
866
+ }
867
+ rows.push(
868
+ { label: "Next gate", value: s.nextGate ?? "\u2014" },
869
+ { label: "Updated", value: formatStamp(s.updatedAt) }
823
870
  );
871
+ definitionList(rows, opts);
824
872
  }
825
873
  var GATE_PHASES = ["spec", "plan", "verify"];
826
874
  function nextGateAfter(phase) {
@@ -913,13 +961,25 @@ async function taskNew(opts) {
913
961
  }
914
962
  mode = opts.mode;
915
963
  }
964
+ let taskClass;
965
+ if (opts.taskClass !== void 0) {
966
+ if (!TASK_CLASSES.includes(opts.taskClass)) {
967
+ fail(
968
+ EXIT.USAGE,
969
+ `task new: invalid class '${opts.taskClass}' (expected one of: ${TASK_CLASSES.join(", ")})`,
970
+ opts
971
+ );
972
+ }
973
+ taskClass = opts.taskClass;
974
+ }
916
975
  const res = await callDaemonTool(
917
976
  opts,
918
977
  "workflow_start",
919
978
  {
920
979
  taskId: opts.slug,
921
980
  slug: opts.slug,
922
- ...mode === void 0 ? {} : { mode }
981
+ ...mode === void 0 ? {} : { mode },
982
+ ...taskClass === void 0 ? {} : { taskClass }
923
983
  }
924
984
  );
925
985
  if (res.ok !== true) {
@@ -953,6 +1013,9 @@ async function taskAdvance(opts) {
953
1013
  const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
954
1014
  fail(EXIT.ERROR, `task advance: ${detail}`, opts);
955
1015
  }
1016
+ if (res.state === "done" && opts.noArtifacts !== true) {
1017
+ writeDoneArtifacts(opts, res.taskId);
1018
+ }
956
1019
  if (typeof opts.to === "string" && opts.to === "verify") {
957
1020
  tip("run `noir handoff` for a ready-to-paste host prompt", opts);
958
1021
  }
@@ -963,6 +1026,272 @@ async function taskAdvance(opts) {
963
1026
  }
964
1027
  renderStatusRow(res, opts);
965
1028
  }
1029
+ function renderResumeBriefing(r, opts) {
1030
+ const flag = r.degraded === true ? ` ${badge("warn", "degraded")}` : "";
1031
+ log(`resume \u2014 ${r.taskId} (${r.state}, ${r.mode})${flag}`, opts);
1032
+ const rows = [
1033
+ { label: "Task", value: String(r.taskId) },
1034
+ { label: "Phase", value: String(r.phase) },
1035
+ { label: "State", value: String(r.state) },
1036
+ { label: "Mode", value: String(r.mode) }
1037
+ ];
1038
+ if (typeof r.taskClass === "string" && r.taskClass.length > 0) {
1039
+ rows.push({ label: "Class", value: r.taskClass });
1040
+ }
1041
+ if (typeof r.blockReason === "string" && r.blockReason.length > 0) {
1042
+ rows.push({ label: "Block reason", value: r.blockReason });
1043
+ }
1044
+ rows.push({ label: "Next gate", value: r.nextGate ?? "\u2014" });
1045
+ definitionList(rows, opts);
1046
+ const suggestion = skillFor(r.phase ?? null);
1047
+ if (suggestion) info(`skill for current phase (${r.phase}): ${suggestion}`, opts);
1048
+ if (r.state === "blocked") {
1049
+ info("resume a blocked task with: noir task advance --to <phase>", opts);
1050
+ }
1051
+ }
1052
+ async function taskResume(opts) {
1053
+ const args = {};
1054
+ if (typeof opts.id === "string" && opts.id.length > 0) {
1055
+ args.taskId = opts.id;
1056
+ }
1057
+ const res = await callDaemonTool(opts, "workflow_resume", args);
1058
+ if (res.resumable !== true) {
1059
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "nothing to resume";
1060
+ if (opts.json === true) {
1061
+ process.stdout.write(`${JSON.stringify({ ok: false, resumable: false, error: detail })}
1062
+ `);
1063
+ return;
1064
+ }
1065
+ fail(EXIT.ERROR, `task resume: ${detail}`, opts);
1066
+ }
1067
+ if (typeof opts.prompt === "string" && opts.prompt.length > 0) {
1068
+ info(`resume prompt: ${opts.prompt}`, opts);
1069
+ }
1070
+ if (opts.json === true) {
1071
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1072
+ `);
1073
+ return;
1074
+ }
1075
+ renderResumeBriefing(res, opts);
1076
+ }
1077
+ async function taskBlock(opts) {
1078
+ if (typeof opts.reason !== "string" || opts.reason.trim().length === 0) {
1079
+ fail(EXIT.USAGE, "task block: a non-empty <reason> is required", opts);
1080
+ }
1081
+ const args = { reason: opts.reason };
1082
+ if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
1083
+ const res = await callDaemonTool(
1084
+ opts,
1085
+ "workflow_block",
1086
+ args
1087
+ );
1088
+ if (res.ok !== true) {
1089
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "block failed";
1090
+ fail(EXIT.ERROR, `task block: ${detail}`, opts);
1091
+ }
1092
+ if (opts.json === true) {
1093
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1094
+ `);
1095
+ return;
1096
+ }
1097
+ renderStatusRow(res, opts);
1098
+ }
1099
+ async function taskAbandon(opts) {
1100
+ if (isInteractive(opts)) {
1101
+ const clack = await import("@clack/prompts");
1102
+ const confirm = await clack.confirm({
1103
+ message: "Abandon this task? This is terminal and cannot be undone.",
1104
+ initialValue: false
1105
+ });
1106
+ if (clack.isCancel(confirm)) {
1107
+ clack.cancel("Cancelled.");
1108
+ fail(EXIT.CANCELLED, "cancelled", opts);
1109
+ }
1110
+ if (confirm !== true) {
1111
+ fail(EXIT.CANCELLED, "abandon cancelled", opts);
1112
+ }
1113
+ }
1114
+ const args = {};
1115
+ if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
1116
+ const res = await callDaemonTool(
1117
+ opts,
1118
+ "workflow_abandon",
1119
+ args
1120
+ );
1121
+ if (res.ok !== true) {
1122
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "abandon failed";
1123
+ fail(EXIT.ERROR, `task abandon: ${detail}`, opts);
1124
+ }
1125
+ if (opts.json === true) {
1126
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1127
+ `);
1128
+ return;
1129
+ }
1130
+ renderStatusRow(res, opts);
1131
+ }
1132
+ function runCheck(cmd) {
1133
+ try {
1134
+ const buf = spawnSync("sh", ["-c", cmd], {
1135
+ stdio: ["ignore", "pipe", "pipe"],
1136
+ encoding: "utf8"
1137
+ });
1138
+ let out = "";
1139
+ if (buf.stdout) out += buf.stdout.toString();
1140
+ if (buf.stderr) out += buf.stderr.toString();
1141
+ const status2 = buf.status;
1142
+ return {
1143
+ exitCode: typeof status2 === "number" ? status2 : 1,
1144
+ digest: createHash("sha256").update(out).digest("hex")
1145
+ };
1146
+ } catch {
1147
+ return { exitCode: 1, digest: createHash("sha256").update("").digest("hex") };
1148
+ }
1149
+ }
1150
+ async function taskVerify(opts) {
1151
+ const project = loadProjectInfo4(process.cwd());
1152
+ const cfgChecks = project.config.workflow?.gate?.verify?.checks ?? [];
1153
+ const all = cfgChecks.map((c2) => ({
1154
+ name: c2.name,
1155
+ command: c2.command,
1156
+ tier: c2.tier ?? "hard"
1157
+ }));
1158
+ let checks = all;
1159
+ if (opts.check && opts.check.length > 0) {
1160
+ const want = new Set(opts.check);
1161
+ checks = all.filter((c2) => want.has(c2.name));
1162
+ if (checks.length === 0) {
1163
+ fail(
1164
+ EXIT.USAGE,
1165
+ `task verify: no configured checks match --check ${[...want].join(",")}`,
1166
+ opts
1167
+ );
1168
+ }
1169
+ }
1170
+ if (checks.length === 0) {
1171
+ fail(
1172
+ EXIT.USAGE,
1173
+ "task verify: no verify checks configured (set workflow.gate.verify.checks in noir.config)",
1174
+ opts
1175
+ );
1176
+ }
1177
+ const ranAt = Date.now();
1178
+ const evidence = {
1179
+ ranAt,
1180
+ checks: checks.map((c2) => {
1181
+ const r = runCheck(c2.command);
1182
+ return {
1183
+ name: c2.name,
1184
+ exitCode: r.exitCode,
1185
+ outputDigest: r.digest,
1186
+ command: c2.command,
1187
+ tier: c2.tier
1188
+ };
1189
+ }),
1190
+ summary: ""
1191
+ };
1192
+ const passed = evidence.checks.filter((c2) => c2.exitCode === 0).length;
1193
+ const failed = evidence.checks.length - passed;
1194
+ evidence.summary = `${passed} passed, ${failed} failed`;
1195
+ const res = await callDaemonTool(opts, "workflow_advance", { evidence });
1196
+ if (res.ok === true) {
1197
+ if (opts.json === true) {
1198
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res, evidence })}
1199
+ `);
1200
+ return;
1201
+ }
1202
+ success(`verify \u2014 ${evidence.summary} \u2192 gate approved`, opts);
1203
+ renderStatusRow(res, opts);
1204
+ return;
1205
+ }
1206
+ if (opts.json === true) {
1207
+ process.stdout.write(`${JSON.stringify({ evidence, ...res })}
1208
+ `);
1209
+ return;
1210
+ }
1211
+ if (res.pendingGate) {
1212
+ warn(`verify gate: ${res.pendingGate.reason} (${evidence.summary})`, opts);
1213
+ const recovery = res.recovery ?? ["retry", "force", "skip", "block"];
1214
+ info(
1215
+ `recovery: ${recovery.map(
1216
+ (r) => r === "retry" ? "`noir task verify`" : r === "force" ? "`noir task advance --force <reason>`" : r === "skip" ? "`noir task advance --skip`" : r === "block" ? "`noir task block <reason>`" : `\`${r}\``
1217
+ ).join(" | ")}`,
1218
+ opts
1219
+ );
1220
+ } else {
1221
+ fail(EXIT.ERROR, `task verify: ${res.error ?? "verify failed"}`, opts);
1222
+ }
1223
+ }
1224
+ async function taskResearch(opts) {
1225
+ const s = await fetchStatus(opts, opts.id);
1226
+ if (opts.json === true) {
1227
+ process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
1228
+ `);
1229
+ return;
1230
+ }
1231
+ renderStatusRow(s, opts);
1232
+ info("research findings are recorded via `noir task research record` (list + record)", opts);
1233
+ }
1234
+ async function taskResearchRecord(opts) {
1235
+ const TYPES = ["assumption", "discovery", "decision", "grounding-fact"];
1236
+ if (!TYPES.includes(opts.type)) {
1237
+ fail(
1238
+ EXIT.USAGE,
1239
+ `task research record: invalid type '${opts.type}' (expected one of: ${TYPES.join(", ")})`,
1240
+ opts
1241
+ );
1242
+ }
1243
+ const args = { type: opts.type, text: opts.text };
1244
+ if (typeof opts.source === "string" && opts.source.length > 0) args.source = opts.source;
1245
+ if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
1246
+ const res = await callDaemonTool(opts, "workflow_research_record", args);
1247
+ if (res.ok !== true) {
1248
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "record failed";
1249
+ fail(EXIT.ERROR, `task research record: ${detail}`, opts);
1250
+ }
1251
+ if (opts.json === true) {
1252
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1253
+ `);
1254
+ return;
1255
+ }
1256
+ success(
1257
+ `research recorded \u2192 ${opts.type}: ${opts.text.slice(0, 80)}${opts.text.length > 80 ? "\u2026" : ""}`,
1258
+ opts
1259
+ );
1260
+ }
1261
+ async function taskDecompose(opts) {
1262
+ const cap = opts.capability.trim();
1263
+ if (cap.length === 0) fail(EXIT.USAGE, "task decompose: <capability-id> is required", opts);
1264
+ const plan = {
1265
+ capabilityId: cap,
1266
+ intent: `Capability ${cap} \u2014 details TBD (run with a provider for an LLM-drafted plan)`,
1267
+ slices: [
1268
+ {
1269
+ id: "s1-walking-skeleton",
1270
+ title: "Walking skeleton \u2014 thinnest end-to-end",
1271
+ type: "feature",
1272
+ rationale: `The first slice for capability ${cap}.`,
1273
+ scopeIn: "End-to-end path linking the main architecture.",
1274
+ scopeOut: "Polish, edge cases, full coverage.",
1275
+ dependsOn: [],
1276
+ files: { create: [], modify: [], preserve: [] },
1277
+ acceptance: [`verify acceptance criteria for capability ${cap}`],
1278
+ doD: ["pnpm test", "pnpm typecheck"],
1279
+ rollbackPlan: { procedure: "revert the slice commit", verifyCommand: "pnpm test" }
1280
+ }
1281
+ ],
1282
+ status: { "s1-walking-skeleton": "planned" }
1283
+ };
1284
+ if (opts.json === true) {
1285
+ process.stdout.write(`${JSON.stringify({ ok: true, data: plan })}
1286
+ `);
1287
+ return;
1288
+ }
1289
+ log(`decompose \u2014 ${cap}`, opts);
1290
+ for (const s of plan.slices) {
1291
+ info(`${s.id}: ${s.title} (${s.type}) \u2014 rationale: ${s.rationale}`, opts);
1292
+ }
1293
+ tip("each slice enters the existing clarify\u2192spec\u2192plan\u2192execute\u2192verify\u2192document FSM cycle", opts);
1294
+ }
966
1295
 
967
1296
  // src/commands/handoff.ts
968
1297
  var SEED_LIMIT = 5;
@@ -1070,7 +1399,7 @@ function resolveDirective(root, p) {
1070
1399
  }
1071
1400
  function handoffFilePath(root, taskId, projectId) {
1072
1401
  const id = typeof taskId === "string" && taskId.length > 0 ? taskId : projectId;
1073
- return join(root, NOIR_DIR, "handoff", `${id}.md`);
1402
+ return join2(root, NOIR_DIR, "handoff", `${id}.md`);
1074
1403
  }
1075
1404
  async function extractSeeds(opts, probe, query) {
1076
1405
  if (!probe.running) return { context: null, memory: null };
@@ -1102,7 +1431,7 @@ async function probeOnly(opts) {
1102
1431
  return probeDaemon(opts);
1103
1432
  }
1104
1433
  async function handoff(opts) {
1105
- const project = loadProjectInfo4(process.cwd());
1434
+ const project = loadProjectInfo5(process.cwd());
1106
1435
  const host = project.config.host;
1107
1436
  const snapshot = await gatherStatusPayload(opts);
1108
1437
  const probe = await probeOnly(opts);
@@ -1136,7 +1465,7 @@ async function handoff(opts) {
1136
1465
  const md = renderMarkdown(full);
1137
1466
  if (opts.write === true) {
1138
1467
  const path = handoffFilePath(project.root, task?.taskId, project.id);
1139
- mkdirSync(join(project.root, NOIR_DIR, "handoff"), { recursive: true });
1468
+ mkdirSync(join2(project.root, NOIR_DIR, "handoff"), { recursive: true });
1140
1469
  writeFileSync(path, md, "utf8");
1141
1470
  info(`handoff written to ${path}`, opts);
1142
1471
  return;
@@ -1147,7 +1476,7 @@ async function handoff(opts) {
1147
1476
  // src/commands/home.ts
1148
1477
  import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
1149
1478
  import {
1150
- loadProjectInfo as loadProjectInfo5,
1479
+ loadProjectInfo as loadProjectInfo6,
1151
1480
  NOIR_VERSION as NOIR_VERSION3,
1152
1481
  readInstallRecord
1153
1482
  } from "@noir-ai/core";
@@ -1445,7 +1774,7 @@ async function resolveSections(commands) {
1445
1774
  // src/commands/home.ts
1446
1775
  function tryProject() {
1447
1776
  try {
1448
- const info2 = loadProjectInfo5(process.cwd());
1777
+ const info2 = loadProjectInfo6(process.cwd());
1449
1778
  return { id: info2.id, name: info2.name, host: info2.config.host };
1450
1779
  } catch {
1451
1780
  return null;
@@ -1609,7 +1938,7 @@ async function runMenu(opts, deps) {
1609
1938
  env: process.env,
1610
1939
  configUpdate: (() => {
1611
1940
  try {
1612
- const info2 = loadProjectInfo5(process.cwd());
1941
+ const info2 = loadProjectInfo6(process.cwd());
1613
1942
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
1614
1943
  } catch {
1615
1944
  return DEFAULT_UPDATE_CONFIG;
@@ -1905,9 +2234,105 @@ async function memoryConsolidate(opts) {
1905
2234
  fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
1906
2235
  }
1907
2236
 
2237
+ // src/commands/release.ts
2238
+ import { execSync, spawnSync as spawnSync2 } from "child_process";
2239
+ function describe(step, who) {
2240
+ const m = {
2241
+ preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
2242
+ bump: "run bump-version.mjs + update CHANGELOG + roadmap docs",
2243
+ gate: "re-run the full gate (lint/build/typecheck/test/docs:validate)",
2244
+ commit: "commit chore(release) + push develop",
2245
+ "ci-develop": "wait for CI on develop \u2192 green",
2246
+ "beta-tag": "pnpm release:tag \u2192 beta tag + push",
2247
+ "human-approve-beta": "approve the release workflow publish job in GitHub Actions",
2248
+ "merge-main": "merge develop \u2192 main (fast-forward) + push",
2249
+ "ci-main": "wait for CI on main \u2192 green",
2250
+ "stable-tag": "pnpm release:tag \u2192 stable tag + push",
2251
+ "human-approve-stable": "approve the release workflow publish job in GitHub Actions (again)",
2252
+ "dist-bump": "update Homebrew (packaging/homebrew/noir.rb) + Scoop (packaging/scoop/noir.json) from npm",
2253
+ sync: "merge main \u2192 develop + verify both branches at same SHA"
2254
+ };
2255
+ return `${m[step]} [${who}]`;
2256
+ }
2257
+ var ALL = [
2258
+ "preflight",
2259
+ "bump",
2260
+ "gate",
2261
+ "commit",
2262
+ "ci-develop",
2263
+ "beta-tag",
2264
+ "human-approve-beta",
2265
+ "merge-main",
2266
+ "ci-main",
2267
+ "stable-tag",
2268
+ "human-approve-stable",
2269
+ "dist-bump",
2270
+ "sync"
2271
+ ];
2272
+ async function release(opts) {
2273
+ if (opts.dryRun) {
2274
+ log("noir release --dry-run (no changes will be made)", opts);
2275
+ for (const s of ALL) {
2276
+ const who = s.startsWith("human-approve") ? "human" : "noir";
2277
+ info(`${s}: ${describe(s, who)}`, opts);
2278
+ }
2279
+ tip('full release flow documented in CLAUDE.md "Patch release flow (beta \u2192 stable)"', opts);
2280
+ return;
2281
+ }
2282
+ const version = opts.version?.trim();
2283
+ if (!version) {
2284
+ fail(EXIT.USAGE, "release: <version> is required (e.g. noir release 1.10.0)", opts);
2285
+ }
2286
+ const channel = opts.channel === "stable" ? "stable" : "beta";
2287
+ log(`noir release ${version} (${channel} channel)`, opts);
2288
+ try {
2289
+ info(`[1/13] preflight: ${describe("preflight", "noir")}`, opts);
2290
+ execSync("git diff --quiet", { encoding: "utf8" });
2291
+ const branch = execSync("git branch --show-current", { encoding: "utf8" }).trim();
2292
+ if (branch !== "develop") fail(EXIT.ERROR, "release: must be on develop", opts);
2293
+ const status2 = execSync("git status --porcelain", { encoding: "utf8" }).trim();
2294
+ if (status2) fail(EXIT.ERROR, "release: tree must be clean", opts);
2295
+ success("preflight: \u2713 clean, on develop", opts);
2296
+ info(`[2/13] bump: ${describe("bump", "noir")}`, opts);
2297
+ spawnSync2("node", ["scripts/bump-version.mjs", version], {
2298
+ encoding: "utf8",
2299
+ stdio: "inherit"
2300
+ });
2301
+ success(`bump: \u2713 version ${version}`, opts);
2302
+ info(`[3/13] gate: ${describe("gate", "noir")}`, opts);
2303
+ execSync("pnpm lint && pnpm build && pnpm typecheck && pnpm test && pnpm docs:validate", {
2304
+ encoding: "utf8",
2305
+ stdio: "inherit"
2306
+ });
2307
+ success("gate: \u2713 full gate green", opts);
2308
+ info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
2309
+ spawnSync2("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
2310
+ spawnSync2("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
2311
+ encoding: "utf8",
2312
+ stdio: "inherit"
2313
+ });
2314
+ execSync("git push origin develop", { encoding: "utf8" });
2315
+ success("commit: \u2713 pushed to develop", opts);
2316
+ info(`[5/13] ci-develop: waiting for CI on develop\u2026`, opts);
2317
+ info(`[6/13] beta-tag: ${describe("beta-tag", "noir")}`, opts);
2318
+ tip("after CI green: run `pnpm release:tag` and push the beta tag", opts);
2319
+ warn(`[7/13] ${describe("human-approve-beta", "human")}`, opts);
2320
+ tip("open GitHub Actions \u2192 approve the publish job, then continue", opts);
2321
+ tip("after approval: verify `npm view @noir-ai/cli dist-tags beta` = beta version", opts);
2322
+ info("remaining steps (merge\u2192stable\u2192dist\u2192sync): see CLAUDE.md", opts);
2323
+ for (const s of ALL.slice(7)) {
2324
+ const who = s.startsWith("human-approve") ? "human" : "noir";
2325
+ info(` ${s}: ${describe(s, who)}`, opts);
2326
+ }
2327
+ success(`release ${version}: preflight\u2192bump\u2192gate\u2192commit done. Resume after approval.`, opts);
2328
+ } catch (err) {
2329
+ fail(EXIT.ERROR, `release failed: ${err instanceof Error ? err.message : String(err)}`, opts);
2330
+ }
2331
+ }
2332
+
1908
2333
  // src/commands/skills.ts
1909
2334
  import { claudeAdapter } from "@noir-ai/adapters";
1910
- import { loadProjectInfo as loadProjectInfo6 } from "@noir-ai/core";
2335
+ import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
1911
2336
  import {
1912
2337
  buildRegistry,
1913
2338
  discoverAll,
@@ -2001,7 +2426,7 @@ async function skillsList(opts) {
2001
2426
  }
2002
2427
  async function skillsSync(opts) {
2003
2428
  const root = process.cwd();
2004
- const project = loadProjectInfo6(root);
2429
+ const project = loadProjectInfo7(root);
2005
2430
  if (!claudeAdapter.skillsDir) {
2006
2431
  const data2 = {
2007
2432
  emitted: [],
@@ -2114,10 +2539,10 @@ async function skillsRegistry(opts) {
2114
2539
  }
2115
2540
 
2116
2541
  // src/serve.ts
2117
- import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
2542
+ import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
2118
2543
  import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
2119
2544
  async function serve(opts) {
2120
- const project = loadProjectInfo7(process.cwd());
2545
+ const project = loadProjectInfo8(process.cwd());
2121
2546
  if (opts.stdio) {
2122
2547
  await startStdioServer({ project, transport: "stdio", daemon: false });
2123
2548
  return;
@@ -2495,15 +2920,20 @@ function createProgram() {
2495
2920
  throw new NoirCliError(EXIT.USAGE, "Usage: noir skills list|sync|lint|registry");
2496
2921
  });
2497
2922
  const taskGrp = program2.command("task").description("workflow task control");
2498
- taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").action(async (...args) => {
2923
+ taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").option(
2924
+ "--class <taskClass>",
2925
+ "task class (feature/epic/enhancement/bugfix/spike/quick-task/refactor) \u2014 drives the PRD gate"
2926
+ ).action(async (...args) => {
2499
2927
  const cmd = trailingCmd(args);
2500
2928
  const g = cmd.optsWithGlobals();
2501
2929
  const slug = typeof g.slug === "string" ? g.slug : "";
2502
2930
  const mode = typeof g.mode === "string" ? g.mode : void 0;
2931
+ const taskClass = typeof g.class === "string" ? g.class : void 0;
2503
2932
  await taskNew({
2504
2933
  ...toCliOptions(g),
2505
2934
  slug,
2506
- ...mode === void 0 ? {} : { mode }
2935
+ ...mode === void 0 ? {} : { mode },
2936
+ ...taskClass === void 0 ? {} : { taskClass }
2507
2937
  });
2508
2938
  });
2509
2939
  taskGrp.command("status").description("active (or named) task status").argument("[id]", "task id (defaults to active)").action(async (...args) => {
@@ -2516,7 +2946,7 @@ function createProgram() {
2516
2946
  ...id === void 0 ? {} : { id }
2517
2947
  });
2518
2948
  });
2519
- taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").action(async (...args) => {
2949
+ taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").option("--no-artifacts", "skip the document-phase artifact writes at done").action(async (...args) => {
2520
2950
  const cmd = trailingCmd(args);
2521
2951
  const g = cmd.optsWithGlobals();
2522
2952
  const to = typeof g.to === "string" ? g.to : void 0;
@@ -2524,14 +2954,99 @@ function createProgram() {
2524
2954
  await taskAdvance({
2525
2955
  ...toCliOptions(g),
2526
2956
  ...to === void 0 ? {} : { to },
2527
- ...force === void 0 ? {} : { force }
2957
+ ...force === void 0 ? {} : { force },
2958
+ ...g.artifacts === false ? { noArtifacts: true } : {}
2528
2959
  });
2529
2960
  });
2530
2961
  taskGrp.command("next").description("suggest the next phase + applicable skill").action(async (...args) => {
2531
2962
  await taskNext(toCliOptions(trailingCmd(args).optsWithGlobals()));
2532
2963
  });
2964
+ taskGrp.command("decompose").description(
2965
+ "decompose a capability into buildable slices (template-only offline; see the spec)"
2966
+ ).argument("<capability>", "capability id, e.g. C4").option("--out <path>", "output path for the SlicePlan JSON").action(async (...args) => {
2967
+ const cmd = trailingCmd(args);
2968
+ const g = cmd.optsWithGlobals();
2969
+ const capability = typeof args[0] === "string" ? args[0] : "";
2970
+ await taskDecompose({
2971
+ ...toCliOptions(g),
2972
+ capability,
2973
+ ...typeof g.out === "string" && g.out.length > 0 ? { out: g.out } : {}
2974
+ });
2975
+ });
2976
+ taskGrp.command("verify").description("run configured verify checks and submit evidence to the verify gate").option(
2977
+ "--check <name>",
2978
+ "restrict to a named check (repeatable)",
2979
+ (val, acc) => [...acc, val],
2980
+ []
2981
+ ).action(async (...args) => {
2982
+ const cmd = trailingCmd(args);
2983
+ const g = cmd.optsWithGlobals();
2984
+ const check = Array.isArray(g.check) ? g.check : void 0;
2985
+ await taskVerify({
2986
+ ...toCliOptions(g),
2987
+ ...check === void 0 || check.length === 0 ? {} : { check }
2988
+ });
2989
+ });
2990
+ taskGrp.command("research").description("list research findings for the active (or named) task").argument("[id]", "task id (defaults to active)").action(async (...args) => {
2991
+ const cmd = trailingCmd(args);
2992
+ const g = cmd.optsWithGlobals();
2993
+ const first = args[0];
2994
+ const id = typeof first === "string" ? first : void 0;
2995
+ await taskResearch({
2996
+ ...toCliOptions(g),
2997
+ ...id === void 0 ? {} : { id }
2998
+ });
2999
+ });
3000
+ taskGrp.command("research-record").description("record a research finding for the active (or named) task").requiredOption("--type <type>", "assumption | discovery | decision | grounding-fact").requiredOption("--text <text>", "finding text (capped)").option("--source <ref>", "evidence/citation (required unless grounding-fact)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
3001
+ const cmd = trailingCmd(args);
3002
+ const g = cmd.optsWithGlobals();
3003
+ await taskResearchRecord({
3004
+ ...toCliOptions(g),
3005
+ type: String(g.type ?? ""),
3006
+ text: String(g.text ?? ""),
3007
+ ...typeof g.source === "string" && g.source.length > 0 ? { source: g.source } : {},
3008
+ ...typeof g.task === "string" && g.task.length > 0 ? { task: g.task } : {}
3009
+ });
3010
+ });
3011
+ taskGrp.command("resume").description("resume the active (or named) in-flight/blocked task").argument("[id]", "task id (defaults to active)").option("--last", "target the active task explicitly (scripting)").option("--prompt <text>", "a continue instruction to surface in the briefing").action(async (...args) => {
3012
+ const cmd = trailingCmd(args);
3013
+ const g = cmd.optsWithGlobals();
3014
+ const first = args[0];
3015
+ const id = typeof first === "string" ? first : void 0;
3016
+ const last = g.last === true;
3017
+ const prompt = typeof g.prompt === "string" ? g.prompt : void 0;
3018
+ await taskResume({
3019
+ ...toCliOptions(g),
3020
+ ...id === void 0 ? {} : { id },
3021
+ ...last ? { last } : {},
3022
+ ...prompt === void 0 ? {} : { prompt }
3023
+ });
3024
+ });
3025
+ taskGrp.command("block").description("mark the active (or named) task blocked with a reason").argument("<reason>", "why the task is stuck (non-empty)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
3026
+ const cmd = trailingCmd(args);
3027
+ const g = cmd.optsWithGlobals();
3028
+ const reason = typeof args[0] === "string" ? args[0] : "";
3029
+ const task = typeof g.task === "string" ? g.task : void 0;
3030
+ await taskBlock({
3031
+ ...toCliOptions(g),
3032
+ reason,
3033
+ ...task === void 0 ? {} : { task }
3034
+ });
3035
+ });
3036
+ taskGrp.command("abandon").description("abandon the active (or named) task (terminal, confirmed)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
3037
+ const cmd = trailingCmd(args);
3038
+ const g = cmd.optsWithGlobals();
3039
+ const task = typeof g.task === "string" ? g.task : void 0;
3040
+ await taskAbandon({
3041
+ ...toCliOptions(g),
3042
+ ...task === void 0 ? {} : { task }
3043
+ });
3044
+ });
2533
3045
  taskGrp.action(() => {
2534
- throw new NoirCliError(EXIT.USAGE, "Usage: noir task new|status|advance|next");
3046
+ throw new NoirCliError(
3047
+ EXIT.USAGE,
3048
+ "Usage: noir task new|status|advance|next|verify|research|research-record|resume|block|abandon"
3049
+ );
2535
3050
  });
2536
3051
  const installCmd = program2.command("install").description(
2537
3052
  "install Noir via the native managed-Node path (or migrate from another install method)"
@@ -2566,6 +3081,16 @@ function createProgram() {
2566
3081
  program2.command("wrap").description("session-end alias for `noir handoff`").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
2567
3082
  await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
2568
3083
  });
3084
+ program2.command("release").description("guided release orchestrator over the patch-release flow").argument("[version]", "target version, e.g. 1.10.0").option("--channel <channel>", "beta (default) | stable").option("--dry-run", "print the checklist without executing any steps").action(async (...args) => {
3085
+ const cmd = trailingCmd(args);
3086
+ const g = cmd.optsWithGlobals();
3087
+ await release({
3088
+ ...toCliOptions(g),
3089
+ ...typeof args[0] === "string" && args[0].length > 0 ? { version: args[0] } : {},
3090
+ ...typeof g.channel === "string" && g.channel.length > 0 ? { channel: g.channel } : {},
3091
+ ...g.dryRun === true ? { dryRun: true } : {}
3092
+ });
3093
+ });
2569
3094
  program2.command("tui").description("interactive Ink dashboard (host \xB7 phase \xB7 daemon + /command dispatch)").action(async (...args) => {
2570
3095
  const g = actionGlobals(args);
2571
3096
  const opts = toCliOptions(g);
@@ -3617,16 +4142,16 @@ function Help() {
3617
4142
  // src/tui/palette/history.ts
3618
4143
  import { existsSync, mkdirSync as mkdirSync2, readFileSync } from "fs";
3619
4144
  import { homedir } from "os";
3620
- import { join as join2 } from "path";
4145
+ import { join as join3 } from "path";
3621
4146
  import { atomicWriteFile } from "@noir-ai/core";
3622
4147
  var MAX_ENTRIES = 50;
3623
4148
  var TUI_HISTORY_DISABLE_ENV = "NOIR_DISABLE_TUI_HISTORY";
3624
4149
  var testHome = null;
3625
4150
  function homeDir() {
3626
- return testHome ?? join2(homedir(), ".noir");
4151
+ return testHome ?? join3(homedir(), ".noir");
3627
4152
  }
3628
4153
  function historyFile(projectId) {
3629
- return join2(homeDir(), projectId, "tui-history.json");
4154
+ return join3(homeDir(), projectId, "tui-history.json");
3630
4155
  }
3631
4156
  function isHistoryEnabled() {
3632
4157
  const v = process.env[TUI_HISTORY_DISABLE_ENV];
@@ -3661,7 +4186,7 @@ function recordRecent(projectId, argv) {
3661
4186
  const current = loadRecent(projectId).filter((e) => e.id !== entry.id);
3662
4187
  const next = [entry, ...current].slice(0, MAX_ENTRIES);
3663
4188
  try {
3664
- const dir = join2(homeDir(), projectId);
4189
+ const dir = join3(homeDir(), projectId);
3665
4190
  mkdirSync2(dir, { recursive: true });
3666
4191
  atomicWriteFile(historyFile(projectId), `${JSON.stringify(next, null, 2)}
3667
4192
  `);
@@ -3695,7 +4220,7 @@ async function runPalette(opts, dispatch) {
3695
4220
  async function buildTuiDeps(opts, dispatch) {
3696
4221
  let projectId = null;
3697
4222
  try {
3698
- projectId = loadProjectInfo8(process.cwd()).id;
4223
+ projectId = loadProjectInfo9(process.cwd()).id;
3699
4224
  } catch {
3700
4225
  projectId = null;
3701
4226
  }