@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/bin.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  log,
28
28
  requireInteractive,
29
29
  spinner,
30
+ success,
30
31
  table,
31
32
  tip,
32
33
  useColor,
@@ -44,7 +45,12 @@ import { Command, Option } from "commander";
44
45
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
45
46
  import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
46
47
  import { loadProjectInfo, NOIR_VERSION } from "@noir-ai/core";
47
- import { ensureDaemonRunning, pidAlive, readDaemonRecord } from "@noir-ai/daemon";
48
+ import {
49
+ ensureDaemonRunning,
50
+ pidAlive,
51
+ readDaemonRecord,
52
+ resolveGateConfig
53
+ } from "@noir-ai/daemon";
48
54
  import { createMemoryEngine } from "@noir-ai/memory";
49
55
  import { openStore } from "@noir-ai/store";
50
56
  import { WorkflowEngine } from "@noir-ai/workflow";
@@ -214,7 +220,12 @@ async function withInProcessRead(opts, fn) {
214
220
  embed,
215
221
  storeDegraded: true
216
222
  });
217
- const workflow = new WorkflowEngine(store, project.root, project.id);
223
+ const workflow = new WorkflowEngine(
224
+ store,
225
+ project.root,
226
+ project.id,
227
+ resolveGateConfig(project.config)
228
+ );
218
229
  return await fn({ context, memory, workflow });
