@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 +554 -29
- package/dist/bin.js.map +1 -1
- package/dist/tui/index.js +562 -37
- package/dist/tui/index.js.map +1 -1
- package/package.json +11 -11
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
|
|
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 {
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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").
|
|
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(
|
|
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
|
|
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 ??
|
|
4151
|
+
return testHome ?? join3(homedir(), ".noir");
|
|
3627
4152
|
}
|
|
3628
4153
|
function historyFile(projectId) {
|
|
3629
|
-
return
|
|
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 =
|
|
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 =
|
|
4223
|
+
projectId = loadProjectInfo9(process.cwd()).id;
|
|
3699
4224
|
} catch {
|
|
3700
4225
|
projectId = null;
|
|
3701
4226
|
}
|