@xiongxianfei/rigorloop 0.2.0 → 0.3.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/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
RigorLoop CLI for repository-local AI-assisted software delivery.
|
|
4
4
|
|
|
5
|
-
This package exposes the `rigorloop` binary for approved CLI workflows such as
|
|
5
|
+
This package exposes the `rigorloop` binary for approved CLI workflows such as
|
|
6
|
+
target initialization and change metadata scaffolding. Release archives remain
|
|
7
|
+
verified GitHub release artifacts; they are not bundled into the npm package.
|
|
8
|
+
npm is the CLI delivery channel, not the canonical source for workflow rules,
|
|
9
|
+
skills, schemas, templates, or adapter archives.
|
|
6
10
|
|
|
7
11
|
## Quick Start
|
|
8
12
|
|
|
@@ -11,15 +15,15 @@ Run directly with `npx`; no install step is required:
|
|
|
11
15
|
```bash
|
|
12
16
|
npx @xiongxianfei/rigorloop@latest --help
|
|
13
17
|
npx @xiongxianfei/rigorloop@latest version
|
|
14
|
-
npx @xiongxianfei/rigorloop@latest init
|
|
15
|
-
npx @xiongxianfei/rigorloop@latest init
|
|
16
|
-
npx @xiongxianfei/rigorloop@latest init
|
|
18
|
+
npx @xiongxianfei/rigorloop@latest init codex
|
|
19
|
+
npx @xiongxianfei/rigorloop@latest init claude
|
|
20
|
+
npx @xiongxianfei/rigorloop@latest init opencode
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
Use a pinned version when you want reproducible setup:
|
|
20
24
|
|
|
21
25
|
```bash
|
|
22
|
-
npx @xiongxianfei/rigorloop@0.
|
|
26
|
+
npx @xiongxianfei/rigorloop@0.3.0 init codex
|
|
23
27
|
```
|
|
24
28
|
|
|
25
29
|
Install as a project-local development dependency:
|
|
@@ -27,7 +31,7 @@ Install as a project-local development dependency:
|
|
|
27
31
|
```bash
|
|
28
32
|
npm install --save-dev @xiongxianfei/rigorloop
|
|
29
33
|
npx rigorloop --help
|
|
30
|
-
npx rigorloop init
|
|
34
|
+
npx rigorloop init codex
|
|
31
35
|
```
|
|
32
36
|
|
|
33
37
|
Install globally only if you want a machine-wide `rigorloop` command:
|
|
@@ -35,7 +39,7 @@ Install globally only if you want a machine-wide `rigorloop` command:
|
|
|
35
39
|
```bash
|
|
36
40
|
npm install --global @xiongxianfei/rigorloop
|
|
37
41
|
rigorloop --help
|
|
38
|
-
rigorloop init
|
|
42
|
+
rigorloop init codex
|
|
39
43
|
```
|
|
40
44
|
|
|
41
45
|
## Commands
|
|
@@ -43,35 +47,35 @@ rigorloop init --adapter codex
|
|
|
43
47
|
```bash
|
|
44
48
|
rigorloop --help
|
|
45
49
|
rigorloop version
|
|
46
|
-
rigorloop init
|
|
50
|
+
rigorloop init codex|claude|opencode [--write-state] [--from-archive <path>] [--dry-run] [--json]
|
|
47
51
|
rigorloop new-change <change-id> --title <title> [--dry-run] [--json]
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
##
|
|
54
|
+
## Target Init
|
|
51
55
|
|
|
52
|
-
Initialize
|
|
56
|
+
Initialize target support from the verified official release archive:
|
|
53
57
|
|
|
54
58
|
```bash
|
|
55
|
-
npx @xiongxianfei/rigorloop@0.
|
|
56
|
-
npx @xiongxianfei/rigorloop@0.
|
|
57
|
-
npx @xiongxianfei/rigorloop@0.
|
|
59
|
+
npx @xiongxianfei/rigorloop@0.3.0 init codex --json
|
|
60
|
+
npx @xiongxianfei/rigorloop@0.3.0 init claude --json
|
|
61
|
+
npx @xiongxianfei/rigorloop@0.3.0 init opencode --json
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
Preview the write plan without mutating files:
|
|
61
65
|
|
|
62
66
|
```bash
|
|
63
|
-
npx @xiongxianfei/rigorloop@0.
|
|
67
|
+
npx @xiongxianfei/rigorloop@0.3.0 init opencode --dry-run --json
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
Use `--from-archive` when you already downloaded the matching official archive, or when Node `fetch()` cannot reach GitHub from your network:
|
|
67
71
|
|
|
68
72
|
```bash
|
|
69
|
-
npx @xiongxianfei/rigorloop@0.
|
|
70
|
-
npx @xiongxianfei/rigorloop@0.
|
|
71
|
-
npx @xiongxianfei/rigorloop@0.
|
|
73
|
+
npx @xiongxianfei/rigorloop@0.3.0 init codex --from-archive ./rigorloop-adapter-codex-v0.3.0.zip --json
|
|
74
|
+
npx @xiongxianfei/rigorloop@0.3.0 init claude --from-archive ./rigorloop-adapter-claude-v0.3.0.zip --json
|
|
75
|
+
npx @xiongxianfei/rigorloop@0.3.0 init opencode --from-archive ./rigorloop-adapter-opencode-v0.3.0.zip --json
|
|
72
76
|
```
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
Default init installs verified target support without writing `rigorloop.yaml` or `rigorloop.lock`. Use `--write-state` when you want RigorLoop-managed project state files. The command verifies the selected archive before extraction and verifies the installed tree before reporting success. Runtime roots are target-specific:
|
|
75
79
|
|
|
76
80
|
```text
|
|
77
81
|
codex: .agents/skills
|
|
@@ -79,27 +83,27 @@ claude: .claude/skills
|
|
|
79
83
|
opencode: .opencode/skills and .opencode/commands when command aliases are declared
|
|
80
84
|
```
|
|
81
85
|
|
|
82
|
-
Network installs use Node `fetch()`. If download fails in a proxied environment, JSON output reports bounded diagnostics such as
|
|
86
|
+
Network installs use Node `fetch()`. If download fails in a proxied environment, JSON output reports bounded diagnostics such as target name, release version, trusted archive URL, detected proxy environment variable names, Node env-proxy status, and failure class. It does not print proxy credentials or raw proxy values. On Node versions that support env-proxy, enable it with `NODE_USE_ENV_PROXY=1`, `NODE_OPTIONS=--use-env-proxy`, or `node --use-env-proxy`; otherwise use the `--from-archive` fallback.
|
|
83
87
|
|
|
84
88
|
## Change Metadata Scaffold
|
|
85
89
|
|
|
86
90
|
Create a new change metadata scaffold:
|
|
87
91
|
|
|
88
92
|
```bash
|
|
89
|
-
npx @xiongxianfei/rigorloop@0.
|
|
93
|
+
npx @xiongxianfei/rigorloop@0.3.0 new-change my-change --title "Describe the change" --json
|
|
90
94
|
```
|
|
91
95
|
|
|
92
96
|
Preview the scaffold first:
|
|
93
97
|
|
|
94
98
|
```bash
|
|
95
|
-
npx @xiongxianfei/rigorloop@0.
|
|
99
|
+
npx @xiongxianfei/rigorloop@0.3.0 new-change my-change --title "Describe the change" --dry-run --json
|
|
96
100
|
```
|
|
97
101
|
|
|
98
102
|
`new-change` creates `docs/changes/<change-id>/change.yaml`. It does not claim that proposal, spec, review, verification, or PR readiness is complete.
|
|
99
103
|
|
|
100
104
|
## Version Guidance
|
|
101
105
|
|
|
102
|
-
Use `@latest` for manual exploration. Use an explicit version such as `@0.
|
|
106
|
+
Use `@latest` for manual exploration. Use an explicit version such as `@0.3.0` for CI, onboarding docs, and repeatable agent setup.
|
|
103
107
|
|
|
104
108
|
## Source of Truth
|
|
105
109
|
|
package/dist/bin/rigorloop.js
CHANGED
|
@@ -36,6 +36,8 @@ function parseFlags(args) {
|
|
|
36
36
|
noColor: Boolean(process.env.NO_COLOR),
|
|
37
37
|
dryRun: false,
|
|
38
38
|
adapter: undefined,
|
|
39
|
+
adapterOptionUsed: false,
|
|
40
|
+
writeState: false,
|
|
39
41
|
fromArchiveProvided: false,
|
|
40
42
|
fromArchive: undefined,
|
|
41
43
|
force: false,
|
|
@@ -55,10 +57,13 @@ function parseFlags(args) {
|
|
|
55
57
|
} else if (arg === "--dry-run") {
|
|
56
58
|
flags.dryRun = true;
|
|
57
59
|
} else if (arg === "--adapter") {
|
|
60
|
+
flags.adapterOptionUsed = true;
|
|
58
61
|
if (args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
59
62
|
flags.adapter = args[index + 1];
|
|
60
63
|
index += 1;
|
|
61
64
|
}
|
|
65
|
+
} else if (arg === "--write-state") {
|
|
66
|
+
flags.writeState = true;
|
|
62
67
|
} else if (arg === "--from-archive") {
|
|
63
68
|
flags.fromArchiveProvided = true;
|
|
64
69
|
if (args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
@@ -110,13 +115,13 @@ function usage() {
|
|
|
110
115
|
Usage:
|
|
111
116
|
rigorloop --help
|
|
112
117
|
rigorloop version
|
|
113
|
-
rigorloop init
|
|
118
|
+
rigorloop init codex|claude|opencode [--write-state] [--dry-run] [--json]
|
|
114
119
|
rigorloop new-change <change-id> --title <title> [--dry-run] [--json]
|
|
115
120
|
|
|
116
121
|
Commands:
|
|
117
122
|
version Print package name and version.
|
|
118
|
-
init
|
|
119
|
-
Initialize
|
|
123
|
+
init codex|claude|opencode
|
|
124
|
+
Initialize verified target support.
|
|
120
125
|
new-change Plan a change metadata scaffold.
|
|
121
126
|
`;
|
|
122
127
|
}
|
|
@@ -141,7 +146,7 @@ function sourceForFlags(flags, info, descriptor) {
|
|
|
141
146
|
};
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
function
|
|
149
|
+
function manifestTargetBlock(source, descriptor, artifact) {
|
|
145
150
|
const sourceLines =
|
|
146
151
|
source.type === "local-archive"
|
|
147
152
|
? [` type: local-archive`, ` archive: "${source.archive}"`]
|
|
@@ -155,22 +160,26 @@ function manifestAdapterBlock(source, descriptor, artifact) {
|
|
|
155
160
|
...Object.entries(installRoots).map(([role, root]) => ` ${role}: "${root}"`),
|
|
156
161
|
];
|
|
157
162
|
|
|
158
|
-
return ` -
|
|
163
|
+
return ` - target: ${descriptor.name}
|
|
159
164
|
${rootLines.join("\n")}
|
|
160
165
|
source:
|
|
161
166
|
${sourceLines.join("\n")}`;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
function parseManifestAdapterBlocks(content) {
|
|
165
|
-
|
|
170
|
+
const isLegacySchema = content.includes("schema_version: 1") && content.includes("adapters:");
|
|
171
|
+
const isTargetSchema = content.includes("schema_version: 2") && content.includes("targets:");
|
|
172
|
+
if (!isLegacySchema && !isTargetSchema) {
|
|
166
173
|
return { error: { code: "invalid-config", message: "Existing rigorloop.yaml is not compatible with the init contract." } };
|
|
167
174
|
}
|
|
168
175
|
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
169
|
-
const
|
|
176
|
+
const listKey = isTargetSchema ? "targets:" : "adapters:";
|
|
177
|
+
const entryPrefix = isTargetSchema ? " - target: " : " - name: ";
|
|
178
|
+
const adapterStart = lines.findIndex((line) => line === listKey);
|
|
170
179
|
const blocks = [];
|
|
171
180
|
let current = [];
|
|
172
181
|
for (const line of lines.slice(adapterStart + 1)) {
|
|
173
|
-
if (line.startsWith(
|
|
182
|
+
if (line.startsWith(entryPrefix)) {
|
|
174
183
|
if (current.length) {
|
|
175
184
|
blocks.push(current);
|
|
176
185
|
}
|
|
@@ -186,37 +195,57 @@ function parseManifestAdapterBlocks(content) {
|
|
|
186
195
|
}
|
|
187
196
|
const adapters = [];
|
|
188
197
|
for (const block of blocks) {
|
|
189
|
-
const name = block[0].slice(
|
|
198
|
+
const name = block[0].slice(entryPrefix.length).trim();
|
|
190
199
|
const descriptor = adapterDescriptor(name);
|
|
191
200
|
if (!descriptor) {
|
|
192
201
|
return { error: { code: "invalid-config", message: `Existing rigorloop.yaml includes unsupported adapter ${name}.` } };
|
|
193
202
|
}
|
|
194
|
-
|
|
203
|
+
const blockText = isTargetSchema ? block.join("\n").replace(/\n+$/, "") : block.join("\n").replace(/\n+$/, "").replace(/^ - name:/, " - target:");
|
|
204
|
+
adapters.push({ name, block: blockText, roots: manifestBlockRoots(blockText) });
|
|
195
205
|
}
|
|
196
206
|
return { adapters };
|
|
197
207
|
}
|
|
198
208
|
|
|
199
|
-
function
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
function manifestBlockRoots(block) {
|
|
210
|
+
const roots = [];
|
|
211
|
+
let inInstallRoots = false;
|
|
212
|
+
for (const line of block.split("\n")) {
|
|
213
|
+
const singleRootMatch = line.match(/^ install_root:\s+"([^"]+)"\s*$/);
|
|
214
|
+
if (singleRootMatch) {
|
|
215
|
+
roots.push(singleRootMatch[1]);
|
|
216
|
+
inInstallRoots = false;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (line === " install_roots:") {
|
|
220
|
+
inInstallRoots = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (/^ [A-Za-z_][A-Za-z0-9_-]*:/.test(line)) {
|
|
224
|
+
inInstallRoots = false;
|
|
225
|
+
}
|
|
226
|
+
const rootMatch = inInstallRoots ? line.match(/^ [A-Za-z_][A-Za-z0-9_-]*:\s+"([^"]+)"\s*$/) : undefined;
|
|
227
|
+
if (rootMatch) {
|
|
228
|
+
roots.push(rootMatch[1]);
|
|
212
229
|
}
|
|
213
230
|
}
|
|
214
|
-
return
|
|
231
|
+
return roots;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function manifestContent(info, source, descriptor, existingContent) {
|
|
235
|
+
const selectedBlock = manifestTargetBlock(source, descriptor, source.artifact);
|
|
236
|
+
const parsed = existingContent ? parseManifestAdapterBlocks(existingContent) : undefined;
|
|
237
|
+
const preserved =
|
|
238
|
+
parsed && !parsed.error
|
|
239
|
+
? parsed.adapters
|
|
240
|
+
.filter((entry) => entry.name !== descriptor.name)
|
|
241
|
+
.map((entry) => entry.block.replace(/^ - name:/, " - target:"))
|
|
242
|
+
: [];
|
|
243
|
+
return `schema_version: 2
|
|
215
244
|
rigorloop:
|
|
216
245
|
package: "${info.name}"
|
|
217
246
|
package_version: "${info.version}"
|
|
218
|
-
|
|
219
|
-
${selectedBlock}
|
|
247
|
+
targets:
|
|
248
|
+
${[...preserved, selectedBlock].join("\n")}
|
|
220
249
|
`;
|
|
221
250
|
}
|
|
222
251
|
|
|
@@ -251,7 +280,7 @@ function usesMultiRootLockfile(descriptor, artifact) {
|
|
|
251
280
|
|
|
252
281
|
function lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes = rootHashesForArtifact(descriptor, artifact)) {
|
|
253
282
|
const entry = {
|
|
254
|
-
|
|
283
|
+
target: descriptor.name,
|
|
255
284
|
release: releaseForPackage(info.version),
|
|
256
285
|
source: source.type,
|
|
257
286
|
archive: source.archive,
|
|
@@ -277,7 +306,7 @@ function lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes
|
|
|
277
306
|
function plannedLockfile(info, source, manifest, descriptor) {
|
|
278
307
|
const artifact = source.artifact;
|
|
279
308
|
return {
|
|
280
|
-
schema_version:
|
|
309
|
+
schema_version: 3,
|
|
281
310
|
rigorloop: {
|
|
282
311
|
package: info.name,
|
|
283
312
|
version: info.version,
|
|
@@ -287,7 +316,7 @@ function plannedLockfile(info, source, manifest, descriptor) {
|
|
|
287
316
|
sha256: sha256NormalizedText(manifest),
|
|
288
317
|
},
|
|
289
318
|
generated: {
|
|
290
|
-
|
|
319
|
+
targets: [lockfileEntryForAdapter(info, source, artifact, descriptor)],
|
|
291
320
|
},
|
|
292
321
|
};
|
|
293
322
|
}
|
|
@@ -301,7 +330,16 @@ function existingLockfileEntries(selectedAdapter) {
|
|
|
301
330
|
if (!parsed.ok) {
|
|
302
331
|
return [];
|
|
303
332
|
}
|
|
304
|
-
|
|
333
|
+
const entries = parsed.lockfile.generated.targets ?? parsed.lockfile.generated.adapters;
|
|
334
|
+
return entries
|
|
335
|
+
.filter((entry) => (entry.target ?? entry.adapter) !== selectedAdapter)
|
|
336
|
+
.map((entry) => {
|
|
337
|
+
if (entry.target) {
|
|
338
|
+
return entry;
|
|
339
|
+
}
|
|
340
|
+
const { adapter, ...rest } = entry;
|
|
341
|
+
return { target: adapter, ...rest };
|
|
342
|
+
});
|
|
305
343
|
}
|
|
306
344
|
|
|
307
345
|
function lockfileForVerifiedInstall(info, source, manifest, artifact, rootHashes, descriptor) {
|
|
@@ -310,7 +348,7 @@ function lockfileForVerifiedInstall(info, source, manifest, artifact, rootHashes
|
|
|
310
348
|
lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes),
|
|
311
349
|
];
|
|
312
350
|
return {
|
|
313
|
-
schema_version:
|
|
351
|
+
schema_version: 3,
|
|
314
352
|
rigorloop: {
|
|
315
353
|
package: info.name,
|
|
316
354
|
version: info.version,
|
|
@@ -320,7 +358,7 @@ function lockfileForVerifiedInstall(info, source, manifest, artifact, rootHashes
|
|
|
320
358
|
sha256: sha256NormalizedText(manifest),
|
|
321
359
|
},
|
|
322
360
|
generated: {
|
|
323
|
-
adapters,
|
|
361
|
+
targets: adapters,
|
|
324
362
|
},
|
|
325
363
|
};
|
|
326
364
|
}
|
|
@@ -751,7 +789,7 @@ function unsafePathCode(name, descriptor, artifact) {
|
|
|
751
789
|
}
|
|
752
790
|
|
|
753
791
|
function isArchiveSupportEntry(name) {
|
|
754
|
-
return name === "AGENTS.md";
|
|
792
|
+
return name === "AGENTS.md" || name === "CLAUDE.md";
|
|
755
793
|
}
|
|
756
794
|
|
|
757
795
|
function fileRowsForTreeRoot(entries, installRoot) {
|
|
@@ -848,11 +886,11 @@ function currentLockfileEntries() {
|
|
|
848
886
|
if (!parsed.ok) {
|
|
849
887
|
return [];
|
|
850
888
|
}
|
|
851
|
-
return parsed.lockfile.generated.adapters;
|
|
889
|
+
return parsed.lockfile.generated.targets ?? parsed.lockfile.generated.adapters;
|
|
852
890
|
}
|
|
853
891
|
|
|
854
892
|
function currentLockfileEntry(descriptor) {
|
|
855
|
-
return currentLockfileEntries().find((entry) => entry.adapter === descriptor.name);
|
|
893
|
+
return currentLockfileEntries().find((entry) => (entry.target ?? entry.adapter) === descriptor.name);
|
|
856
894
|
}
|
|
857
895
|
|
|
858
896
|
function installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) {
|
|
@@ -936,6 +974,7 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
936
974
|
const rootHash = lockfileEntry.root_hashes[role];
|
|
937
975
|
const blocker = lockfileDriftBlocker({
|
|
938
976
|
adapter: lockfileEntry.adapter,
|
|
977
|
+
target: lockfileEntry.target,
|
|
939
978
|
installed_root: root,
|
|
940
979
|
tree_sha256: rootHash.tree_sha256,
|
|
941
980
|
file_count: rootHash.file_count,
|
|
@@ -952,7 +991,7 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
952
991
|
return {
|
|
953
992
|
code: "generated-output-missing",
|
|
954
993
|
message: "Codex generated output recorded in rigorloop.lock is missing.",
|
|
955
|
-
|
|
994
|
+
target: lockfileEntry.target ?? lockfileEntry.adapter,
|
|
956
995
|
installed_root: lockfileEntry.installed_root,
|
|
957
996
|
expected_tree_sha256: lockfileEntry.tree_sha256,
|
|
958
997
|
actual_tree_sha256: null,
|
|
@@ -973,7 +1012,7 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
973
1012
|
return {
|
|
974
1013
|
code: "generated-output-drift",
|
|
975
1014
|
message: "Codex generated output differs from rigorloop.lock.",
|
|
976
|
-
|
|
1015
|
+
target: lockfileEntry.target ?? lockfileEntry.adapter,
|
|
977
1016
|
installed_root: lockfileEntry.installed_root,
|
|
978
1017
|
expected_tree_sha256: lockfileEntry.tree_sha256,
|
|
979
1018
|
actual_tree_sha256: actualTree.treeHash,
|
|
@@ -986,6 +1025,116 @@ function lockfileDriftBlocker(lockfileEntry) {
|
|
|
986
1025
|
return undefined;
|
|
987
1026
|
}
|
|
988
1027
|
|
|
1028
|
+
function lockfileEntryTarget(entry) {
|
|
1029
|
+
return entry.target ?? entry.adapter;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function lockfileEntryRoots(entry) {
|
|
1033
|
+
if (entry.installed_roots) {
|
|
1034
|
+
return Object.values(entry.installed_roots);
|
|
1035
|
+
}
|
|
1036
|
+
return entry.installed_root ? [entry.installed_root] : [];
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function rootsOverlap(leftRoots, rightRoots) {
|
|
1040
|
+
return leftRoots.some((left) =>
|
|
1041
|
+
rightRoots.some((right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`)),
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function targetRootConflictBlocker(target, path, reason) {
|
|
1046
|
+
return {
|
|
1047
|
+
code: "target-root-conflict",
|
|
1048
|
+
message: reason,
|
|
1049
|
+
target,
|
|
1050
|
+
path,
|
|
1051
|
+
next_action: "Resolve the existing RigorLoop state before running init.",
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function existingStateSafetyBlocker(descriptor, artifact) {
|
|
1056
|
+
const targetRoots = Object.values(rootsForArtifact(descriptor, artifact));
|
|
1057
|
+
const manifestPath = resolve(process.cwd(), "rigorloop.yaml");
|
|
1058
|
+
if (existsSync(manifestPath)) {
|
|
1059
|
+
const parsedManifest = parseManifestAdapterBlocks(readFileSync(manifestPath, "utf8"));
|
|
1060
|
+
if (parsedManifest.error) {
|
|
1061
|
+
return {
|
|
1062
|
+
code: "state-invalid",
|
|
1063
|
+
message: "Existing RigorLoop state is malformed; refusing to mutate target roots.",
|
|
1064
|
+
path: "rigorloop.yaml",
|
|
1065
|
+
next_action: "Fix or move rigorloop.yaml before running init.",
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const selectedEntries = parsedManifest.adapters.filter((entry) => entry.name === descriptor.name);
|
|
1069
|
+
if (selectedEntries.length > 1) {
|
|
1070
|
+
return {
|
|
1071
|
+
code: "duplicate-target-entry",
|
|
1072
|
+
message: `Existing rigorloop.yaml contains duplicate ${descriptor.displayName} target entries.`,
|
|
1073
|
+
path: "rigorloop.yaml",
|
|
1074
|
+
next_action: "Remove duplicate target entries before running init.",
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
for (const entry of parsedManifest.adapters) {
|
|
1078
|
+
if (entry.name === descriptor.name && entry.roots.length && !rootsOverlap(entry.roots, targetRoots)) {
|
|
1079
|
+
return targetRootConflictBlocker(
|
|
1080
|
+
descriptor.name,
|
|
1081
|
+
"rigorloop.yaml",
|
|
1082
|
+
`Existing RigorLoop state records target ${descriptor.name} with a conflicting install root.`,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
if (entry.name !== descriptor.name && rootsOverlap(entry.roots, targetRoots)) {
|
|
1086
|
+
return targetRootConflictBlocker(
|
|
1087
|
+
descriptor.name,
|
|
1088
|
+
"rigorloop.yaml",
|
|
1089
|
+
`Existing RigorLoop state records an overlapping install root for target ${entry.name}.`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const lockfilePath = resolve(process.cwd(), LOCKFILE_PATH);
|
|
1096
|
+
if (!existsSync(lockfilePath)) {
|
|
1097
|
+
return undefined;
|
|
1098
|
+
}
|
|
1099
|
+
const parsedLockfile = parseLockfile(readFileSync(lockfilePath, "utf8"));
|
|
1100
|
+
if (!parsedLockfile.ok) {
|
|
1101
|
+
return {
|
|
1102
|
+
code: parsedLockfile.code,
|
|
1103
|
+
message: "Existing RigorLoop lock state is malformed or unsupported; refusing to mutate target roots.",
|
|
1104
|
+
path: LOCKFILE_PATH,
|
|
1105
|
+
next_action: "Fix or move rigorloop.lock before running init.",
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const entries = parsedLockfile.lockfile.generated.targets ?? parsedLockfile.lockfile.generated.adapters;
|
|
1109
|
+
for (const entry of entries) {
|
|
1110
|
+
const entryTarget = lockfileEntryTarget(entry);
|
|
1111
|
+
const entryRoots = lockfileEntryRoots(entry);
|
|
1112
|
+
const selected = entryTarget === descriptor.name;
|
|
1113
|
+
const overlapping = rootsOverlap(entryRoots, targetRoots);
|
|
1114
|
+
if (selected && entryRoots.length && !overlapping) {
|
|
1115
|
+
return targetRootConflictBlocker(
|
|
1116
|
+
descriptor.name,
|
|
1117
|
+
LOCKFILE_PATH,
|
|
1118
|
+
`Existing RigorLoop lock state records target ${descriptor.name} with a conflicting install root.`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (!selected && overlapping) {
|
|
1122
|
+
return targetRootConflictBlocker(
|
|
1123
|
+
descriptor.name,
|
|
1124
|
+
LOCKFILE_PATH,
|
|
1125
|
+
`Existing RigorLoop lock state records an overlapping install root for target ${entryTarget}.`,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
if (selected || overlapping) {
|
|
1129
|
+
const drift = lockfileDriftBlocker(entry);
|
|
1130
|
+
if (drift) {
|
|
1131
|
+
return drift;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return undefined;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
989
1138
|
function firstLockfileDriftBlocker() {
|
|
990
1139
|
for (const entry of currentLockfileEntries()) {
|
|
991
1140
|
const blocker = lockfileDriftBlocker(entry);
|
|
@@ -1232,7 +1381,7 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
|
|
|
1232
1381
|
status: flags.dryRun ? "planned" : "pending",
|
|
1233
1382
|
reason: flags.dryRun
|
|
1234
1383
|
? "Plan durable lockfile content."
|
|
1235
|
-
: "Write durable lockfile after verified
|
|
1384
|
+
: "Write durable lockfile after verified target install.",
|
|
1236
1385
|
});
|
|
1237
1386
|
artifacts.push({
|
|
1238
1387
|
path: LOCKFILE_PATH,
|
|
@@ -1250,7 +1399,7 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
|
|
|
1250
1399
|
status: flags.dryRun ? "planned" : "pending",
|
|
1251
1400
|
reason: flags.dryRun
|
|
1252
1401
|
? "Plan update to supported rigorloop.lock."
|
|
1253
|
-
: "Update supported rigorloop.lock after verified
|
|
1402
|
+
: "Update supported rigorloop.lock after verified target install.",
|
|
1254
1403
|
});
|
|
1255
1404
|
artifacts.push({
|
|
1256
1405
|
path: LOCKFILE_PATH,
|
|
@@ -1298,7 +1447,7 @@ function buildInitPlan(flags, descriptor, artifact) {
|
|
|
1298
1447
|
const manifestPath = "rigorloop.yaml";
|
|
1299
1448
|
const manifestAbsolutePath = resolve(process.cwd(), manifestPath);
|
|
1300
1449
|
const existingManifest = existsSync(manifestAbsolutePath) ? readFileSync(manifestAbsolutePath, "utf8") : undefined;
|
|
1301
|
-
const manifest = manifestContent(info, source, descriptor, existingManifest);
|
|
1450
|
+
const manifest = flags.writeState ? manifestContent(info, source, descriptor, existingManifest) : undefined;
|
|
1302
1451
|
const actions = [];
|
|
1303
1452
|
const artifacts = [];
|
|
1304
1453
|
const blockers = [];
|
|
@@ -1325,40 +1474,41 @@ function buildInitPlan(flags, descriptor, artifact) {
|
|
|
1325
1474
|
artifacts.push(...directoryPlan.artifacts);
|
|
1326
1475
|
blockers.push(...directoryPlan.blockers);
|
|
1327
1476
|
|
|
1328
|
-
if (
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1477
|
+
if (flags.writeState) {
|
|
1478
|
+
if (existingManifest !== undefined) {
|
|
1479
|
+
const parsedManifest = parseManifestAdapterBlocks(existingManifest);
|
|
1480
|
+
if (parsedManifest.error) {
|
|
1481
|
+
errors.push({
|
|
1482
|
+
code: parsedManifest.error.code,
|
|
1483
|
+
message: parsedManifest.error.message,
|
|
1484
|
+
path: manifestPath,
|
|
1485
|
+
next_action: "Review or move the existing file before running init.",
|
|
1486
|
+
});
|
|
1487
|
+
} else if (parsedManifest.adapters.filter((entry) => entry.name === descriptor.name).length > 1) {
|
|
1488
|
+
blockers.push({
|
|
1489
|
+
code: "duplicate-target-entry",
|
|
1490
|
+
message: `Existing rigorloop.yaml contains duplicate ${descriptor.displayName} target entries.`,
|
|
1491
|
+
path: manifestPath,
|
|
1492
|
+
next_action: "Remove duplicate target entries before running init.",
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1345
1495
|
actions.push({
|
|
1346
1496
|
type: "write",
|
|
1347
1497
|
path: manifestPath,
|
|
1348
|
-
status: flags.dryRun ? "planned" : "
|
|
1349
|
-
reason:
|
|
1498
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1499
|
+
reason: `Write target-oriented rigorloop.yaml for ${descriptor.displayName} support.`,
|
|
1350
1500
|
});
|
|
1351
1501
|
artifacts.push({
|
|
1352
1502
|
path: manifestPath,
|
|
1353
1503
|
kind: "project-manifest",
|
|
1354
|
-
status: "
|
|
1504
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1355
1505
|
});
|
|
1356
1506
|
} else {
|
|
1357
1507
|
actions.push({
|
|
1358
1508
|
type: "write",
|
|
1359
1509
|
path: manifestPath,
|
|
1360
1510
|
status: flags.dryRun ? "planned" : "pending",
|
|
1361
|
-
reason:
|
|
1511
|
+
reason: "Create target-oriented RigorLoop project manifest.",
|
|
1362
1512
|
});
|
|
1363
1513
|
artifacts.push({
|
|
1364
1514
|
path: manifestPath,
|
|
@@ -1366,21 +1516,9 @@ function buildInitPlan(flags, descriptor, artifact) {
|
|
|
1366
1516
|
status: flags.dryRun ? "planned" : "pending",
|
|
1367
1517
|
});
|
|
1368
1518
|
}
|
|
1369
|
-
} else {
|
|
1370
|
-
actions.push({
|
|
1371
|
-
type: "write",
|
|
1372
|
-
path: manifestPath,
|
|
1373
|
-
status: flags.dryRun ? "planned" : "pending",
|
|
1374
|
-
reason: "Create first-slice RigorLoop project manifest.",
|
|
1375
|
-
});
|
|
1376
|
-
artifacts.push({
|
|
1377
|
-
path: manifestPath,
|
|
1378
|
-
kind: "project-manifest",
|
|
1379
|
-
status: flags.dryRun ? "planned" : "pending",
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
1519
|
|
|
1383
|
-
|
|
1520
|
+
addLockfilePlan(flags, actions, artifacts, blockers, errors);
|
|
1521
|
+
}
|
|
1384
1522
|
|
|
1385
1523
|
return {
|
|
1386
1524
|
info,
|
|
@@ -1390,7 +1528,7 @@ function buildInitPlan(flags, descriptor, artifact) {
|
|
|
1390
1528
|
artifacts,
|
|
1391
1529
|
blockers,
|
|
1392
1530
|
errors,
|
|
1393
|
-
planned_lockfile: plannedLockfile(info, source, manifest, descriptor),
|
|
1531
|
+
planned_lockfile: flags.writeState ? plannedLockfile(info, source, manifest, descriptor) : undefined,
|
|
1394
1532
|
};
|
|
1395
1533
|
}
|
|
1396
1534
|
|
|
@@ -1483,11 +1621,11 @@ function invalidArchivePath(message, flags) {
|
|
|
1483
1621
|
function unsupportedAdapter(adapter, flags) {
|
|
1484
1622
|
const result = envelope("init", flags, {
|
|
1485
1623
|
status: "blocked",
|
|
1486
|
-
summary: `
|
|
1624
|
+
summary: `Target '${adapter}' is not supported.`,
|
|
1487
1625
|
blockers: [
|
|
1488
1626
|
{
|
|
1489
|
-
code: "
|
|
1490
|
-
message: `
|
|
1627
|
+
code: "target-unknown",
|
|
1628
|
+
message: `Target '${adapter}' is not supported.`,
|
|
1491
1629
|
next_action: `Use one of: ${supportedAdapterNames().join(", ")}.`,
|
|
1492
1630
|
},
|
|
1493
1631
|
],
|
|
@@ -1501,6 +1639,27 @@ function unsupportedAdapter(adapter, flags) {
|
|
|
1501
1639
|
return exitCodeForResult({ ...result, exit_class: "blocked" });
|
|
1502
1640
|
}
|
|
1503
1641
|
|
|
1642
|
+
function removedAdapterSyntax(flags) {
|
|
1643
|
+
const targets = supportedAdapterNames();
|
|
1644
|
+
const result = envelope("init", flags, {
|
|
1645
|
+
status: "error",
|
|
1646
|
+
summary: "`init --adapter` was removed in RigorLoop 0.3.0.",
|
|
1647
|
+
errors: [
|
|
1648
|
+
{
|
|
1649
|
+
code: "adapter-option-removed",
|
|
1650
|
+
message: "`init --adapter` was removed in RigorLoop 0.3.0.",
|
|
1651
|
+
next_action: `Use target-native init: ${targets.map((target) => `rigorloop init ${target}`).join(", ")}.`,
|
|
1652
|
+
},
|
|
1653
|
+
],
|
|
1654
|
+
});
|
|
1655
|
+
if (flags.json) {
|
|
1656
|
+
writeJson(result);
|
|
1657
|
+
} else {
|
|
1658
|
+
process.stderr.write(`${result.summary}\n${result.errors[0].next_action}\n`);
|
|
1659
|
+
}
|
|
1660
|
+
return exitCodeForResult({ ...result, exit_class: "invalid_usage" });
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1504
1663
|
function writeBlockedResult(flags, plan, summary, blockers, exitClass = "blocked") {
|
|
1505
1664
|
for (const action of plan.actions) {
|
|
1506
1665
|
if (action.status === "pending") {
|
|
@@ -1513,17 +1672,22 @@ function writeBlockedResult(flags, plan, summary, blockers, exitClass = "blocked
|
|
|
1513
1672
|
artifact.status = "blocked";
|
|
1514
1673
|
}
|
|
1515
1674
|
}
|
|
1675
|
+
const statePlan = flags.writeState
|
|
1676
|
+
? {
|
|
1677
|
+
planned_manifest: {
|
|
1678
|
+
path: "rigorloop.yaml",
|
|
1679
|
+
content: plan.manifest,
|
|
1680
|
+
},
|
|
1681
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1682
|
+
}
|
|
1683
|
+
: {};
|
|
1516
1684
|
const result = envelope("init", flags, {
|
|
1517
1685
|
status: "blocked",
|
|
1518
1686
|
summary,
|
|
1519
1687
|
actions: plan.actions,
|
|
1520
1688
|
artifacts: plan.artifacts,
|
|
1521
1689
|
blockers,
|
|
1522
|
-
|
|
1523
|
-
path: "rigorloop.yaml",
|
|
1524
|
-
content: plan.manifest,
|
|
1525
|
-
},
|
|
1526
|
-
planned_lockfile: plan.planned_lockfile,
|
|
1690
|
+
...statePlan,
|
|
1527
1691
|
});
|
|
1528
1692
|
if (blockers[0]?.diagnostics) {
|
|
1529
1693
|
result.diagnostics = { ...result.diagnostics, ...blockers[0].diagnostics };
|
|
@@ -1552,17 +1716,22 @@ function writeValidationErrorResult(flags, plan, error) {
|
|
|
1552
1716
|
artifact.status = "blocked";
|
|
1553
1717
|
}
|
|
1554
1718
|
}
|
|
1719
|
+
const statePlan = flags.writeState
|
|
1720
|
+
? {
|
|
1721
|
+
planned_manifest: {
|
|
1722
|
+
path: "rigorloop.yaml",
|
|
1723
|
+
content: plan.manifest,
|
|
1724
|
+
},
|
|
1725
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1726
|
+
}
|
|
1727
|
+
: {};
|
|
1555
1728
|
const result = envelope("init", flags, {
|
|
1556
1729
|
status: "error",
|
|
1557
1730
|
summary: error.message,
|
|
1558
1731
|
actions: plan.actions,
|
|
1559
1732
|
artifacts: plan.artifacts,
|
|
1560
1733
|
errors: [error],
|
|
1561
|
-
|
|
1562
|
-
path: "rigorloop.yaml",
|
|
1563
|
-
content: plan.manifest,
|
|
1564
|
-
},
|
|
1565
|
-
planned_lockfile: plan.planned_lockfile,
|
|
1734
|
+
...statePlan,
|
|
1566
1735
|
});
|
|
1567
1736
|
if (flags.json) {
|
|
1568
1737
|
writeJson(result);
|
|
@@ -1661,13 +1830,17 @@ async function archiveWorkForInit(flags, info, descriptor) {
|
|
|
1661
1830
|
return { artifact, entries: inspected.entries, archiveHash: inspected.archiveHash, treeHash: inspected.treeHash };
|
|
1662
1831
|
}
|
|
1663
1832
|
|
|
1664
|
-
async function handleInit(flags) {
|
|
1665
|
-
if (
|
|
1666
|
-
return
|
|
1833
|
+
async function handleInit(flags, initArgs = []) {
|
|
1834
|
+
if (flags.adapterOptionUsed) {
|
|
1835
|
+
return removedAdapterSyntax(flags);
|
|
1836
|
+
}
|
|
1837
|
+
if (initArgs.length !== 1) {
|
|
1838
|
+
return invalidUsage(`init requires exactly one target: ${supportedAdapterNames().join(", ")}.`, flags, "init");
|
|
1667
1839
|
}
|
|
1668
|
-
const
|
|
1840
|
+
const target = initArgs[0];
|
|
1841
|
+
const descriptor = adapterDescriptor(target);
|
|
1669
1842
|
if (!descriptor) {
|
|
1670
|
-
return unsupportedAdapter(
|
|
1843
|
+
return unsupportedAdapter(target, flags);
|
|
1671
1844
|
}
|
|
1672
1845
|
if (flags.fromArchiveProvided && (!flags.fromArchive || flags.fromArchive.startsWith("--"))) {
|
|
1673
1846
|
return invalidArchivePath("Missing required value for --from-archive.", flags);
|
|
@@ -1685,11 +1858,15 @@ async function handleInit(flags) {
|
|
|
1685
1858
|
actions: plan.actions,
|
|
1686
1859
|
artifacts: plan.artifacts,
|
|
1687
1860
|
errors: plan.errors,
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1861
|
+
...(flags.writeState
|
|
1862
|
+
? {
|
|
1863
|
+
planned_manifest: {
|
|
1864
|
+
path: "rigorloop.yaml",
|
|
1865
|
+
content: plan.manifest,
|
|
1866
|
+
},
|
|
1867
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1868
|
+
}
|
|
1869
|
+
: {}),
|
|
1693
1870
|
});
|
|
1694
1871
|
if (flags.json) {
|
|
1695
1872
|
writeJson(result);
|
|
@@ -1705,6 +1882,12 @@ async function handleInit(flags) {
|
|
|
1705
1882
|
if (plan.blockers.length > 0 && !deferrableRootBlockers) {
|
|
1706
1883
|
return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
|
|
1707
1884
|
}
|
|
1885
|
+
if (flags.dryRun) {
|
|
1886
|
+
const stateSafety = existingStateSafetyBlocker(descriptor);
|
|
1887
|
+
if (stateSafety) {
|
|
1888
|
+
return writeBlockedResult(flags, plan, stateSafety.message, [stateSafety], exitClassForBlockers([stateSafety]));
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1708
1891
|
|
|
1709
1892
|
const archiveWork = await archiveWorkForInit(flags, info, descriptor);
|
|
1710
1893
|
if (archiveWork.artifact && !archiveWork.blocker && !archiveWork.error) {
|
|
@@ -1721,7 +1904,11 @@ async function handleInit(flags) {
|
|
|
1721
1904
|
if (conflict) {
|
|
1722
1905
|
return writeBlockedResult(flags, plan, conflict.message, [conflict], "mutation_conflict");
|
|
1723
1906
|
}
|
|
1724
|
-
const
|
|
1907
|
+
const stateSafety = existingStateSafetyBlocker(descriptor, archiveWork.artifact);
|
|
1908
|
+
if (stateSafety) {
|
|
1909
|
+
return writeBlockedResult(flags, plan, stateSafety.message, [stateSafety], exitClassForBlockers([stateSafety]));
|
|
1910
|
+
}
|
|
1911
|
+
const drift = flags.writeState ? firstLockfileDriftBlocker() : undefined;
|
|
1725
1912
|
if (drift) {
|
|
1726
1913
|
return writeBlockedResult(
|
|
1727
1914
|
flags,
|
|
@@ -1749,7 +1936,7 @@ async function handleInit(flags) {
|
|
|
1749
1936
|
}
|
|
1750
1937
|
|
|
1751
1938
|
if (!flags.dryRun) {
|
|
1752
|
-
const manifestAction = plan.actions.find((action) => action.path === "rigorloop.yaml");
|
|
1939
|
+
const manifestAction = flags.writeState ? plan.actions.find((action) => action.path === "rigorloop.yaml") : undefined;
|
|
1753
1940
|
const directoryActions = plan.actions.filter((action) => action.type === "create-dir" && action.status === "pending");
|
|
1754
1941
|
for (const directoryAction of directoryActions) {
|
|
1755
1942
|
mkdirSync(resolve(process.cwd(), directoryAction.path));
|
|
@@ -1784,11 +1971,15 @@ async function handleInit(flags) {
|
|
|
1784
1971
|
partial_state: "scaffold files may have been written; adapter files may be incomplete.",
|
|
1785
1972
|
},
|
|
1786
1973
|
],
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1974
|
+
...(flags.writeState
|
|
1975
|
+
? {
|
|
1976
|
+
planned_manifest: {
|
|
1977
|
+
path: "rigorloop.yaml",
|
|
1978
|
+
content: plan.manifest,
|
|
1979
|
+
},
|
|
1980
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1981
|
+
}
|
|
1982
|
+
: {}),
|
|
1792
1983
|
});
|
|
1793
1984
|
if (flags.json) {
|
|
1794
1985
|
writeJson(result);
|
|
@@ -1798,7 +1989,7 @@ async function handleInit(flags) {
|
|
|
1798
1989
|
return exitCodeForResult({ ...result, exit_class: "internal" });
|
|
1799
1990
|
}
|
|
1800
1991
|
}
|
|
1801
|
-
if (archiveWork.entries) {
|
|
1992
|
+
if (flags.writeState && archiveWork.entries) {
|
|
1802
1993
|
const lockfileAction = plan.actions.find((action) => action.path === LOCKFILE_PATH);
|
|
1803
1994
|
const lockfileArtifact = plan.artifacts.find((artifact) => artifact.path === LOCKFILE_PATH);
|
|
1804
1995
|
if (lockfileAction?.status === "pending") {
|
|
@@ -1834,16 +2025,23 @@ async function handleInit(flags) {
|
|
|
1834
2025
|
summary: flags.dryRun
|
|
1835
2026
|
? "RigorLoop init dry run completed. No files were written."
|
|
1836
2027
|
: archiveWork.entries
|
|
1837
|
-
|
|
2028
|
+
? `RigorLoop initialized with verified ${descriptor.displayName} target support.`
|
|
1838
2029
|
: `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
|
|
1839
2030
|
actions: plan.actions,
|
|
1840
2031
|
artifacts: plan.artifacts,
|
|
1841
2032
|
warnings,
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2033
|
+
state_files: flags.writeState
|
|
2034
|
+
? { action: flags.dryRun ? "planned" : "written" }
|
|
2035
|
+
: { action: "skipped", reason: "Use --write-state to write rigorloop.yaml and rigorloop.lock." },
|
|
2036
|
+
...(flags.writeState
|
|
2037
|
+
? {
|
|
2038
|
+
planned_manifest: {
|
|
2039
|
+
path: "rigorloop.yaml",
|
|
2040
|
+
content: plan.manifest,
|
|
2041
|
+
},
|
|
2042
|
+
planned_lockfile: plan.planned_lockfile,
|
|
2043
|
+
}
|
|
2044
|
+
: {}),
|
|
1847
2045
|
});
|
|
1848
2046
|
|
|
1849
2047
|
if (flags.json) {
|
|
@@ -1853,9 +2051,9 @@ async function handleInit(flags) {
|
|
|
1853
2051
|
? ["RigorLoop init dry run completed.", "No files were written."]
|
|
1854
2052
|
: [
|
|
1855
2053
|
archiveWork.entries
|
|
1856
|
-
? `RigorLoop initialized with verified ${descriptor.displayName}
|
|
2054
|
+
? `RigorLoop initialized with verified ${descriptor.displayName} target support.`
|
|
1857
2055
|
: `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
|
|
1858
|
-
|
|
2056
|
+
flags.writeState ? "rigorloop.yaml and rigorloop.lock were written." : "State files were not written; use --write-state to write them.",
|
|
1859
2057
|
];
|
|
1860
2058
|
for (const warning of warnings) {
|
|
1861
2059
|
lines.push(`warning ${warning.code}: ${warning.message}`);
|
|
@@ -1882,7 +2080,7 @@ async function main() {
|
|
|
1882
2080
|
return handleVersion(flags);
|
|
1883
2081
|
}
|
|
1884
2082
|
if (command === "init") {
|
|
1885
|
-
return handleInit(flags);
|
|
2083
|
+
return handleInit(flags, positional.slice(1));
|
|
1886
2084
|
}
|
|
1887
2085
|
if (command === "new-change") {
|
|
1888
2086
|
return handleNewChange(rawArgs.slice(rawArgs.indexOf("new-change") + 1));
|
package/dist/lib/lockfile.js
CHANGED
|
@@ -6,7 +6,7 @@ const SUPPORTED_ADAPTERS = new Set(["codex", "claude", "opencode"]);
|
|
|
6
6
|
const TOP_LEVEL_FIELDS = ["schema_version", "rigorloop", "manifest", "generated"];
|
|
7
7
|
const RIGORLOOP_FIELDS = ["package", "version"];
|
|
8
8
|
const MANIFEST_FIELDS = ["path", "sha256"];
|
|
9
|
-
const GENERATED_FIELDS = ["adapters"];
|
|
9
|
+
const GENERATED_FIELDS = ["adapters", "targets"];
|
|
10
10
|
const ADAPTER_FIELDS = [
|
|
11
11
|
"adapter",
|
|
12
12
|
"release",
|
|
@@ -227,6 +227,25 @@ function parseAdapters(lines) {
|
|
|
227
227
|
return { adapters };
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
function parseTargets(lines) {
|
|
231
|
+
const transformed = lines.map((line) => {
|
|
232
|
+
if (line === " targets:") {
|
|
233
|
+
return " adapters:";
|
|
234
|
+
}
|
|
235
|
+
return line.replace(/^ - target:/, " - adapter:");
|
|
236
|
+
});
|
|
237
|
+
const parsed = parseAdapters(transformed);
|
|
238
|
+
if (parsed.ok === false || parsed.missing) {
|
|
239
|
+
return parsed;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
targets: parsed.adapters.map((adapter) => {
|
|
243
|
+
const { adapter: target, ...rest } = adapter;
|
|
244
|
+
return { target, ...rest };
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
230
249
|
function unexpectedFields(adapter, allowed) {
|
|
231
250
|
return Object.keys(adapter).filter((field) => !allowed.includes(field));
|
|
232
251
|
}
|
|
@@ -337,6 +356,18 @@ function validateAdapter(adapter, schemaVersion) {
|
|
|
337
356
|
return validateSingleRootAdapter(adapter, schemaVersion);
|
|
338
357
|
}
|
|
339
358
|
|
|
359
|
+
function validateTarget(target) {
|
|
360
|
+
const adapter = { ...target, adapter: target.target };
|
|
361
|
+
delete adapter.target;
|
|
362
|
+
const error = validateAdapter(adapter, 2);
|
|
363
|
+
if (error) {
|
|
364
|
+
return error;
|
|
365
|
+
}
|
|
366
|
+
const { adapter: normalizedTarget, ...rest } = adapter;
|
|
367
|
+
Object.assign(target, { target: normalizedTarget, ...rest });
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
340
371
|
export function parseLockfile(text) {
|
|
341
372
|
if (typeof text !== "string" || !text.trim()) {
|
|
342
373
|
return failure("invalid", "invalid-lockfile", "rigorloop.lock is empty or not text.");
|
|
@@ -359,7 +390,7 @@ export function parseLockfile(text) {
|
|
|
359
390
|
}
|
|
360
391
|
const schemaVersion = parseScalar(top.get("schema_version"));
|
|
361
392
|
const parsedSchemaVersion = Number.parseInt(schemaVersion, 10);
|
|
362
|
-
if (!/^\d+$/.test(schemaVersion) || ![1, 2].includes(parsedSchemaVersion)) {
|
|
393
|
+
if (!/^\d+$/.test(schemaVersion) || ![1, 2, 3].includes(parsedSchemaVersion)) {
|
|
363
394
|
return failure("unsupported", "unsupported-lockfile-shape", "Unsupported lockfile schema_version.");
|
|
364
395
|
}
|
|
365
396
|
|
|
@@ -385,6 +416,33 @@ export function parseLockfile(text) {
|
|
|
385
416
|
return failure("invalid", "invalid-lockfile", "Invalid manifest lockfile entry.");
|
|
386
417
|
}
|
|
387
418
|
|
|
419
|
+
if (parsedSchemaVersion === 3) {
|
|
420
|
+
const targetResult = parseTargets(lines);
|
|
421
|
+
if (targetResult.ok === false) {
|
|
422
|
+
return targetResult;
|
|
423
|
+
}
|
|
424
|
+
if (targetResult.missing || !targetResult.targets.length) {
|
|
425
|
+
return failure("invalid", "invalid-lockfile", "generated.targets must contain at least one target entry.");
|
|
426
|
+
}
|
|
427
|
+
for (const target of targetResult.targets) {
|
|
428
|
+
const targetError = validateTarget(target);
|
|
429
|
+
if (targetError) {
|
|
430
|
+
return targetError;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
ok: true,
|
|
435
|
+
lockfile: {
|
|
436
|
+
schema_version: parsedSchemaVersion,
|
|
437
|
+
rigorloop: rigorloop.fields,
|
|
438
|
+
manifest: manifest.fields,
|
|
439
|
+
generated: {
|
|
440
|
+
targets: targetResult.targets,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
388
446
|
const adapterResult = parseAdapters(lines);
|
|
389
447
|
if (adapterResult.ok === false) {
|
|
390
448
|
return adapterResult;
|
|
@@ -413,7 +471,17 @@ export function parseLockfile(text) {
|
|
|
413
471
|
}
|
|
414
472
|
|
|
415
473
|
export function serializeLockfile(lockfile) {
|
|
416
|
-
const
|
|
474
|
+
const isTargetSchema = lockfile.schema_version === 3;
|
|
475
|
+
const sourceEntries = lockfile.generated.targets ?? lockfile.generated.adapters;
|
|
476
|
+
const entries = [...sourceEntries].map((entry) => {
|
|
477
|
+
if (isTargetSchema || entry.adapter) {
|
|
478
|
+
return entry;
|
|
479
|
+
}
|
|
480
|
+
const { target, ...rest } = entry;
|
|
481
|
+
return { adapter: target, ...rest };
|
|
482
|
+
}).sort((left, right) =>
|
|
483
|
+
(left.target ?? left.adapter).localeCompare(right.target ?? right.adapter),
|
|
484
|
+
);
|
|
417
485
|
const lines = [
|
|
418
486
|
`schema_version: ${lockfile.schema_version ?? 2}`,
|
|
419
487
|
"",
|
|
@@ -426,11 +494,11 @@ export function serializeLockfile(lockfile) {
|
|
|
426
494
|
` sha256: "${lockfile.manifest.sha256}"`,
|
|
427
495
|
"",
|
|
428
496
|
"generated:",
|
|
429
|
-
" adapters:",
|
|
497
|
+
isTargetSchema ? " targets:" : " adapters:",
|
|
430
498
|
];
|
|
431
|
-
for (const adapter of
|
|
499
|
+
for (const adapter of entries) {
|
|
432
500
|
lines.push(
|
|
433
|
-
` - adapter: ${adapter.adapter}`,
|
|
501
|
+
isTargetSchema ? ` - target: ${adapter.target}` : ` - adapter: ${adapter.adapter}`,
|
|
434
502
|
` release: "${adapter.release}"`,
|
|
435
503
|
` source: ${adapter.source}`,
|
|
436
504
|
` archive: "${adapter.archive}"`,
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"size_bytes": 105317,
|
|
21
21
|
"install_root": ".agents/skills",
|
|
22
22
|
"tree_hash_algorithm": "rigorloop-tree-hash-v1",
|
|
23
|
-
"tree_sha256": "
|
|
23
|
+
"tree_sha256": "6c01d71d2a0e4cd3b276092728f52de23345095b25ce2135b1a0759afd25c12e",
|
|
24
|
+
"file_count": 38
|
|
24
25
|
}
|
|
25
26
|
],
|
|
26
27
|
"validation": {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"release": {
|
|
4
|
+
"version": "v0.3.0",
|
|
5
|
+
"source_repository": "xiongxianfei/rigorloop",
|
|
6
|
+
"source_commit": "02a9d7d6d514fc99908abf32898494dbbbae00c9",
|
|
7
|
+
"release_tag": "v0.3.0",
|
|
8
|
+
"published_at": "2026-05-24"
|
|
9
|
+
},
|
|
10
|
+
"metadata": {
|
|
11
|
+
"url": "https://github.com/xiongxianfei/rigorloop/releases/download/v0.3.0/adapter-artifacts-v0.3.0.json",
|
|
12
|
+
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
|
|
13
|
+
},
|
|
14
|
+
"artifacts": [
|
|
15
|
+
{
|
|
16
|
+
"adapter": "codex",
|
|
17
|
+
"archive": "rigorloop-adapter-codex-v0.3.0.zip",
|
|
18
|
+
"url": "https://github.com/xiongxianfei/rigorloop/releases/download/v0.3.0/rigorloop-adapter-codex-v0.3.0.zip",
|
|
19
|
+
"sha256": "ee5c35aa5ffaa9007aea2f2687b82c72135a42489edf56fe6427b37c55159550",
|
|
20
|
+
"size_bytes": 105318,
|
|
21
|
+
"install_root": ".agents/skills",
|
|
22
|
+
"tree_hash_algorithm": "rigorloop-tree-hash-v1",
|
|
23
|
+
"tree_sha256": "6c01d71d2a0e4cd3b276092728f52de23345095b25ce2135b1a0759afd25c12e",
|
|
24
|
+
"file_count": 38
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"adapter": "claude",
|
|
28
|
+
"archive": "rigorloop-adapter-claude-v0.3.0.zip",
|
|
29
|
+
"url": "https://github.com/xiongxianfei/rigorloop/releases/download/v0.3.0/rigorloop-adapter-claude-v0.3.0.zip",
|
|
30
|
+
"sha256": "91e0dd0d1573b7b3cfa5dfefb54a98b85dfe66d042889ecb8ba691f5c7bdc9e3",
|
|
31
|
+
"size_bytes": 104643,
|
|
32
|
+
"install_root": ".claude/skills",
|
|
33
|
+
"tree_hash_algorithm": "rigorloop-tree-hash-v1",
|
|
34
|
+
"tree_sha256": "8d1378e2b66be8502ef07a0013a60b19276b500c719056824a50335e4d1d3d1b",
|
|
35
|
+
"file_count": 38
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"adapter": "opencode",
|
|
39
|
+
"archive": "rigorloop-adapter-opencode-v0.3.0.zip",
|
|
40
|
+
"url": "https://github.com/xiongxianfei/rigorloop/releases/download/v0.3.0/rigorloop-adapter-opencode-v0.3.0.zip",
|
|
41
|
+
"sha256": "89f0b8f58681dc4563da6e4918ea039a2ed0dcca1dc84caabaf9836ea32fa0db",
|
|
42
|
+
"size_bytes": 104887,
|
|
43
|
+
"install_root": ".opencode/skills",
|
|
44
|
+
"tree_hash_algorithm": "rigorloop-tree-hash-v1",
|
|
45
|
+
"tree_sha256": "8d1378e2b66be8502ef07a0013a60b19276b500c719056824a50335e4d1d3d1b",
|
|
46
|
+
"file_count": 38,
|
|
47
|
+
"skills_only_compatibility": {
|
|
48
|
+
"releases": [
|
|
49
|
+
"v0.3.0"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"compatibility": {
|
|
55
|
+
"opencode_skills_only": {
|
|
56
|
+
"releases": [
|
|
57
|
+
"v0.3.0"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"validation": {
|
|
62
|
+
"command": "python scripts/validate-adapters.py --root <release-output-dir> --version v0.3.0",
|
|
63
|
+
"result": "pass"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"releases": {
|
|
4
|
-
"v0.
|
|
4
|
+
"v0.3.0": {
|
|
5
5
|
"source_repository": "xiongxianfei/rigorloop",
|
|
6
|
-
"release_tag": "v0.
|
|
7
|
-
"bundled_metadata": "adapter-artifacts-v0.
|
|
8
|
-
"bundled_metadata_sha256": "
|
|
6
|
+
"release_tag": "v0.3.0",
|
|
7
|
+
"bundled_metadata": "adapter-artifacts-v0.3.0.json",
|
|
8
|
+
"bundled_metadata_sha256": "510060d820851b98ec864d7727c3acb730e03afe8212bdfdd9ecead07100563d"
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiongxianfei/rigorloop",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Git-first workflow for AI coding agents: proposals, specs, tests, review gates, and durable validation evidence from idea to PR.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agents",
|
|
7
|
+
"ai-coding",
|
|
8
|
+
"coding-agent",
|
|
9
|
+
"agentic-workflow",
|
|
10
|
+
"llm",
|
|
11
|
+
"developer-tools",
|
|
12
|
+
"software-engineering",
|
|
13
|
+
"code-review",
|
|
14
|
+
"git-workflow",
|
|
15
|
+
"cli",
|
|
16
|
+
"npm-package",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"codex",
|
|
19
|
+
"opencode",
|
|
20
|
+
"workflow",
|
|
21
|
+
"testing",
|
|
22
|
+
"validation",
|
|
23
|
+
"pull-requests"
|
|
24
|
+
],
|
|
5
25
|
"repository": {
|
|
6
26
|
"type": "git",
|
|
7
27
|
"url": "https://github.com/xiongxianfei/rigorloop"
|