219
230
  } finally {
220
231
  await store.close().catch(() => {
@@ -605,13 +616,13 @@ async function daemonRestart(opts) {
605
616
 
606
617
  // src/commands/handoff.ts
607
618
  import { mkdirSync, writeFileSync } from "fs";
608
- import { join } from "path";
619
+ import { join as join2 } from "path";
609
620
  import {
610
621
  defaultHandoffBlock,
611
622
  hostLaunchDirective,
612
623
  resolveAdapter
613
624
  } from "@noir-ai/adapters";
614
- import { loadProjectInfo as loadProjectInfo4, NOIR_DIR } from "@noir-ai/core";
625
+ import { loadProjectInfo as loadProjectInfo5, NOIR_DIR } from "@noir-ai/core";
615
626
 
616
627
  // src/commands/status.ts
617
628
  import { loadProjectInfo as loadProjectInfo3, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
@@ -728,6 +739,9 @@ function renderHuman(p, opts) {
728
739
  ],
729
740
  opts
730
741
  );
742
+ if (p.workflow && p.workflow.state !== "done" && p.workflow.state !== "abandoned") {
743
+ tip("resume: `noir task resume`", opts);
744
+ }
731
745
  }
732
746
  async function gatherStatusPayload(opts) {
733
747
  const project = loadProjectInfo3(process.cwd());
@@ -775,7 +789,17 @@ async function status(opts) {
775
789
  }
776
790
 
777
791
  // src/commands/task.ts
778
- import { PHASES } from "@noir-ai/workflow";
792
+ import { spawnSync } from "child_process";
793
+ import { createHash } from "crypto";
794
+ import { readdirSync } from "fs";
795
+ import { join } from "path";
796
+ import { loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
797
+ import {
798
+ PHASES,
799
+ TASK_CLASSES,
800
+ writeChangelogStub,
801
+ writeDecisionStub
802
+ } from "@noir-ai/workflow";
779
803
  var PHASE_SKILL = {
780
804
  // C3 curation (2026-08-10): intake+clarify merged into brainstorming;
781
805
  // execute → executing-plans; verify → verifying; document → wrap.
@@ -791,6 +815,24 @@ function skillFor(phase) {
791
815
  if (typeof phase !== "string") return null;
792
816
  return PHASE_SKILL[phase] ?? null;
793
817
  }
818
+ function writeDoneArtifacts(opts, taskId) {
819
+ try {
820
+ const project = loadProjectInfo4(process.cwd());
821
+ const conflict = { conflictPolicy: "preserve", interactive: false };
822
+ writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
823
+ const decisionsDir = join(project.root, ".noir", "decisions");
824
+ let nextN = 1;
825
+ try {
826
+ const files = readdirSync(decisionsDir);
827
+ const nums = files.map((f) => Number.parseInt(f.replace(/\D.*$/, ""), 10)).filter((n) => Number.isFinite(n) && n > 0);
828
+ if (nums.length > 0) nextN = Math.max(...nums) + 1;
829
+ } catch {
830
+ }
831
+ writeDecisionStub(project.root, nextN, taskId, conflict);
832
+ } catch {
833
+ tip("document-phase artifacts could not be written (use --no-artifacts to skip)", opts);
834
+ }
835
+ }
794
836
  async function fetchStatus(opts, taskId) {
795
837
  const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
796
838
  const res = await callDaemonTool(
@@ -809,17 +851,23 @@ function formatStamp(ms) {
809
851
  function renderStatusRow(s, opts) {
810
852
  const flag = s.degraded === true ? ` ${badge("warn", "degraded")}` : "";
811
853
  log(`task status \u2014 ${s.taskId}${flag}`, opts);
812
- definitionList(
813
- [
814
- { label: "Task", value: s.taskId },
815
- { label: "Phase", value: s.phase },
816
- { label: "State", value: s.state },
817
- { label: "Mode", value: s.mode },
818
- { label: "Next gate", value: s.nextGate ?? "\u2014" },
819
- { label: "Updated", value: formatStamp(s.updatedAt) }
820
- ],
821
- opts
854
+ const rows = [
855
+ { label: "Task", value: s.taskId },
856
+ { label: "Phase", value: s.phase },
857
+ { label: "State", value: s.state },
858
+ { label: "Mode", value: s.mode }
859
+ ];
860
+ if (typeof s.taskClass === "string" && s.taskClass.length > 0) {
861
+ rows.push({ label: "Class", value: s.taskClass });
862
+ }
863
+ if (typeof s.blockReason === "string" && s.blockReason.length > 0) {
864
+ rows.push({ label: "Block reason", value: s.blockReason });
865
+ }
866
+ rows.push(
867
+ { label: "Next gate", value: s.nextGate ?? "\u2014" },
868
+ { label: "Updated", value: formatStamp(s.updatedAt) }
822
869
  );
870
+ definitionList(rows, opts);
823
871
  }
824
872
  var GATE_PHASES = ["spec", "plan", "verify"];
825
873
  function nextGateAfter(phase) {
@@ -912,13 +960,25 @@ async function taskNew(opts) {
912
960
  }
913
961
  mode = opts.mode;
914
962
  }
963
+ let taskClass;
964
+ if (opts.taskClass !== void 0) {
965
+ if (!TASK_CLASSES.includes(opts.taskClass)) {
966
+ fail(
967
+ EXIT.USAGE,
968
+ `task new: invalid class '${opts.taskClass}' (expected one of: ${TASK_CLASSES.join(", ")})`,
969
+ opts
970
+ );
971
+ }
972
+ taskClass = opts.taskClass;
973
+ }
915
974
  const res = await callDaemonTool(
916
975
  opts,
917
976
  "workflow_start",
918
977
  {
919
978
  taskId: opts.slug,
920
979
  slug: opts.slug,
921
- ...mode === void 0 ? {} : { mode }
980
+ ...mode === void 0 ? {} : { mode },
981
+ ...taskClass === void 0 ? {} : { taskClass }
922
982
  }
923
983
  );
924
984
  if (res.ok !== true) {
@@ -952,6 +1012,9 @@ async function taskAdvance(opts) {
952
1012
  const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
953
1013
  fail(EXIT.ERROR, `task advance: ${detail}`, opts);
954
1014
  }
1015
+ if (res.state === "done" && opts.noArtifacts !== true) {
1016
+ writeDoneArtifacts(opts, res.taskId);
1017
+ }
955
1018
  if (typeof opts.to === "string" && opts.to === "verify") {
956
1019
  tip("run `noir handoff` for a ready-to-paste host prompt", opts);
957
1020
  }
@@ -962,6 +1025,272 @@ async function taskAdvance(opts) {
962
1025
  }
963
1026
  renderStatusRow(res, opts);
964
1027
  }
1028
+ function renderResumeBriefing(r, opts) {
1029
+ const flag = r.degraded === true ? ` ${badge("warn", "degraded")}` : "";
1030
+ log(`resume \u2014 ${r.taskId} (${r.state}, ${r.mode})${flag}`, opts);
1031
+ const rows = [
1032
+ { label: "Task", value: String(r.taskId) },
1033
+ { label: "Phase", value: String(r.phase) },
1034
+ { label: "State", value: String(r.state) },
1035
+ { label: "Mode", value: String(r.mode) }
1036
+ ];
1037
+ if (typeof r.taskClass === "string" && r.taskClass.length > 0) {
1038
+ rows.push({ label: "Class", value: r.taskClass });
1039
+ }
1040
+ if (typeof r.blockReason === "string" && r.blockReason.length > 0) {
1041
+ rows.push({ label: "Block reason", value: r.blockReason });
1042
+ }
1043
+ rows.push({ label: "Next gate", value: r.nextGate ?? "\u2014" });
1044
+ definitionList(rows, opts);
1045
+ const suggestion = skillFor(r.phase ?? null);
1046
+ if (suggestion) info(`skill for current phase (${r.phase}): ${suggestion}`, opts);
1047
+ if (r.state === "blocked") {
1048
+ info("resume a blocked task with: noir task advance --to <phase>", opts);
1049
+ }
1050
+ }
1051
+ async function taskResume(opts) {
1052
+ const args = {};
1053
+ if (typeof opts.id === "string" && opts.id.length > 0) {
1054
+ args.taskId = opts.id;
1055
+ }
1056
+ const res = await callDaemonTool(opts, "workflow_resume", args);
1057
+ if (res.resumable !== true) {
1058
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "nothing to resume";
1059
+ if (opts.json === true) {
1060
+ process.stdout.write(`${JSON.stringify({ ok: false, resumable: false, error: detail })}
1061
+ `);
1062
+ return;
1063
+ }
1064
+ fail(EXIT.ERROR, `task resume: ${detail}`, opts);
1065
+ }
1066
+ if (typeof opts.prompt === "string" && opts.prompt.length > 0) {
1067
+ info(`resume prompt: ${opts.prompt}`, opts);
1068
+ }
1069
+ if (opts.json === true) {
1070
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1071
+ `);
1072
+ return;
1073
+ }
1074
+ renderResumeBriefing(res, opts);
1075
+ }
1076
+ async function taskBlock(opts) {
1077
+ if (typeof opts.reason !== "string" || opts.reason.trim().length === 0) {
1078
+ fail(EXIT.USAGE, "task block: a non-empty <reason> is required", opts);
1079
+ }
1080
+ const args = { reason: opts.reason };
1081
+ if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
1082
+ const res = await callDaemonTool(
1083
+ opts,
1084
+ "workflow_block",
1085
+ args
1086
+ );
1087
+ if (res.ok !== true) {
1088
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "block failed";
1089
+ fail(EXIT.ERROR, `task block: ${detail}`, opts);
1090
+ }
1091
+ if (opts.json === true) {
1092
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1093
+ `);
1094
+ return;
1095
+ }
1096
+ renderStatusRow(res, opts);
1097
+ }
1098
+ async function taskAbandon(opts) {
1099
+ if (isInteractive(opts)) {
1100
+ const clack = await import("@clack/prompts");
1101
+ const confirm = await clack.confirm({
1102
+ message: "Abandon this task? This is terminal and cannot be undone.",
1103
+ initialValue: false
1104
+ });
1105
+ if (clack.isCancel(confirm)) {
1106
+ clack.cancel("Cancelled.");
1107
+ fail(EXIT.CANCELLED, "cancelled", opts);
1108
+ }
1109
+ if (confirm !== true) {
1110
+ fail(EXIT.CANCELLED, "abandon cancelled", opts);
1111
+ }
1112
+ }
1113
+ const args = {};
1114
+ if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
1115
+ const res = await callDaemonTool(
1116
+ opts,
1117
+ "workflow_abandon",
1118
+ args
1119
+ );
1120
+ if (res.ok !== true) {
1121
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "abandon failed";
1122
+ fail(EXIT.ERROR, `task abandon: ${detail}`, opts);
1123
+ }
1124
+ if (opts.json === true) {
1125
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1126
+ `);
1127
+ return;
1128
+ }
1129
+ renderStatusRow(res, opts);
1130
+ }
1131
+ function runCheck(cmd) {
1132
+ try {
1133
+ const buf = spawnSync("sh", ["-c", cmd], {
1134
+ stdio: ["ignore", "pipe", "pipe"],
1135
+ encoding: "utf8"
1136
+ });
1137
+ let out = "";
1138
+ if (buf.stdout) out += buf.stdout.toString();
1139
+ if (buf.stderr) out += buf.stderr.toString();
1140
+ const status2 = buf.status;
1141
+ return {
1142
+ exitCode: typeof status2 === "number" ? status2 : 1,
1143
+ digest: createHash("sha256").update(out).digest("hex")
1144
+ };
1145
+ } catch {
1146
+ return { exitCode: 1, digest: createHash("sha256").update("").digest("hex") };
1147
+ }
1148
+ }
1149
+ async function taskVerify(opts) {
1150
+ const project = loadProjectInfo4(process.cwd());
1151
+ const cfgChecks = project.config.workflow?.gate?.verify?.checks ?? [];
1152
+ const all = cfgChecks.map((c2) => ({
1153
+ name: c2.name,
1154
+ command: c2.command,
1155
+ tier: c2.tier ?? "hard"
1156
+ }));
1157
+ let checks = all;
1158
+ if (opts.check && opts.check.length > 0) {
1159
+ const want = new Set(opts.check);
1160
+ checks = all.filter((c2) => want.has(c2.name));
1161
+ if (checks.length === 0) {
1162
+ fail(
1163
+ EXIT.USAGE,
1164
+ `task verify: no configured checks match --check ${[...want].join(",")}`,
1165
+ opts
1166
+ );
1167
+ }
1168
+ }
1169
+ if (checks.length === 0) {
1170
+ fail(
1171
+ EXIT.USAGE,
1172
+ "task verify: no verify checks configured (set workflow.gate.verify.checks in noir.config)",
1173
+ opts
1174
+ );
1175
+ }
1176
+ const ranAt = Date.now();
1177
+ const evidence = {
1178
+ ranAt,
1179
+ checks: checks.map((c2) => {
1180
+ const r = runCheck(c2.command);
1181
+ return {
1182
+ name: c2.name,
1183
+ exitCode: r.exitCode,
1184
+ outputDigest: r.digest,
1185
+ command: c2.command,
1186
+ tier: c2.tier
1187
+ };
1188
+ }),
1189
+ summary: ""
1190
+ };
1191
+ const passed = evidence.checks.filter((c2) => c2.exitCode === 0).length;
1192
+ const failed = evidence.checks.length - passed;
1193
+ evidence.summary = `${passed} passed, ${failed} failed`;
1194
+ const res = await callDaemonTool(opts, "workflow_advance", { evidence });
1195
+ if (res.ok === true) {
1196
+ if (opts.json === true) {
1197
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res, evidence })}
1198
+ `);
1199
+ return;
1200
+ }
1201
+ success(`verify \u2014 ${evidence.summary} \u2192 gate approved`, opts);
1202
+ renderStatusRow(res, opts);
1203
+ return;
1204
+ }
1205
+ if (opts.json === true) {
1206
+ process.stdout.write(`${JSON.stringify({ evidence, ...res })}
1207
+ `);
1208
+ return;
1209
+ }
1210
+ if (res.pendingGate) {
1211
+ warn(`verify gate: ${res.pendingGate.reason} (${evidence.summary})`, opts);
1212
+ const recovery = res.recovery ?? ["retry", "force", "skip", "block"];
1213
+ info(
1214
+ `recovery: ${recovery.map(
1215
+ (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}\``
1216
+ ).join(" | ")}`,
1217
+ opts
1218
+ );
1219
+ } else {
1220
+ fail(EXIT.ERROR, `task verify: ${res.error ?? "verify failed"}`, opts);
1221
+ }
1222
+ }
1223
+ async function taskResearch(opts) {
1224
+ const s = await fetchStatus(opts, opts.id);
1225
+ if (opts.json === true) {
1226
+ process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
1227
+ `);
1228
+ return;
1229
+ }
1230
+ renderStatusRow(s, opts);
1231
+ info("research findings are recorded via `noir task research record` (list + record)", opts);
1232
+ }
1233
+ async function taskResearchRecord(opts) {
1234
+ const TYPES = ["assumption", "discovery", "decision", "grounding-fact"];
1235
+ if (!TYPES.includes(opts.type)) {
1236
+ fail(
1237
+ EXIT.USAGE,
1238
+ `task research record: invalid type '${opts.type}' (expected one of: ${TYPES.join(", ")})`,
1239
+ opts
1240
+ );
1241
+ }
1242
+ const args = { type: opts.type, text: opts.text };
1243
+ if (typeof opts.source === "string" && opts.source.length > 0) args.source = opts.source;
1244
+ if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
1245
+ const res = await callDaemonTool(opts, "workflow_research_record", args);
1246
+ if (res.ok !== true) {
1247
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "record failed";
1248
+ fail(EXIT.ERROR, `task research record: ${detail}`, opts);
1249
+ }
1250
+ if (opts.json === true) {
1251
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
1252
+ `);
1253
+ return;
1254
+ }
1255
+ success(
1256
+ `research recorded \u2192 ${opts.type}: ${opts.text.slice(0, 80)}${opts.text.length > 80 ? "\u2026" : ""}`,
1257
+ opts
1258
+ );
1259
+ }
1260
+ async function taskDecompose(opts) {
1261
+ const cap = opts.capability.trim();
1262
+ if (cap.length === 0) fail(EXIT.USAGE, "task decompose: <capability-id> is required", opts);
1263
+ const plan = {
1264
+ capabilityId: cap,
1265
+ intent: `Capability ${cap} \u2014 details TBD (run with a provider for an LLM-drafted plan)`,
1266
+ slices: [
1267
+ {
1268
+ id: "s1-walking-skeleton",
1269
+ title: "Walking skeleton \u2014 thinnest end-to-end",
1270
+ type: "feature",
1271
+ rationale: `The first slice for capability ${cap}.`,
1272
+ scopeIn: "End-to-end path linking the main architecture.",
1273
+ scopeOut: "Polish, edge cases, full coverage.",
1274
+ dependsOn: [],
1275
+ files: { create: [], modify: [], preserve: [] },
1276
+ acceptance: [`verify acceptance criteria for capability ${cap}`],
1277
+ doD: ["pnpm test", "pnpm typecheck"],
1278
+ rollbackPlan: { procedure: "revert the slice commit", verifyCommand: "pnpm test" }
1279
+ }
1280
+ ],
1281
+ status: { "s1-walking-skeleton": "planned" }
1282
+ };
1283
+ if (opts.json === true) {
1284
+ process.stdout.write(`${JSON.stringify({ ok: true, data: plan })}
1285
+ `);
1286
+ return;
1287
+ }
1288
+ log(`decompose \u2014 ${cap}`, opts);
1289
+ for (const s of plan.slices) {
1290
+ info(`${s.id}: ${s.title} (${s.type}) \u2014 rationale: ${s.rationale}`, opts);
1291
+ }
1292
+ tip("each slice enters the existing clarify\u2192spec\u2192plan\u2192execute\u2192verify\u2192document FSM cycle", opts);
1293
+ }
965
1294
 
966
1295
  // src/commands/handoff.ts
967
1296
  var SEED_LIMIT = 5;
@@ -1069,7 +1398,7 @@ function resolveDirective(root, p) {
1069
1398
  }
1070
1399
  function handoffFilePath(root, taskId, projectId) {
1071
1400
  const id = typeof taskId === "string" && taskId.length > 0 ? taskId : projectId;
1072
- return join(root, NOIR_DIR, "handoff", `${id}.md`);
1401
+ return join2(root, NOIR_DIR, "handoff", `${id}.md`);
1073
1402
  }
1074
1403
  async function extractSeeds(opts, probe, query) {
1075
1404
  if (!probe.running) return { context: null, memory: null };
@@ -1101,7 +1430,7 @@ async function probeOnly(opts) {
1101
1430
  return probeDaemon(opts);
1102
1431
  }
1103
1432
  async function handoff(opts) {
1104
- const project = loadProjectInfo4(process.cwd());
1433
+ const project = loadProjectInfo5(process.cwd());
1105
1434
  const host = project.config.host;
1106
1435
  const snapshot = await gatherStatusPayload(opts);
1107
1436
  const probe = await probeOnly(opts);
@@ -1135,7 +1464,7 @@ async function handoff(opts) {
1135
1464
  const md = renderMarkdown(full);
1136
1465
  if (opts.write === true) {
1137
1466
  const path = handoffFilePath(project.root, task?.taskId, project.id);
1138
- mkdirSync(join(project.root, NOIR_DIR, "handoff"), { recursive: true });
1467
+ mkdirSync(join2(project.root, NOIR_DIR, "handoff"), { recursive: true });
1139
1468
  writeFileSync(path, md, "utf8");
1140
1469
  info(`handoff written to ${path}`, opts);
1141
1470
  return;
@@ -1146,7 +1475,7 @@ async function handoff(opts) {
1146
1475
  // src/commands/home.ts
1147
1476
  import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
1148
1477
  import {
1149
- loadProjectInfo as loadProjectInfo5,
1478
+ loadProjectInfo as loadProjectInfo6,
1150
1479
  NOIR_VERSION as NOIR_VERSION3,
1151
1480
  readInstallRecord
1152
1481
  } from "@noir-ai/core";
@@ -1444,7 +1773,7 @@ async function resolveSections(commands) {
1444
1773
  // src/commands/home.ts
1445
1774
  function tryProject() {
1446
1775
  try {
1447
- const info2 = loadProjectInfo5(process.cwd());
1776
+ const info2 = loadProjectInfo6(process.cwd());
1448
1777
  return { id: info2.id, name: info2.name, host: info2.config.host };
1449
1778
  } catch {
1450
1779
  return null;
@@ -1608,7 +1937,7 @@ async function runMenu(opts, deps) {
1608
1937
  env: process.env,
1609
1938
  configUpdate: (() => {
1610
1939
  try {
1611
- const info2 = loadProjectInfo5(process.cwd());
1940
+ const info2 = loadProjectInfo6(process.cwd());
1612
1941
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
1613
1942
  } catch {
1614
1943
  return DEFAULT_UPDATE_CONFIG;
@@ -1904,9 +2233,105 @@ async function memoryConsolidate(opts) {
1904
2233
  fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
1905
2234
  }
1906
2235
 
2236
+ // src/commands/release.ts
2237
+ import { execSync, spawnSync as spawnSync2 } from "child_process";
2238
+ function describe(step, who) {
2239
+ const m = {
2240
+ preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
2241
+ bump: "run bump-version.mjs + update CHANGELOG + roadmap docs",
2242
+ gate: "re-run the full gate (lint/build/typecheck/test/docs:validate)",
2243
+ commit: "commit chore(release) + push develop",
2244
+ "ci-develop": "wait for CI on develop \u2192 green",
2245
+ "beta-tag": "pnpm release:tag \u2192 beta tag + push",
2246
+ "human-approve-beta": "approve the release workflow publish job in GitHub Actions",
2247
+ "merge-main": "merge develop \u2192 main (fast-forward) + push",
2248
+ "ci-main": "wait for CI on main \u2192 green",
2249
+ "stable-tag": "pnpm release:tag \u2192 stable tag + push",
2250
+ "human-approve-stable": "approve the release workflow publish job in GitHub Actions (again)",
2251
+ "dist-bump": "update Homebrew (packaging/homebrew/noir.rb) + Scoop (packaging/scoop/noir.json) from npm",
2252
+ sync: "merge main \u2192 develop + verify both branches at same SHA"
2253
+ };
2254
+ return `${m[step]} [${who}]`;
2255
+ }
2256
+ var ALL = [
2257
+ "preflight",
2258
+ "bump",
2259
+ "gate",
2260
+ "commit",
2261
+ "ci-develop",
2262
+ "beta-tag",
2263
+ "human-approve-beta",
2264
+ "merge-main",
2265
+ "ci-main",
2266
+ "stable-tag",
2267
+ "human-approve-stable",
2268
+ "dist-bump",
2269
+ "sync"
2270
+ ];
2271
+ async function release(opts) {
2272
+ if (opts.dryRun) {
2273
+ log("noir release --dry-run (no changes will be made)", opts);
2274
+ for (const s of ALL) {
2275
+ const who = s.startsWith("human-approve") ? "human" : "noir";
2276
+ info(`${s}: ${describe(s, who)}`, opts);
2277
+ }
2278
+ tip('full release flow documented in CLAUDE.md "Patch release flow (beta \u2192 stable)"', opts);
2279
+ return;
2280
+ }
2281
+ const version = opts.version?.trim();
2282
+ if (!version) {
2283
+ fail(EXIT.USAGE, "release: <version> is required (e.g. noir release 1.10.0)", opts);
2284
+ }
2285
+ const channel = opts.channel === "stable" ? "stable" : "beta";
2286
+ log(`noir release ${version} (${channel} channel)`, opts);
2287
+ try {
2288
+ info(`[1/13] preflight: ${describe("preflight", "noir")}`, opts);
2289
+ execSync("git diff --quiet", { encoding: "utf8" });
2290
+ const branch = execSync("git branch --show-current", { encoding: "utf8" }).trim();
2291
+ if (branch !== "develop") fail(EXIT.ERROR, "release: must be on develop", opts);
2292
+ const status2 = execSync("git status --porcelain", { encoding: "utf8" }).trim();
2293
+ if (status2) fail(EXIT.ERROR, "release: tree must be clean", opts);
2294
+ success("preflight: \u2713 clean, on develop", opts);
2295
+ info(`[2/13] bump: ${describe("bump", "noir")}`, opts);
2296
+ spawnSync2("node", ["scripts/bump-version.mjs", version], {
2297
+ encoding: "utf8",
2298
+ stdio: "inherit"
2299
+ });
2300
+ success(`bump: \u2713 version ${version}`, opts);
2301
+ info(`[3/13] gate: ${describe("gate", "noir")}`, opts);
2302
+ execSync("pnpm lint && pnpm build && pnpm typecheck && pnpm test && pnpm docs:validate", {
2303
+ encoding: "utf8",
2304
+ stdio: "inherit"
2305
+ });
2306
+ success("gate: \u2713 full gate green", opts);
2307
+ info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
2308
+ spawnSync2("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
2309
+ spawnSync2("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
2310
+ encoding: "utf8",
2311
+ stdio: "inherit"
2312
+ });
2313
+ execSync("git push origin develop", { encoding: "utf8" });
2314
+ success("commit: \u2713 pushed to develop", opts);
2315
+ info(`[5/13] ci-develop: waiting for CI on develop\u2026`, opts);
2316
+ info(`[6/13] beta-tag: ${describe("beta-tag", "noir")}`, opts);
2317
+ tip("after CI green: run `pnpm release:tag` and push the beta tag", opts);
2318
+ warn(`[7/13] ${describe("human-approve-beta", "human")}`, opts);
2319
+ tip("open GitHub Actions \u2192 approve the publish job, then continue", opts);
2320
+ tip("after approval: verify `npm view @noir-ai/cli dist-tags beta` = beta version", opts);
2321
+ info("remaining steps (merge\u2192stable\u2192dist\u2192sync): see CLAUDE.md", opts);
2322
+ for (const s of ALL.slice(7)) {
2323
+ const who = s.startsWith("human-approve") ? "human" : "noir";
2324
+ info(` ${s}: ${describe(s, who)}`, opts);
2325
+ }
2326
+ success(`release ${version}: preflight\u2192bump\u2192gate\u2192commit done. Resume after approval.`, opts);
2327
+ } catch (err) {
2328
+ fail(EXIT.ERROR, `release failed: ${err instanceof Error ? err.message : String(err)}`, opts);
2329
+ }
2330
+ }
2331
+
1907
2332
  // src/commands/skills.ts
1908
2333
  import { claudeAdapter } from "@noir-ai/adapters";
1909
- import { loadProjectInfo as loadProjectInfo6 } from "@noir-ai/core";
2334
+ import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
1910
2335
  import {
1911
2336
  buildRegistry,
1912
2337
  discoverAll,
@@ -2000,7 +2425,7 @@ async function skillsList(opts) {
2000
2425
  }
2001
2426
  async function skillsSync(opts) {
2002
2427
  const root = process.cwd();
2003
- const project = loadProjectInfo6(root);
2428
+ const project = loadProjectInfo7(root);
2004
2429
  if (!claudeAdapter.skillsDir) {
2005
2430
  const data2 = {
2006
2431
  emitted: [],
@@ -2467,15 +2892,20 @@ function createProgram() {
2467
2892
  throw new NoirCliError(EXIT.USAGE, "Usage: noir skills list|sync|lint|registry");
2468
2893
  });
2469
2894
  const taskGrp = program2.command("task").description("workflow task control");
2470
- taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").action(async (...args) => {
2895
+ taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").option(
2896
+ "--class <taskClass>",
2897
+ "task class (feature/epic/enhancement/bugfix/spike/quick-task/refactor) \u2014 drives the PRD gate"
2898
+ ).action(async (...args) => {
2471
2899
  const cmd = trailingCmd(args);
2472
2900
  const g = cmd.optsWithGlobals();
2473
2901
  const slug = typeof g.slug === "string" ? g.slug : "";
2474
2902
  const mode = typeof g.mode === "string" ? g.mode : void 0;
2903
+ const taskClass = typeof g.class === "string" ? g.class : void 0;
2475
2904
  await taskNew({
2476
2905
  ...toCliOptions(g),
2477
2906
  slug,
2478
- ...mode === void 0 ? {} : { mode }
2907
+ ...mode === void 0 ? {} : { mode },
2908
+ ...taskClass === void 0 ? {} : { taskClass }
2479
2909
  });
2480
2910
  });
2481
2911
  taskGrp.command("status").description("active (or named) task status").argument("[id]", "task id (defaults to active)").action(async (...args) => {
@@ -2488,7 +2918,7 @@ function createProgram() {
2488
2918
  ...id === void 0 ? {} : { id }
2489
2919
  });
2490
2920
  });
2491
- 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) => {
2921
+ 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) => {
2492
2922
  const cmd = trailingCmd(args);
2493
2923
  const g = cmd.optsWithGlobals();
2494
2924
  const to = typeof g.to === "string" ? g.to : void 0;
@@ -2496,14 +2926,99 @@ function createProgram() {
2496
2926
  await taskAdvance({
2497
2927
  ...toCliOptions(g),
2498
2928
  ...to === void 0 ? {} : { to },
2499
- ...force === void 0 ? {} : { force }
2929
+ ...force === void 0 ? {} : { force },
2930
+ ...g.artifacts === false ? { noArtifacts: true } : {}
2500
2931
  });
2501
2932
  });
2502
2933
  taskGrp.command("next").description("suggest the next phase + applicable skill").action(async (...args) => {
2503
2934
  await taskNext(toCliOptions(trailingCmd(args).optsWithGlobals()));
2504
2935
  });
2936
+ taskGrp.command("decompose").description(
2937
+ "decompose a capability into buildable slices (template-only offline; see the spec)"
2938
+ ).argument("<capability>", "capability id, e.g. C4").option("--out <path>", "output path for the SlicePlan JSON").action(async (...args) => {
2939
+ const cmd = trailingCmd(args);
2940
+ const g = cmd.optsWithGlobals();
2941
+ const capability = typeof args[0] === "string" ? args[0] : "";
2942
+ await taskDecompose({
2943
+ ...toCliOptions(g),
2944
+ capability,
2945
+ ...typeof g.out === "string" && g.out.length > 0 ? { out: g.out } : {}
2946
+ });
2947
+ });
2948
+ taskGrp.command("verify").description("run configured verify checks and submit evidence to the verify gate").option(
2949
+ "--check <name>",
2950
+ "restrict to a named check (repeatable)",
2951
+ (val, acc) => [...acc, val],
2952
+ []
2953
+ ).action(async (...args) => {
2954
+ const cmd = trailingCmd(args);
2955
+ const g = cmd.optsWithGlobals();
2956
+ const check = Array.isArray(g.check) ? g.check : void 0;
2957
+ await taskVerify({
2958
+ ...toCliOptions(g),
2959
+ ...check === void 0 || check.length === 0 ? {} : { check }
2960
+ });
2961
+ });
2962
+ taskGrp.command("research").description("list research findings for the active (or named) task").argument("[id]", "task id (defaults to active)").action(async (...args) => {
2963
+ const cmd = trailingCmd(args);
2964
+ const g = cmd.optsWithGlobals();
2965
+ const first = args[0];
2966
+ const id = typeof first === "string" ? first : void 0;
2967
+ await taskResearch({
2968
+ ...toCliOptions(g),
2969
+ ...id === void 0 ? {} : { id }
2970
+ });
2971
+ });
2972
+ 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) => {
2973
+ const cmd = trailingCmd(args);
2974
+ const g = cmd.optsWithGlobals();
2975
+ await taskResearchRecord({
2976
+ ...toCliOptions(g),
2977
+ type: String(g.type ?? ""),
2978
+ text: String(g.text ?? ""),
2979
+ ...typeof g.source === "string" && g.source.length > 0 ? { source: g.source } : {},
2980
+ ...typeof g.task === "string" && g.task.length > 0 ? { task: g.task } : {}
2981
+ });
2982
+ });
2983
+ 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) => {
2984
+ const cmd = trailingCmd(args);
2985
+ const g = cmd.optsWithGlobals();
2986
+ const first = args[0];
2987
+ const id = typeof first === "string" ? first : void 0;
2988
+ const last = g.last === true;
2989
+ const prompt = typeof g.prompt === "string" ? g.prompt : void 0;
2990
+ await taskResume({
2991
+ ...toCliOptions(g),
2992
+ ...id === void 0 ? {} : { id },
2993
+ ...last ? { last } : {},
2994
+ ...prompt === void 0 ? {} : { prompt }
2995
+ });
2996
+ });
2997
+ 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) => {
2998
+ const cmd = trailingCmd(args);
2999
+ const g = cmd.optsWithGlobals();
3000
+ const reason = typeof args[0] === "string" ? args[0] : "";
3001
+ const task = typeof g.task === "string" ? g.task : void 0;
3002
+ await taskBlock({
3003
+ ...toCliOptions(g),
3004
+ reason,
3005
+ ...task === void 0 ? {} : { task }
3006
+ });
3007
+ });
3008
+ taskGrp.command("abandon").description("abandon the active (or named) task (terminal, confirmed)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
3009
+ const cmd = trailingCmd(args);
3010
+ const g = cmd.optsWithGlobals();
3011
+ const task = typeof g.task === "string" ? g.task : void 0;
3012
+ await taskAbandon({
3013
+ ...toCliOptions(g),
3014
+ ...task === void 0 ? {} : { task }
3015
+ });
3016
+ });
2505
3017
  taskGrp.action(() => {
2506
- throw new NoirCliError(EXIT.USAGE, "Usage: noir task new|status|advance|next");
3018
+ throw new NoirCliError(
3019
+ EXIT.USAGE,
3020
+ "Usage: noir task new|status|advance|next|verify|research|research-record|resume|block|abandon"
3021
+ );
2507
3022
  });
2508
3023
  const installCmd = program2.command("install").description(
2509
3024
  "install Noir via the native managed-Node path (or migrate from another install method)"
@@ -2538,6 +3053,16 @@ function createProgram() {
2538
3053
  program2.command("wrap").description("session-end alias for `noir handoff`").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
2539
3054
  await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
2540
3055
  });
3056
+ 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) => {
3057
+ const cmd = trailingCmd(args);
3058
+ const g = cmd.optsWithGlobals();
3059
+ await release({
3060
+ ...toCliOptions(g),
3061
+ ...typeof args[0] === "string" && args[0].length > 0 ? { version: args[0] } : {},
3062
+ ...typeof g.channel === "string" && g.channel.length > 0 ? { channel: g.channel } : {},
3063
+ ...g.dryRun === true ? { dryRun: true } : {}
3064
+ });
3065
+ });
2541
3066
  program2.command("tui").description("interactive Ink dashboard (host \xB7 phase \xB7 daemon + /command dispatch)").action(async (...args) => {
2542
3067
  const g = actionGlobals(args);
2543
3068
  const opts = toCliOptions(g);