@xiongxianfei/rigorloop 0.2.0 → 0.3.1

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 adapter initialization and change metadata scaffolding. Adapter archives remain verified GitHub release artifacts; they are not bundled into the npm package.
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 --adapter codex
15
- npx @xiongxianfei/rigorloop@latest init --adapter claude
16
- npx @xiongxianfei/rigorloop@latest init --adapter opencode
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.1.5 init --adapter codex
26
+ npx @xiongxianfei/rigorloop@0.3.1 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 --adapter codex
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 --adapter codex
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 --adapter codex|claude|opencode [--from-archive <path>] [--dry-run] [--json]
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
- ## Adapter Install
54
+ ## Target Init
51
55
 
52
- Initialize an adapter from the verified official release archive:
56
+ Initialize target support from the verified official release archive:
53
57
 
54
58
  ```bash
55
- npx @xiongxianfei/rigorloop@0.1.5 init --adapter codex --json
56
- npx @xiongxianfei/rigorloop@0.1.5 init --adapter claude --json
57
- npx @xiongxianfei/rigorloop@0.1.5 init --adapter opencode --json
59
+ npx @xiongxianfei/rigorloop@0.3.1 init codex --json
60
+ npx @xiongxianfei/rigorloop@0.3.1 init claude --json
61
+ npx @xiongxianfei/rigorloop@0.3.1 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.1.5 init --adapter opencode --dry-run --json
67
+ npx @xiongxianfei/rigorloop@0.3.1 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.1.5 init --adapter codex --from-archive ./rigorloop-adapter-codex-v0.1.5.zip --json
70
- npx @xiongxianfei/rigorloop@0.1.5 init --adapter claude --from-archive ./rigorloop-adapter-claude-v0.1.5.zip --json
71
- npx @xiongxianfei/rigorloop@0.1.5 init --adapter opencode --from-archive ./rigorloop-adapter-opencode-v0.1.5.zip --json
73
+ npx @xiongxianfei/rigorloop@0.3.1 init codex --from-archive ./rigorloop-adapter-codex-v0.3.1.zip --json
74
+ npx @xiongxianfei/rigorloop@0.3.1 init claude --from-archive ./rigorloop-adapter-claude-v0.3.1.zip --json
75
+ npx @xiongxianfei/rigorloop@0.3.1 init opencode --from-archive ./rigorloop-adapter-opencode-v0.3.1.zip --json
72
76
  ```
73
77
 
