@next-generation-collective/factory 0.3.2 → 0.4.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/lib/agent.js +5 -2
- package/lib/banner.js +6 -3
- package/lib/cli.js +5 -1
- package/lib/commands.js +1 -1
- package/lib/mcp.js +316 -0
- package/lib/spec.js +2 -1
- package/package.json +1 -1
package/lib/agent.js
CHANGED
|
@@ -102,11 +102,14 @@ export function runAgent(root, prompt, config, promptFile) {
|
|
|
102
102
|
|
|
103
103
|
const { file, shell } = launch(command);
|
|
104
104
|
const options = { cwd: root, shell, maxBuffer: 64 * 1024 * 1024 };
|
|
105
|
+
// Inside an AI tool, our stdout is the conversation with that tool. The agent's own
|
|
106
|
+
// output goes to stderr there, so it can never corrupt it.
|
|
107
|
+
const out = process.env.FACTORY_MCP ? 2 : "inherit";
|
|
105
108
|
if (call.stdin != null) {
|
|
106
109
|
options.input = call.stdin;
|
|
107
|
-
options.stdio = ["pipe",
|
|
110
|
+
options.stdio = ["pipe", out, "inherit"];
|
|
108
111
|
} else {
|
|
109
|
-
options.stdio = "inherit";
|
|
112
|
+
options.stdio = ["inherit", out, "inherit"];
|
|
110
113
|
}
|
|
111
114
|
const args = shell ? call.args.map(quoteForWindows) : call.args;
|
|
112
115
|
const run = spawnSync(file, args, options);
|
package/lib/banner.js
CHANGED
|
@@ -19,7 +19,7 @@ const width = (text) => [...text.replace(/\u001b\[[0-9;]*m/g, "")].length;
|
|
|
19
19
|
|
|
20
20
|
// The builder's first name, from their git identity or, on a Mac, their account's full
|
|
21
21
|
// name. Nothing is sent anywhere.
|
|
22
|
-
export function
|
|
22
|
+
export function fullName() {
|
|
23
23
|
const ask = (cmd, args) => {
|
|
24
24
|
try {
|
|
25
25
|
return execFileSync(cmd, args, { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -27,8 +27,11 @@ export function firstName() {
|
|
|
27
27
|
return "";
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
return ask("git", ["config", "user.name"]) || (process.platform === "darwin" ? ask("id", ["-F"]) : "") || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function firstName() {
|
|
34
|
+
return fullName()?.split(/\s+/)[0] || null;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
// One small file in the builder's own settings folder, so the first welcome happens once.
|
package/lib/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { banner } from "./banner.js";
|
|
3
|
+
import { serve } from "./mcp.js";
|
|
3
4
|
import { parse as parse_, resolve } from "node:path";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import {
|
|
@@ -22,7 +23,7 @@ const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
|
22
23
|
|
|
23
24
|
const COMMANDS = new Set([
|
|
24
25
|
"init", "spec", "approve", "plan", "tasks", "verify", "build", "review", "report",
|
|
25
|
-
"lesson", "status", "doctor", "help", "version",
|
|
26
|
+
"lesson", "status", "doctor", "help", "version", "mcp",
|
|
26
27
|
]);
|
|
27
28
|
|
|
28
29
|
const VALUE_FLAGS = new Set([
|
|
@@ -82,6 +83,7 @@ Steps, if you want them one at a time
|
|
|
82
83
|
factory doctor what was detected, which agent, which checks apply
|
|
83
84
|
factory lesson new turn a real failure into a dated lesson
|
|
84
85
|
factory init answer five questions to fill factory.config.json
|
|
86
|
+
factory mcp run inside Claude Code, Cursor, Codex, Gemini or VS Code
|
|
85
87
|
|
|
86
88
|
Options
|
|
87
89
|
--ask ask all seven questions before writing the spec
|
|
@@ -147,6 +149,8 @@ export async function main(argv) {
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
switch (command) {
|
|
152
|
+
case "mcp":
|
|
153
|
+
return serve(VERSION, root);
|
|
150
154
|
case "init":
|
|
151
155
|
return init(root, flags);
|
|
152
156
|
case "spec":
|
package/lib/commands.js
CHANGED
|
@@ -703,7 +703,7 @@ function reviewerConfig(config) {
|
|
|
703
703
|
return config.agent.review ? { ...config, agent: normaliseAgent(config.agent.review) } : config;
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
-
function runReview(root, config, { artifacts, base, run }) {
|
|
706
|
+
export function runReview(root, config, { artifacts, base, run }) {
|
|
707
707
|
const reviewer = reviewerConfig(config);
|
|
708
708
|
const stop = (problem) => {
|
|
709
709
|
writeState(root, { lastReview: { at: new Date().toISOString(), passed: false, summary: problem, file: null, unproven: [] } });
|
package/lib/mcp.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// The factory inside an AI tool. Claude Code, Cursor, Codex, Gemini CLI and VS Code all
|
|
2
|
+
// speak the Model Context Protocol: JSON-RPC messages, one per line, over stdin and stdout.
|
|
3
|
+
// The person talks to their AI as usual; the AI uses these tools, and the factory keeps the
|
|
4
|
+
// same rules as on the command line: a person approves, the checks decide, proof is kept.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { analyseProject } from "./analyse.js";
|
|
10
|
+
import { approvalView } from "./approval.js";
|
|
11
|
+
import { firstName, fullName, isFirstVisit, rememberVisit } from "./banner.js";
|
|
12
|
+
import { ensureConfig, report, runReview } from "./commands.js";
|
|
13
|
+
import { paths, readConfig, readState, writeState } from "./config.js";
|
|
14
|
+
import { composeBuildPrompt, composeSpecPrompt } from "./prompts.js";
|
|
15
|
+
import { isGitRepo, reviewBase } from "./review.js";
|
|
16
|
+
import { activeSpec, approve, findSpec, nextSpecName, setActive } from "./spec.js";
|
|
17
|
+
import { formatResults, verify } from "./verify.js";
|
|
18
|
+
|
|
19
|
+
const PROTOCOLS = ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"];
|
|
20
|
+
|
|
21
|
+
const INSTRUCTIONS = `Next-Gen Factory checks the work you build with AI and proves it works.
|
|
22
|
+
|
|
23
|
+
When the person asks you to build, add, fix or change something in this project:
|
|
24
|
+
1. Call factory_start with their request, then write the spec exactly where it says.
|
|
25
|
+
2. Call factory_approve. The person approves the spec, not you. If the tool asks you to,
|
|
26
|
+
show the person the summary and wait for their own words before calling it again.
|
|
27
|
+
3. Build what the approved spec says.
|
|
28
|
+
4. Call factory_prove. If it says NOT DONE, fix what failed and call it again.
|
|
29
|
+
5. Tell the person the result in plain words, with the proof file.
|
|
30
|
+
|
|
31
|
+
Never say the work is done, bug-free, secure or compliant unless factory_prove says DONE,
|
|
32
|
+
and even then only report what was proven. Call factory_check when the person asks what
|
|
33
|
+
the project can prove, or before you start on a project for the first time.`;
|
|
34
|
+
|
|
35
|
+
const TOOLS = [
|
|
36
|
+
{
|
|
37
|
+
name: "factory_check",
|
|
38
|
+
description:
|
|
39
|
+
"What this project can prove right now: credentials in code, tests that check nothing, missing tests, personal data in logs. Fast, and it changes nothing. Use it first on a new project, or when the person asks how healthy the project is.",
|
|
40
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "factory_start",
|
|
44
|
+
description:
|
|
45
|
+
"Start any build, feature, fix or change the person asks for. Returns the instructions for writing the spec and where to save it. Always call this before changing code.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: { request: { type: "string", description: "What the person asked for, in their words." } },
|
|
49
|
+
required: ["request"],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "factory_approve",
|
|
55
|
+
description:
|
|
56
|
+
"Get the person's approval of the spec before building. The person approves, never the AI. When the AI tool cannot show the person an approval box, this returns the summary to show them; call it again with person_said set to their exact words only after they approve.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
spec: { type: "string", description: "Spec name. Leave out for the active spec." },
|
|
61
|
+
person_said: { type: "string", description: "The person's own words approving the spec, quoted exactly." },
|
|
62
|
+
},
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "factory_prove",
|
|
68
|
+
description:
|
|
69
|
+
"After building, prove the work: runs the project's tests and build and the factory's checks. Says DONE or NOT DONE with what failed. Fix and call again until DONE, then call factory_review.",
|
|
70
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "factory_review",
|
|
74
|
+
description:
|
|
75
|
+
"The independent review: a separate AI session that never saw how you worked checks the change against the approved spec. Run it after factory_prove says DONE. It can take a few minutes.",
|
|
76
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Every console line from the commands is collected for the reply, never written to stdout.
|
|
81
|
+
function captured(fn) {
|
|
82
|
+
const lines = [];
|
|
83
|
+
const keep = (...args) => lines.push(args.map(String).join(" "));
|
|
84
|
+
const saved = { log: console.log, error: console.error, warn: console.warn };
|
|
85
|
+
console.log = keep;
|
|
86
|
+
console.error = keep;
|
|
87
|
+
console.warn = keep;
|
|
88
|
+
try {
|
|
89
|
+
const value = fn();
|
|
90
|
+
return { value, text: lines.join("\n") };
|
|
91
|
+
} finally {
|
|
92
|
+
Object.assign(console, saved);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function welcome() {
|
|
97
|
+
const name = firstName();
|
|
98
|
+
const you = name ? `, ${name}` : "";
|
|
99
|
+
if (!isFirstVisit()) return null;
|
|
100
|
+
rememberVisit();
|
|
101
|
+
return `Welcome to Next-Gen Factory${you}. You are one of the builders of the AI economy. Africa builds.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const text = (body, isError = false) => ({ content: [{ type: "text", text: body }], ...(isError ? { isError: true } : {}) });
|
|
105
|
+
|
|
106
|
+
export function createServer({ root = process.cwd(), send }) {
|
|
107
|
+
let clientCanAsk = false;
|
|
108
|
+
let nextId = 1;
|
|
109
|
+
const waiting = new Map();
|
|
110
|
+
|
|
111
|
+
// A question to the person through the AI tool's own approval box, which the AI cannot answer.
|
|
112
|
+
const askPerson = (message, requestedSchema) =>
|
|
113
|
+
new Promise((resolveAnswer) => {
|
|
114
|
+
const id = `factory-${nextId++}`;
|
|
115
|
+
waiting.set(id, resolveAnswer);
|
|
116
|
+
send({ jsonrpc: "2.0", id, method: "elicitation/create", params: { message, requestedSchema } });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const tools = {
|
|
120
|
+
factory_check() {
|
|
121
|
+
const analysis = analyseProject(root);
|
|
122
|
+
if (analysis.empty) return text("No project found here. Open a project folder first.", true);
|
|
123
|
+
const config = readConfig(root);
|
|
124
|
+
const { value: run } = captured(() => verify(root, { deep: false, config, save: false }));
|
|
125
|
+
const greeting = welcome();
|
|
126
|
+
return text(
|
|
127
|
+
[
|
|
128
|
+
greeting ? `${greeting}\n(Show this welcome to the person.)\n` : "",
|
|
129
|
+
`${config.name} (${analysis.label})`,
|
|
130
|
+
...analysis.summary.slice(1),
|
|
131
|
+
"",
|
|
132
|
+
"What this project can prove right now",
|
|
133
|
+
formatResults(root, run, { header: false }),
|
|
134
|
+
"",
|
|
135
|
+
"Checked by Next-Gen Factory.",
|
|
136
|
+
].join("\n"),
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
factory_start({ request }) {
|
|
141
|
+
const job = String(request ?? "").trim();
|
|
142
|
+
if (!job) return text("Say what the person asked for in request.", true);
|
|
143
|
+
const config = captured(() => ensureConfig(root)).value;
|
|
144
|
+
const name = nextSpecName(root, job);
|
|
145
|
+
const target = `specs/${name}/spec.md`;
|
|
146
|
+
mkdirSync(join(root, "specs", name), { recursive: true });
|
|
147
|
+
setActive(root, name);
|
|
148
|
+
const prompt = composeSpecPrompt(root, config, job, target, { analysis: analyseProject(root), answers: {} });
|
|
149
|
+
const greeting = welcome();
|
|
150
|
+
return text(
|
|
151
|
+
[
|
|
152
|
+
greeting ? `${greeting}\n(Show this welcome to the person.)\n` : "",
|
|
153
|
+
`Next-Gen Factory job: ${name}`,
|
|
154
|
+
`Write the spec to ${target}, following the instructions below. Then call factory_approve.`,
|
|
155
|
+
"",
|
|
156
|
+
prompt,
|
|
157
|
+
].join("\n"),
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async factory_approve({ spec, person_said: personSaid }) {
|
|
162
|
+
const config = readConfig(root);
|
|
163
|
+
const name = findSpec(root, String(spec ?? config.spec?.active ?? "").trim());
|
|
164
|
+
if (!name) return text("No spec yet. Call factory_start first.", true);
|
|
165
|
+
const current = activeSpec(root);
|
|
166
|
+
const specText = current?.name === name ? current.artifacts?.spec : null;
|
|
167
|
+
const view = approvalView(specText ?? "");
|
|
168
|
+
if (!String(specText ?? "").trim()) return text(`The spec for ${name} is empty. Write it first.`, true);
|
|
169
|
+
|
|
170
|
+
let how = null;
|
|
171
|
+
const said = String(personSaid ?? "").trim();
|
|
172
|
+
const answer = clientCanAsk && !said
|
|
173
|
+
? await askPerson(
|
|
174
|
+
`Next-Gen Factory: approve this plan before anything is built?\n\n${view}`,
|
|
175
|
+
{ type: "object", properties: { approve: { type: "boolean", title: "Approve and build" } }, required: ["approve"] },
|
|
176
|
+
)
|
|
177
|
+
: null;
|
|
178
|
+
if (answer?.action === "accept" && answer.content?.approve === true) {
|
|
179
|
+
how = { via: "approval box in the AI tool" };
|
|
180
|
+
} else if (answer?.action === "decline" || (answer?.action === "accept" && answer.content?.approve === false)) {
|
|
181
|
+
return text("The person did not approve. Ask what to change, update the spec, then call factory_approve again.");
|
|
182
|
+
} else if (said) {
|
|
183
|
+
how = { via: "chat", personSaid: String(personSaid).trim().slice(0, 500) };
|
|
184
|
+
} else {
|
|
185
|
+
return text(
|
|
186
|
+
[
|
|
187
|
+
"Show the person this summary of the spec and ask them to approve it.",
|
|
188
|
+
"Only after they say yes, call factory_approve again with person_said set to their exact words.",
|
|
189
|
+
"",
|
|
190
|
+
view,
|
|
191
|
+
].join("\n"),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const approval = approve(root, name, fullName(), how);
|
|
196
|
+
setActive(root, name);
|
|
197
|
+
const head = isGitRepo(root) ? spawnSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).stdout?.trim() : null;
|
|
198
|
+
writeState(root, { buildBase: head || null, buildStartedAt: new Date().toISOString() });
|
|
199
|
+
const artifacts = activeSpec(root)?.artifacts;
|
|
200
|
+
return text(
|
|
201
|
+
[
|
|
202
|
+
`Approved by ${approval.approvedBy} (${how.via}). Editing the spec now cancels the approval.`,
|
|
203
|
+
"Build it now, following these rules. Then call factory_prove.",
|
|
204
|
+
"",
|
|
205
|
+
composeBuildPrompt(root, config, "", [], artifacts),
|
|
206
|
+
].join("\n"),
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
factory_prove() {
|
|
211
|
+
const current = activeSpec(root);
|
|
212
|
+
if (!current) return text("No job yet. Call factory_start first.", true);
|
|
213
|
+
if (current.state !== "approved") return text(`The spec ${current.name} is ${current.state}. Get the person's approval with factory_approve.`, true);
|
|
214
|
+
const config = readConfig(root);
|
|
215
|
+
const { value: run } = captured(() => verify(root, { deep: true, config, save: true }));
|
|
216
|
+
const results = formatResults(root, run, { header: false });
|
|
217
|
+
if (!run.ok) {
|
|
218
|
+
return text(["NOT DONE. Fix what failed, then call factory_prove again.", "", results].join("\n"));
|
|
219
|
+
}
|
|
220
|
+
return text(["DONE: the checks passed.", "", results, "", "Now call factory_review for the independent review."].join("\n"));
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
factory_review() {
|
|
224
|
+
if (!isGitRepo(root)) return text("The independent review needs git, to know what changed.", true);
|
|
225
|
+
const current = activeSpec(root);
|
|
226
|
+
const config = readConfig(root);
|
|
227
|
+
const base = reviewBase(root, { state: readState(root) });
|
|
228
|
+
const p = paths(root);
|
|
229
|
+
const lastRun = existsSync(p.lastVerify) ? JSON.parse(readFileSync(p.lastVerify, "utf8")) : null;
|
|
230
|
+
const { value: result } = captured(() =>
|
|
231
|
+
runReview(root, config, { artifacts: current?.state === "approved" ? current.artifacts : null, base, run: lastRun }),
|
|
232
|
+
);
|
|
233
|
+
const { text: reportText } = captured(() => report(root, {}));
|
|
234
|
+
if (result.problem?.startsWith("No reviewer agent")) {
|
|
235
|
+
return text(
|
|
236
|
+
`The independent review did not run: no second AI is installed on this computer (it uses ${config.agent?.command ?? "claude"}). Tell the person the change passed the checks but has no independent review. ${reportText}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const verdict = result.passed ? "Proven by Next-Gen: the checks passed and a separate session approved the change." : `NOT DONE: ${result.summary}`;
|
|
240
|
+
return text([verdict, result.file ? `Review: ${result.file}` : "", reportText].filter(Boolean).join("\n"));
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
async function handle(message) {
|
|
245
|
+
const { id, method, params } = message;
|
|
246
|
+
|
|
247
|
+
if (method === undefined && id !== undefined && waiting.has(id)) {
|
|
248
|
+
const resolveAnswer = waiting.get(id);
|
|
249
|
+
waiting.delete(id);
|
|
250
|
+
resolveAnswer(message.result ?? { action: "unavailable" });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const reply = (result) => id !== undefined && send({ jsonrpc: "2.0", id, result });
|
|
255
|
+
const fail = (code, msg) => id !== undefined && send({ jsonrpc: "2.0", id, error: { code, message: msg } });
|
|
256
|
+
|
|
257
|
+
switch (method) {
|
|
258
|
+
case "initialize": {
|
|
259
|
+
clientCanAsk = Boolean(params?.capabilities?.elicitation);
|
|
260
|
+
const asked = params?.protocolVersion;
|
|
261
|
+
reply({
|
|
262
|
+
protocolVersion: PROTOCOLS.includes(asked) ? asked : "2025-06-18",
|
|
263
|
+
capabilities: { tools: {} },
|
|
264
|
+
serverInfo: { name: "next-gen-factory", title: "Next-Gen Factory", version: VERSION },
|
|
265
|
+
instructions: INSTRUCTIONS,
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
case "notifications/initialized":
|
|
270
|
+
case "notifications/cancelled":
|
|
271
|
+
return;
|
|
272
|
+
case "ping":
|
|
273
|
+
reply({});
|
|
274
|
+
return;
|
|
275
|
+
case "tools/list":
|
|
276
|
+
reply({ tools: TOOLS });
|
|
277
|
+
return;
|
|
278
|
+
case "tools/call": {
|
|
279
|
+
const tool = tools[params?.name];
|
|
280
|
+
if (!tool) return fail(-32602, `Unknown tool: ${params?.name}`);
|
|
281
|
+
try {
|
|
282
|
+
reply(await tool(params?.arguments ?? {}));
|
|
283
|
+
} catch (error) {
|
|
284
|
+
reply(text(`The factory hit an error: ${error?.message ?? error}`, true));
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
default:
|
|
289
|
+
if (id !== undefined) fail(-32601, `Method not found: ${method}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { handle };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let VERSION = "0.0.0";
|
|
297
|
+
|
|
298
|
+
export async function serve(version, root = process.cwd()) {
|
|
299
|
+
VERSION = version;
|
|
300
|
+
process.env.FACTORY_MCP = "1";
|
|
301
|
+
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
302
|
+
const server = createServer({ root: resolve(root), send });
|
|
303
|
+
const lines = createInterface({ input: process.stdin });
|
|
304
|
+
for await (const line of lines) {
|
|
305
|
+
if (!line.trim()) continue;
|
|
306
|
+
let message;
|
|
307
|
+
try {
|
|
308
|
+
message = JSON.parse(line);
|
|
309
|
+
} catch {
|
|
310
|
+
send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
server.handle(message);
|
|
314
|
+
}
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
package/lib/spec.js
CHANGED
|
@@ -113,7 +113,7 @@ export function setActive(root, name) {
|
|
|
113
113
|
return config;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export function approve(root, name, by) {
|
|
116
|
+
export function approve(root, name, by, how = null) {
|
|
117
117
|
const artifacts = readArtifacts(root, name);
|
|
118
118
|
if (!artifacts.spec.trim()) return null;
|
|
119
119
|
const approval = {
|
|
@@ -121,6 +121,7 @@ export function approve(root, name, by) {
|
|
|
121
121
|
hash: hashText(artifacts.spec),
|
|
122
122
|
approvedAt: new Date().toISOString(),
|
|
123
123
|
approvedBy: String(by || "").trim() || gitUser(root) || userInfo().username || "unknown",
|
|
124
|
+
...(how ? { how } : {}),
|
|
124
125
|
};
|
|
125
126
|
mkdirSync(specPaths(root, name).dir, { recursive: true });
|
|
126
127
|
writeFileSync(specPaths(root, name).approval, `${JSON.stringify(approval, null, 2)}\n`);
|
package/package.json
CHANGED