@xiongxianfei/rigorloop 0.1.4
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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/bin/rigorloop.js +1499 -0
- package/dist/lib/command-result.js +33 -0
- package/dist/lib/lockfile.js +303 -0
- package/dist/lib/new-change-filesystem.js +154 -0
- package/dist/lib/new-change.js +226 -0
- package/dist/lib/official-archive-url.js +42 -0
- package/dist/metadata/adapter-artifacts-v0.1.4.json +30 -0
- package/dist/metadata/releases.json +11 -0
- package/package.json +19 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const EXIT = Object.freeze({
|
|
2
|
+
success: 0,
|
|
3
|
+
blocked: 2,
|
|
4
|
+
validationFailed: 3,
|
|
5
|
+
invalidUsage: 4,
|
|
6
|
+
mutationConflict: 5,
|
|
7
|
+
internal: 1,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const EXIT_CLASS_TO_CODE = Object.freeze({
|
|
11
|
+
success: EXIT.success,
|
|
12
|
+
blocked: EXIT.blocked,
|
|
13
|
+
validation_failed: EXIT.validationFailed,
|
|
14
|
+
invalid_usage: EXIT.invalidUsage,
|
|
15
|
+
mutation_conflict: EXIT.mutationConflict,
|
|
16
|
+
internal: EXIT.internal,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function exitCodeForResult(result) {
|
|
20
|
+
const exitClass = result.exit_class ?? result.exitClass;
|
|
21
|
+
if (Object.hasOwn(EXIT_CLASS_TO_CODE, exitClass)) {
|
|
22
|
+
return EXIT_CLASS_TO_CODE[exitClass];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (result.status === "success" || result.status === "warning") {
|
|
26
|
+
return EXIT.success;
|
|
27
|
+
}
|
|
28
|
+
if (result.status === "blocked") {
|
|
29
|
+
return EXIT.blocked;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return EXIT.internal;
|
|
33
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/i;
|
|
4
|
+
const SUPPORTED_SOURCES = new Set(["release-archive", "local-archive"]);
|
|
5
|
+
const TOP_LEVEL_FIELDS = ["schema_version", "rigorloop", "manifest", "generated"];
|
|
6
|
+
const RIGORLOOP_FIELDS = ["package", "version"];
|
|
7
|
+
const MANIFEST_FIELDS = ["path", "sha256"];
|
|
8
|
+
const GENERATED_FIELDS = ["adapters"];
|
|
9
|
+
const ADAPTER_FIELDS = [
|
|
10
|
+
"adapter",
|
|
11
|
+
"release",
|
|
12
|
+
"source",
|
|
13
|
+
"archive",
|
|
14
|
+
"archive_sha256",
|
|
15
|
+
"installed_root",
|
|
16
|
+
"tree_hash_algorithm",
|
|
17
|
+
"tree_sha256",
|
|
18
|
+
"file_count",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function failure(kind, code, message, path = "rigorloop.lock") {
|
|
22
|
+
return { ok: false, kind, code, message, path };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function unquote(value) {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
28
|
+
return trimmed.slice(1, -1);
|
|
29
|
+
}
|
|
30
|
+
return trimmed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseScalar(value) {
|
|
34
|
+
return unquote(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSha256(value) {
|
|
38
|
+
return typeof value === "string" && SHA256_PATTERN.test(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseTopLevel(lines) {
|
|
42
|
+
const sections = new Map();
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*):(?:\s+(.*))?$/);
|
|
45
|
+
if (match) {
|
|
46
|
+
sections.set(match[1], match[2] ?? "");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return sections;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseSection(lines, sectionName, allowedFields) {
|
|
53
|
+
const fields = {};
|
|
54
|
+
const start = lines.findIndex((line) => line === `${sectionName}:`);
|
|
55
|
+
if (start < 0) {
|
|
56
|
+
return { missing: true };
|
|
57
|
+
}
|
|
58
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
59
|
+
const line = lines[index];
|
|
60
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*:/.test(line)) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (line.trim() === "") {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const match = line.match(/^ ([A-Za-z_][A-Za-z0-9_-]*):(?:\s*(.*))?$/);
|
|
67
|
+
if (!match) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const [, key, value] = match;
|
|
71
|
+
if (!allowedFields.includes(key)) {
|
|
72
|
+
return failure("unsupported", "unsupported-lockfile-shape", `Unsupported field ${sectionName}.${key}`);
|
|
73
|
+
}
|
|
74
|
+
if (value === undefined || value === "") {
|
|
75
|
+
return failure("invalid", "invalid-lockfile", `Missing scalar value for ${sectionName}.${key}`);
|
|
76
|
+
}
|
|
77
|
+
fields[key] = parseScalar(value);
|
|
78
|
+
}
|
|
79
|
+
return { fields };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseGeneratedSection(lines) {
|
|
83
|
+
const start = lines.findIndex((line) => line === "generated:");
|
|
84
|
+
if (start < 0) {
|
|
85
|
+
return { missing: true };
|
|
86
|
+
}
|
|
87
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
88
|
+
const line = lines[index];
|
|
89
|
+
if (/^[A-Za-z_][A-Za-z0-9_-]*:/.test(line)) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (line.trim() === "" || line.startsWith(" ")) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const match = line.match(/^ ([A-Za-z_][A-Za-z0-9_-]*):(?:\s*(.*))?$/);
|
|
96
|
+
if (!match) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const key = match[1];
|
|
100
|
+
if (!GENERATED_FIELDS.includes(key)) {
|
|
101
|
+
return failure("unsupported", "unsupported-lockfile-shape", `Unsupported field generated.${key}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseAdapters(lines) {
|
|
108
|
+
const adaptersStart = lines.findIndex((line, index) => line === " adapters:" && lines[index - 1] === "generated:");
|
|
109
|
+
if (adaptersStart < 0) {
|
|
110
|
+
return { missing: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const adapters = [];
|
|
114
|
+
let current;
|
|
115
|
+
for (let index = adaptersStart + 1; index < lines.length; index += 1) {
|
|
116
|
+
const line = lines[index];
|
|
117
|
+
if (/^[A-Za-z_][A-Za-z0-9_-]*:/.test(line)) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (line.trim() === "") {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const generatedFieldMatch = line.match(/^ ([A-Za-z_][A-Za-z0-9_-]*):/);
|
|
124
|
+
if (generatedFieldMatch) {
|
|
125
|
+
return failure("unsupported", "unsupported-lockfile-shape", `Unsupported field generated.${generatedFieldMatch[1]}`);
|
|
126
|
+
}
|
|
127
|
+
const startMatch = line.match(/^ - ([A-Za-z_][A-Za-z0-9_-]*):(?:\s*(.*))?$/);
|
|
128
|
+
if (startMatch) {
|
|
129
|
+
if (current) {
|
|
130
|
+
adapters.push(current);
|
|
131
|
+
}
|
|
132
|
+
current = {};
|
|
133
|
+
const [, key, value] = startMatch;
|
|
134
|
+
if (!ADAPTER_FIELDS.includes(key)) {
|
|
135
|
+
return failure("unsupported", "unsupported-lockfile-shape", `Unsupported field generated.adapters[${adapters.length}].${key}`);
|
|
136
|
+
}
|
|
137
|
+
if (value === undefined || value === "") {
|
|
138
|
+
return failure("invalid", "invalid-lockfile", `Missing scalar value for generated.adapters[${adapters.length}].${key}`);
|
|
139
|
+
}
|
|
140
|
+
current[key] = parseScalar(value);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const fieldMatch = line.match(/^ ([A-Za-z_][A-Za-z0-9_-]*):(?:\s*(.*))?$/);
|
|
145
|
+
if (fieldMatch && current) {
|
|
146
|
+
const [, key, value] = fieldMatch;
|
|
147
|
+
if (!ADAPTER_FIELDS.includes(key)) {
|
|
148
|
+
return failure("unsupported", "unsupported-lockfile-shape", `Unsupported field generated.adapters[${adapters.length}].${key}`);
|
|
149
|
+
}
|
|
150
|
+
if (value === undefined || value === "") {
|
|
151
|
+
return failure("invalid", "invalid-lockfile", `Missing scalar value for generated.adapters[${adapters.length}].${key}`);
|
|
152
|
+
}
|
|
153
|
+
current[key] = parseScalar(value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (current) {
|
|
157
|
+
adapters.push(current);
|
|
158
|
+
}
|
|
159
|
+
return { adapters };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validateAdapter(adapter) {
|
|
163
|
+
for (const field of ADAPTER_FIELDS) {
|
|
164
|
+
if (adapter[field] === undefined) {
|
|
165
|
+
return failure("invalid", "invalid-lockfile", `Missing adapter field: ${field}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (adapter.adapter !== "codex") {
|
|
169
|
+
return failure("unsupported", "unsupported-lockfile-shape", "Only codex lockfile entries are supported in this slice.");
|
|
170
|
+
}
|
|
171
|
+
if (!SUPPORTED_SOURCES.has(adapter.source)) {
|
|
172
|
+
return failure("unsupported", "unsupported-lockfile-shape", "Unsupported lockfile adapter source.");
|
|
173
|
+
}
|
|
174
|
+
if (adapter.tree_hash_algorithm !== "rigorloop-tree-hash-v1") {
|
|
175
|
+
return failure("unsupported", "unsupported-lockfile-shape", "Unsupported tree hash algorithm.");
|
|
176
|
+
}
|
|
177
|
+
if (adapter.installed_root !== ".agents/skills") {
|
|
178
|
+
return failure("unsupported", "unsupported-lockfile-shape", "Unsupported installed root.");
|
|
179
|
+
}
|
|
180
|
+
if (!isSha256(adapter.archive_sha256) || !isSha256(adapter.tree_sha256)) {
|
|
181
|
+
return failure("invalid", "invalid-lockfile", "Adapter hashes must be SHA-256 values.");
|
|
182
|
+
}
|
|
183
|
+
const fileCount = Number.parseInt(adapter.file_count, 10);
|
|
184
|
+
if (!/^\d+$/.test(String(adapter.file_count)) || !Number.isInteger(fileCount) || fileCount < 0) {
|
|
185
|
+
return failure("invalid", "invalid-lockfile", "Adapter file_count must be a non-negative integer.");
|
|
186
|
+
}
|
|
187
|
+
adapter.file_count = fileCount;
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function parseLockfile(text) {
|
|
192
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
193
|
+
return failure("invalid", "invalid-lockfile", "rigorloop.lock is empty or not text.");
|
|
194
|
+
}
|
|
195
|
+
if (/[\[\]{}]/.test(text)) {
|
|
196
|
+
return failure("invalid", "invalid-lockfile", "rigorloop.lock is not valid strict schema_version 1 YAML.");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
200
|
+
const top = parseTopLevel(lines);
|
|
201
|
+
for (const key of top.keys()) {
|
|
202
|
+
if (!TOP_LEVEL_FIELDS.includes(key)) {
|
|
203
|
+
return failure("unsupported", "unsupported-lockfile-shape", `Unsupported top-level lockfile section: ${key}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const key of TOP_LEVEL_FIELDS) {
|
|
207
|
+
if (!top.has(key)) {
|
|
208
|
+
return failure("invalid", "invalid-lockfile", `Missing top-level lockfile section: ${key}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const schemaVersion = parseScalar(top.get("schema_version"));
|
|
212
|
+
if (!/^\d+$/.test(schemaVersion) || Number.parseInt(schemaVersion, 10) !== 1) {
|
|
213
|
+
return failure("unsupported", "unsupported-lockfile-shape", "Unsupported lockfile schema_version.");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rigorloop = parseSection(lines, "rigorloop", RIGORLOOP_FIELDS);
|
|
217
|
+
if (rigorloop.ok === false) {
|
|
218
|
+
return rigorloop;
|
|
219
|
+
}
|
|
220
|
+
const manifest = parseSection(lines, "manifest", MANIFEST_FIELDS);
|
|
221
|
+
if (manifest.ok === false) {
|
|
222
|
+
return manifest;
|
|
223
|
+
}
|
|
224
|
+
const generated = parseGeneratedSection(lines);
|
|
225
|
+
if (generated.ok === false) {
|
|
226
|
+
return generated;
|
|
227
|
+
}
|
|
228
|
+
if (rigorloop.missing || manifest.missing) {
|
|
229
|
+
return failure("invalid", "invalid-lockfile", "Missing required lockfile section.");
|
|
230
|
+
}
|
|
231
|
+
if (rigorloop.fields.package !== "@xiongxianfei/rigorloop" || typeof rigorloop.fields.version !== "string") {
|
|
232
|
+
return failure("invalid", "invalid-lockfile", "Invalid rigorloop package identity.");
|
|
233
|
+
}
|
|
234
|
+
if (manifest.fields.path !== "rigorloop.yaml" || !isSha256(manifest.fields.sha256)) {
|
|
235
|
+
return failure("invalid", "invalid-lockfile", "Invalid manifest lockfile entry.");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const adapterResult = parseAdapters(lines);
|
|
239
|
+
if (adapterResult.ok === false) {
|
|
240
|
+
return adapterResult;
|
|
241
|
+
}
|
|
242
|
+
if (adapterResult.missing || !adapterResult.adapters.length) {
|
|
243
|
+
return failure("invalid", "invalid-lockfile", "generated.adapters must contain at least one adapter entry.");
|
|
244
|
+
}
|
|
245
|
+
for (const adapter of adapterResult.adapters) {
|
|
246
|
+
const adapterError = validateAdapter(adapter);
|
|
247
|
+
if (adapterError) {
|
|
248
|
+
return adapterError;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
lockfile: {
|
|
255
|
+
schema_version: 1,
|
|
256
|
+
rigorloop: rigorloop.fields,
|
|
257
|
+
manifest: manifest.fields,
|
|
258
|
+
generated: {
|
|
259
|
+
adapters: adapterResult.adapters,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function serializeLockfile(lockfile) {
|
|
266
|
+
const adapters = [...lockfile.generated.adapters].sort((left, right) => left.adapter.localeCompare(right.adapter));
|
|
267
|
+
const lines = [
|
|
268
|
+
"schema_version: 1",
|
|
269
|
+
"",
|
|
270
|
+
"rigorloop:",
|
|
271
|
+
` package: "${lockfile.rigorloop.package}"`,
|
|
272
|
+
` version: "${lockfile.rigorloop.version}"`,
|
|
273
|
+
"",
|
|
274
|
+
"manifest:",
|
|
275
|
+
` path: "${lockfile.manifest.path}"`,
|
|
276
|
+
` sha256: "${lockfile.manifest.sha256}"`,
|
|
277
|
+
"",
|
|
278
|
+
"generated:",
|
|
279
|
+
" adapters:",
|
|
280
|
+
];
|
|
281
|
+
for (const adapter of adapters) {
|
|
282
|
+
lines.push(
|
|
283
|
+
` - adapter: ${adapter.adapter}`,
|
|
284
|
+
` release: "${adapter.release}"`,
|
|
285
|
+
` source: ${adapter.source}`,
|
|
286
|
+
` archive: "${adapter.archive}"`,
|
|
287
|
+
` archive_sha256: "${adapter.archive_sha256}"`,
|
|
288
|
+
` installed_root: "${adapter.installed_root}"`,
|
|
289
|
+
` tree_hash_algorithm: ${adapter.tree_hash_algorithm}`,
|
|
290
|
+
` tree_sha256: "${adapter.tree_sha256}"`,
|
|
291
|
+
` file_count: ${adapter.file_count}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return `${lines.join("\n")}\n`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function sha256NormalizedText(bytes) {
|
|
298
|
+
let text = Buffer.isBuffer(bytes) ? bytes.toString("utf8") : String(bytes);
|
|
299
|
+
if (text.charCodeAt(0) === 0xfeff) {
|
|
300
|
+
text = text.slice(1);
|
|
301
|
+
}
|
|
302
|
+
return createHash("sha256").update(Buffer.from(text.replace(/\r\n?/g, "\n"), "utf8")).digest("hex");
|
|
303
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_FS_OPS = {
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function lstatRelative(cwd, relativePath, fsOps) {
|
|
11
|
+
try {
|
|
12
|
+
return fsOps.lstatSync(resolve(cwd, relativePath));
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error.code === "ENOENT") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function newChangeAction(type, path, status, reason) {
|
|
22
|
+
return { type, path, status, reason };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function newChangeBlocker(code, path, message) {
|
|
26
|
+
return {
|
|
27
|
+
code,
|
|
28
|
+
message,
|
|
29
|
+
path,
|
|
30
|
+
next_action: code === "path-exists"
|
|
31
|
+
? "Choose a new change id or move the existing file before rerunning."
|
|
32
|
+
: "Replace the conflicting path with a directory before rerunning.",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function newChangeWriteError(path, error) {
|
|
37
|
+
return {
|
|
38
|
+
code: "write-failed",
|
|
39
|
+
message: `Failed to write ${path}: ${error.message}`,
|
|
40
|
+
path,
|
|
41
|
+
next_action: "Inspect filesystem permissions and rerun the command after resolving the write failure.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function planNewChangeWrite(cwd, draft, fsOps = DEFAULT_FS_OPS) {
|
|
46
|
+
const directories = ["docs", "docs/changes", draft.change.root];
|
|
47
|
+
const actions = [];
|
|
48
|
+
const blockers = [];
|
|
49
|
+
|
|
50
|
+
for (const relativePath of directories) {
|
|
51
|
+
const stat = lstatRelative(cwd, relativePath, fsOps);
|
|
52
|
+
if (!stat) {
|
|
53
|
+
actions.push(newChangeAction("create-dir", relativePath, "planned", "Directory will be created."));
|
|
54
|
+
} else if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
55
|
+
actions.push(newChangeAction("create-dir", relativePath, "existing", "Directory already exists."));
|
|
56
|
+
} else {
|
|
57
|
+
actions.push(newChangeAction("create-dir", relativePath, "blocked", "Path exists and is not a directory."));
|
|
58
|
+
blockers.push(newChangeBlocker("path-not-directory", relativePath, "Planned directory path exists and is not a directory."));
|
|
59
|
+
return { actions, blockers };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const metadataStat = lstatRelative(cwd, draft.change.metadata_path, fsOps);
|
|
64
|
+
if (metadataStat) {
|
|
65
|
+
actions.push(newChangeAction("write", draft.change.metadata_path, "blocked", "File already exists and will not be overwritten."));
|
|
66
|
+
blockers.push(newChangeBlocker("path-exists", draft.change.metadata_path, "Planned change metadata file already exists."));
|
|
67
|
+
} else {
|
|
68
|
+
actions.push(newChangeAction("write", draft.change.metadata_path, "planned", "Change metadata file will be written."));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { actions, blockers };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function artifactForNewChange(draft, status) {
|
|
75
|
+
return {
|
|
76
|
+
path: draft.change.metadata_path,
|
|
77
|
+
kind: "change-metadata",
|
|
78
|
+
status,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function applyNewChangePlan(cwd, actions, draft, fsOps = DEFAULT_FS_OPS) {
|
|
83
|
+
for (const action of actions) {
|
|
84
|
+
if (action.type === "create-dir" && action.status === "planned") {
|
|
85
|
+
try {
|
|
86
|
+
fsOps.mkdirSync(resolve(cwd, action.path));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
action.status = "failed";
|
|
89
|
+
action.reason = "Directory creation failed.";
|
|
90
|
+
return { ok: false, error: newChangeWriteError(action.path, error) };
|
|
91
|
+
}
|
|
92
|
+
action.status = "done";
|
|
93
|
+
action.reason = "Directory created.";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const metadataAction = actions.find((action) => action.path === draft.change.metadata_path);
|
|
98
|
+
if (metadataAction?.status === "planned") {
|
|
99
|
+
try {
|
|
100
|
+
fsOps.writeFileSync(resolve(cwd, draft.change.metadata_path), draft.planned_change_metadata.content, "utf8");
|
|
101
|
+
} catch (error) {
|
|
102
|
+
metadataAction.status = "failed";
|
|
103
|
+
metadataAction.reason = "Change metadata file write failed.";
|
|
104
|
+
return { ok: false, error: newChangeWriteError(draft.change.metadata_path, error) };
|
|
105
|
+
}
|
|
106
|
+
metadataAction.status = "done";
|
|
107
|
+
metadataAction.reason = "Change metadata file written.";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function runNewChangePlan({ cwd, draft, flags, profile, fsOps = DEFAULT_FS_OPS }) {
|
|
114
|
+
const writePlan = planNewChangeWrite(cwd, draft, fsOps);
|
|
115
|
+
const blocked = writePlan.blockers.length > 0;
|
|
116
|
+
const warnings =
|
|
117
|
+
profile === "minimal" && !blocked
|
|
118
|
+
? [
|
|
119
|
+
{
|
|
120
|
+
code: "durable-reasoning-not-scaffolded",
|
|
121
|
+
message: "new-change created only change metadata; durable reasoning remains a later workflow requirement.",
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
: [];
|
|
125
|
+
|
|
126
|
+
let applyResult = { ok: true };
|
|
127
|
+
if (!flags.dryRun && !blocked) {
|
|
128
|
+
applyResult = applyNewChangePlan(cwd, writePlan.actions, draft, fsOps);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const failed = !applyResult.ok;
|
|
132
|
+
const artifactStatus = failed ? "failed" : blocked ? "blocked" : flags.dryRun ? "planned" : "created";
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
exit_class: failed ? "internal" : blocked ? "mutation_conflict" : "success",
|
|
136
|
+
result: {
|
|
137
|
+
status: failed ? "error" : blocked ? "blocked" : warnings.length > 0 ? "warning" : "success",
|
|
138
|
+
summary: failed
|
|
139
|
+
? "RigorLoop new-change failed while writing files."
|
|
140
|
+
: blocked
|
|
141
|
+
? "RigorLoop new-change blocked before writing files."
|
|
142
|
+
: flags.dryRun
|
|
143
|
+
? "RigorLoop new-change dry run completed. No files were written."
|
|
144
|
+
: "RigorLoop change metadata scaffold created.",
|
|
145
|
+
actions: writePlan.actions,
|
|
146
|
+
artifacts: [artifactForNewChange(draft, artifactStatus)],
|
|
147
|
+
blockers: writePlan.blockers,
|
|
148
|
+
warnings,
|
|
149
|
+
errors: failed ? [applyResult.error] : [],
|
|
150
|
+
change: draft.change,
|
|
151
|
+
planned_change_metadata: draft.planned_change_metadata,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|