74
- The install command writes repository-local RigorLoop files, verifies the selected archive before extraction, and verifies the installed tree before reporting success. Runtime roots are adapter-specific:
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 adapter 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.
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.1.5 new-change my-change --title "Describe the change" --json
93
+ npx @xiongxianfei/rigorloop@0.3.1 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.1.5 new-change my-change --title "Describe the change" --dry-run --json
99
+ npx @xiongxianfei/rigorloop@0.3.1 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.1.5` for CI, onboarding docs, and repeatable agent setup.
106
+ Use `@latest` for manual exploration. Use an explicit version such as `@0.3.1` for CI, onboarding docs, and repeatable agent setup.
103
107
 
104
108
  ## Source of Truth
105
109
 
@@ -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 --adapter codex|claude|opencode [--dry-run] [--json]
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 --adapter codex|claude|opencode
119
- Initialize a verified adapter install plan.
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 manifestAdapterBlock(source, descriptor, artifact) {
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 ` - name: ${descriptor.name}
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
- if (!content.includes("schema_version: 1") || !content.includes("adapters:")) {
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 adapterStart = lines.findIndex((line) => line === "adapters:");
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(" - name: ")) {
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(" - name: ".length).trim();
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
- adapters.push({ name, block: block.join("\n").replace(/\n+$/, "") });
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 manifestContent(info, source, descriptor, existingContent) {
200
- const selectedBlock = manifestAdapterBlock(source, descriptor, source.artifact);
201
- if (existingContent) {
202
- const parsed = parseManifestAdapterBlocks(existingContent);
203
- if (!parsed.error) {
204
- const preserved = parsed.adapters.filter((entry) => entry.name !== descriptor.name).map((entry) => entry.block);
205
- return `schema_version: 1
206
- rigorloop:
207
- package: "${info.name}"
208
- package_version: "${info.version}"
209
- adapters:
210
- ${[...preserved, selectedBlock].join("\n")}
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 `schema_version: 1
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
- adapters:
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
- adapter: descriptor.name,
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: 2,
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
- adapters: [lockfileEntryForAdapter(info, source, artifact, descriptor)],
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
- return parsed.lockfile.generated.adapters.filter((entry) => entry.adapter !== selectedAdapter);
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: 2,
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
- adapter: lockfileEntry.adapter,
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
- adapter: lockfileEntry.adapter,
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 Codex adapter install.",
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 Codex adapter install.",
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 (existingManifest !== undefined) {
1329
- const parsedManifest = parseManifestAdapterBlocks(existingManifest);
1330
- if (parsedManifest.error) {
1331
- errors.push({
1332
- code: parsedManifest.error.code,
1333
- message: parsedManifest.error.message,
1334
- path: manifestPath,
1335
- next_action: "Review or move the existing file before running init.",
1336
- });
1337
- } else if (parsedManifest.adapters.filter((entry) => entry.name === descriptor.name).length > 1) {
1338
- blockers.push({
1339
- code: "duplicate-adapter-entry",
1340
- message: `Existing rigorloop.yaml contains duplicate ${descriptor.displayName} adapter entries.`,
1341
- path: manifestPath,
1342
- next_action: "Remove duplicate adapter entries before running init.",
1343
- });
1344
- } else if (compatibleManifest(existingManifest, descriptor, artifact)) {
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" : "skipped",
1349
- reason: "Compatible rigorloop.yaml already exists.",
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: "existing",
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: `Update rigorloop.yaml for ${descriptor.displayName} adapter.`,
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
- addLockfilePlan(flags, actions, artifacts, blockers, errors);
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: `Adapter '${adapter}' is not supported.`,
1624
+ summary: `Target '${adapter}' is not supported.`,
1487
1625
  blockers: [
1488
1626
  {
1489
- code: "adapter-unknown",
1490
- message: `Adapter '${adapter}' is not supported.`,
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
- planned_manifest: {
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
- planned_manifest: {
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 (!flags.adapter) {
1666
- return invalidUsage("Missing required option: --adapter codex|claude|opencode.", flags, "init");
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 descriptor = adapterDescriptor(flags.adapter);
1840
+ const target = initArgs[0];
1841
+ const descriptor = adapterDescriptor(target);
1669
1842
  if (!descriptor) {
1670
- return unsupportedAdapter(flags.adapter, flags);
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
- planned_manifest: {
1689
- path: "rigorloop.yaml",
1690
- content: plan.manifest,
1691
- },
1692
- planned_lockfile: plan.planned_lockfile,
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 drift = firstLockfileDriftBlocker();
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
- planned_manifest: {
1788
- path: "rigorloop.yaml",
1789
- content: plan.manifest,
1790
- },
1791
- planned_lockfile: plan.planned_lockfile,
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
- ? `RigorLoop initialized with verified ${descriptor.displayName} adapter files.`
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
- planned_manifest: {
1843
- path: "rigorloop.yaml",
1844
- content: plan.manifest,
1845
- },
1846
- planned_lockfile: plan.planned_lockfile,
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} adapter files.`
2054
+ ? `RigorLoop initialized with verified ${descriptor.displayName} target support.`
1857
2055
  : `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
1858
- archiveWork.entries ? "rigorloop.lock was written." : "No adapter files were installed.",
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));
@@ -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 adapters = [...lockfile.generated.adapters].sort((left, right) => left.adapter.localeCompare(right.adapter));
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 adapters) {
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": "698be6488b06534ed927278f7d0671fd9ee7ead8f4e20d1df838894b45d139bf"
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
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "schema_version": 1,
3
+ "release": {
4
+ "version": "v0.3.1",
5
+ "source_repository": "xiongxianfei/rigorloop",
6
+ "source_commit": "ffe9a6d0622b8cd97fee535bae245250f93cfc8c",
7
+ "release_tag": "v0.3.1",
8
+ "published_at": "2026-05-25"
9
+ },
10
+ "metadata": {
11
+ "url": "https://github.com/xiongxianfei/rigorloop/releases/download/v0.3.1/adapter-artifacts-v0.3.1.json",
12
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000"
13
+ },
14
+ "artifacts": [
15
+ {
16
+ "adapter": "codex",
17
+ "archive": "rigorloop-adapter-codex-v0.3.1.zip",
18
+ "url": "https://github.com/xiongxianfei/rigorloop/releases/download/v0.3.1/rigorloop-adapter-codex-v0.3.1.zip",
19
+ "sha256": "29be9c9d3a958f11167739f76abc08c1a049c9ea166c67ee9988b8b6a606939e",
20
+ "size_bytes": 106069,
21
+ "install_root": ".agents/skills",
22
+ "tree_hash_algorithm": "rigorloop-tree-hash-v1",
23
+ "tree_sha256": "d7ad8380f749131a3202686fab3efa3ee701eb68fa36a1a03c4c16163867a55e",
24
+ "file_count": 38
25
+ },
26
+ {
27
+ "adapter": "claude",
28
+ "archive": "rigorloop-adapter-claude-v0.3.1.zip",
29
+ "url": "https://github.com/xiongxianfei/rigorloop/releases/download/v0.3.1/rigorloop-adapter-claude-v0.3.1.zip",
30
+ "sha256": "eed95a4741d58891f11567e678e39642d77bd4f1544d35cabacf6469a37b4b35",
31
+ "size_bytes": 105396,
32
+ "install_root": ".claude/skills",
33
+ "tree_hash_algorithm": "rigorloop-tree-hash-v1",
34
+ "tree_sha256": "5e267126b3caca13c7590ba77365d5212b7d5e671a041aec7a7eaa7a9868061d",
35
+ "file_count": 38
36
+ },
37
+ {
38
+ "adapter": "opencode",
39
+ "archive": "rigorloop-adapter-opencode-v0.3.1.zip",
40
+ "url": "https://github.com/xiongxianfei/rigorloop/releases/download/v0.3.1/rigorloop-adapter-opencode-v0.3.1.zip",
41
+ "sha256": "bd07a33632f6f9355ec5e9902004a55a4bcda67b7740189df50be924c0a4c7e3",
42
+ "size_bytes": 105639,
43
+ "install_root": ".opencode/skills",
44
+ "tree_hash_algorithm": "rigorloop-tree-hash-v1",
45
+ "tree_sha256": "5e267126b3caca13c7590ba77365d5212b7d5e671a041aec7a7eaa7a9868061d",
46
+ "file_count": 38,
47
+ "skills_only_compatibility": {
48
+ "releases": [
49
+ "v0.3.1"
50
+ ]
51
+ }
52
+ }
53
+ ],
54
+ "compatibility": {
55
+ "opencode_skills_only": {
56
+ "releases": [
57
+ "v0.3.1"
58
+ ]
59
+ }
60
+ },
61
+ "validation": {
62
+ "command": "python scripts/validate-adapters.py --root <release-output-dir> --version v0.3.1",
63
+ "result": "pass"
64
+ }
65
+ }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "releases": {
4
- "v0.2.0": {
4
+ "v0.3.1": {
5
5
  "source_repository": "xiongxianfei/rigorloop",
6
- "release_tag": "v0.2.0",
7
- "bundled_metadata": "adapter-artifacts-v0.2.0.json",
8
- "bundled_metadata_sha256": "686e41154de342f2852dd4745a5378d1440bc02a39255630cc009cc28f39fa06"
6
+ "release_tag": "v0.3.1",
7
+ "bundled_metadata": "adapter-artifacts-v0.3.1.json",
8
+ "bundled_metadata_sha256": "e2c9b9bac02ee0b506a2be5fe3a6e2888536a1799e5a21134026dda0dea282f8"
9
9
  }
10
10
  }
11
11
  }
package/package.json CHANGED
@@ -1,7 +1,27 @@
1
1
  {
2
2
  "name": "@xiongxianfei/rigorloop",
3
- "version": "0.2.0",
4
- "description": "RigorLoop CLI.",
3
+ "version": "0.3.1",
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"