@xiongxianfei/rigorloop 0.1.5 → 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.
@@ -7,16 +7,13 @@ import { basename, dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
9
  import { EXIT, exitCodeForResult } from "../lib/command-result.js";
10
+ import { adapterDescriptor, supportedAdapterNames } from "../lib/adapters.js";
10
11
  import { parseLockfile, serializeLockfile, sha256NormalizedText } from "../lib/lockfile.js";
11
12
  import { buildNewChangeDraft, parseNewChangeArgs } from "../lib/new-change.js";
12
13
  import { runNewChangePlan } from "../lib/new-change-filesystem.js";
13
14
  import { validateOfficialArchiveUrl } from "../lib/official-archive-url.js";
14
15
 
15
- const ADAPTER = "codex";
16
- const AGENTS_ROOT = ".agents";
17
- const INSTALL_ROOT = ".agents/skills";
18
16
  const LOCKFILE_PATH = "rigorloop.lock";
19
- const DIRECTORY_PLAN = [AGENTS_ROOT, INSTALL_ROOT];
20
17
 
21
18
  function packageInfo() {
22
19
  const here = dirname(fileURLToPath(import.meta.url));
@@ -39,6 +36,8 @@ function parseFlags(args) {
39
36
  noColor: Boolean(process.env.NO_COLOR),
40
37
  dryRun: false,
41
38
  adapter: undefined,
39
+ adapterOptionUsed: false,
40
+ writeState: false,
42
41
  fromArchiveProvided: false,
43
42
  fromArchive: undefined,
44
43
  force: false,
@@ -58,10 +57,13 @@ function parseFlags(args) {
58
57
  } else if (arg === "--dry-run") {
59
58
  flags.dryRun = true;
60
59
  } else if (arg === "--adapter") {
60
+ flags.adapterOptionUsed = true;
61
61
  if (args[index + 1] && !args[index + 1].startsWith("--")) {
62
62
  flags.adapter = args[index + 1];
63
63
  index += 1;
64
64
  }
65
+ } else if (arg === "--write-state") {
66
+ flags.writeState = true;
65
67
  } else if (arg === "--from-archive") {
66
68
  flags.fromArchiveProvided = true;
67
69
  if (args[index + 1] && !args[index + 1].startsWith("--")) {
@@ -113,12 +115,13 @@ function usage() {
113
115
  Usage:
114
116
  rigorloop --help
115
117
  rigorloop version
116
- rigorloop init --adapter codex [--dry-run] [--json]
118
+ rigorloop init codex|claude|opencode [--write-state] [--dry-run] [--json]
117
119
  rigorloop new-change <change-id> --title <title> [--dry-run] [--json]
118
120
 
119
121
  Commands:
120
122
  version Print package name and version.
121
- init --adapter codex Initialize the first-slice Codex adapter plan.
123
+ init codex|claude|opencode
124
+ Initialize verified target support.
122
125
  new-change Plan a change metadata scaffold.
123
126
  `;
124
127
  }
@@ -127,43 +130,183 @@ function releaseForPackage(version) {
127
130
  return `v${version}`;
128
131
  }
129
132
 
130
- function sourceForFlags(flags, info) {
133
+ function sourceForFlags(flags, info, descriptor) {
131
134
  if (flags.fromArchiveProvided) {
132
135
  return {
133
136
  type: "local-archive",
134
- archive: flags.fromArchive,
137
+ archive: basename(flags.fromArchive),
138
+ inputPath: flags.fromArchive,
135
139
  };
136
140
  }
137
141
 
138
142
  return {
139
143
  type: "release-archive",
140
144
  release: releaseForPackage(info.version),
141
- archive: `rigorloop-adapter-codex-${releaseForPackage(info.version)}.zip`,
145
+ archive: descriptor.archiveName(releaseForPackage(info.version)),
142
146
  };
143
147
  }
144
148
 
145
- function manifestContent(info, source) {
149
+ function manifestTargetBlock(source, descriptor, artifact) {
146
150
  const sourceLines =
147
151
  source.type === "local-archive"
148
152
  ? [` type: local-archive`, ` archive: "${source.archive}"`]
149
153
  : [` type: release-archive`, ` release: "${source.release}"`];
154
+ const installRoots = rootsForArtifact(descriptor, artifact);
155
+ const rootLines =
156
+ descriptor.name !== "opencode" && Object.keys(installRoots).length === 1
157
+ ? [` install_root: "${Object.values(installRoots)[0]}"`]
158
+ : [
159
+ ` install_roots:`,
160
+ ...Object.entries(installRoots).map(([role, root]) => ` ${role}: "${root}"`),
161
+ ];
150
162
 
151
- return `schema_version: 1
163
+ return ` - target: ${descriptor.name}
164
+ ${rootLines.join("\n")}
165
+ source:
166
+ ${sourceLines.join("\n")}`;
167
+ }
168
+
169
+ function parseManifestAdapterBlocks(content) {
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) {
173
+ return { error: { code: "invalid-config", message: "Existing rigorloop.yaml is not compatible with the init contract." } };
174
+ }
175
+ const lines = content.replace(/\r\n?/g, "\n").split("\n");
176
+ const listKey = isTargetSchema ? "targets:" : "adapters:";
177
+ const entryPrefix = isTargetSchema ? " - target: " : " - name: ";
178
+ const adapterStart = lines.findIndex((line) => line === listKey);
179
+ const blocks = [];
180
+ let current = [];
181
+ for (const line of lines.slice(adapterStart + 1)) {
182
+ if (line.startsWith(entryPrefix)) {
183
+ if (current.length) {
184
+ blocks.push(current);
185
+ }
186
+ current = [line];
187
+ } else if (current.length) {
188
+ current.push(line);
189
+ } else if (line.trim() !== "") {
190
+ return { error: { code: "invalid-config", message: "Existing rigorloop.yaml has malformed adapter entries." } };
191
+ }
192
+ }
193
+ if (current.length) {
194
+ blocks.push(current);
195
+ }
196
+ const adapters = [];
197
+ for (const block of blocks) {
198
+ const name = block[0].slice(entryPrefix.length).trim();
199
+ const descriptor = adapterDescriptor(name);
200
+ if (!descriptor) {
201
+ return { error: { code: "invalid-config", message: `Existing rigorloop.yaml includes unsupported adapter ${name}.` } };
202
+ }
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) });
205
+ }
206
+ return { adapters };
207
+ }
208
+
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]);
229
+ }
230
+ }
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
152
244
  rigorloop:
153
245
  package: "${info.name}"
154
246
  package_version: "${info.version}"
155
- adapters:
156
- - name: codex
157
- install_root: "${INSTALL_ROOT}"
158
- source:
159
- ${sourceLines.join("\n")}
247
+ targets:
248
+ ${[...preserved, selectedBlock].join("\n")}
160
249
  `;
161
250
  }
162
251
 
163
- function plannedLockfile(info, source, manifest) {
252
+ function rootsForArtifact(descriptor, artifact) {
253
+ if (artifact?.install_roots) {
254
+ return artifact.install_roots;
255
+ }
256
+ if (artifact?.install_root) {
257
+ return { skills: artifact.install_root };
258
+ }
259
+ return descriptor.installRoots;
260
+ }
261
+
262
+ function rootHashesForArtifact(descriptor, artifact) {
263
+ if (artifact?.root_hashes) {
264
+ return artifact.root_hashes;
265
+ }
266
+ return Object.fromEntries(
267
+ Object.keys(rootsForArtifact(descriptor, artifact)).map((role) => [
268
+ role,
269
+ {
270
+ tree_sha256: artifact?.tree_sha256 ?? "<planned-after-install>",
271
+ file_count: artifact?.file_count ?? "<planned-after-install>",
272
+ },
273
+ ]),
274
+ );
275
+ }
276
+
277
+ function usesMultiRootLockfile(descriptor, artifact) {
278
+ return descriptor.name === "opencode" || Object.keys(rootsForArtifact(descriptor, artifact)).length > 1;
279
+ }
280
+
281
+ function lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes = rootHashesForArtifact(descriptor, artifact)) {
282
+ const entry = {
283
+ target: descriptor.name,
284
+ release: releaseForPackage(info.version),
285
+ source: source.type,
286
+ archive: source.archive,
287
+ archive_sha256: artifact?.sha256 ?? "<planned>",
288
+ tree_hash_algorithm: "rigorloop-tree-hash-v1",
289
+ };
290
+ if (usesMultiRootLockfile(descriptor, artifact)) {
291
+ return {
292
+ ...entry,
293
+ installed_roots: rootsForArtifact(descriptor, artifact),
294
+ root_hashes: rootHashes,
295
+ };
296
+ }
297
+ const [role] = Object.keys(rootsForArtifact(descriptor, artifact));
298
+ return {
299
+ ...entry,
300
+ installed_root: rootsForArtifact(descriptor, artifact)[role],
301
+ tree_sha256: rootHashes[role].tree_sha256,
302
+ file_count: rootHashes[role].file_count,
303
+ };
304
+ }
305
+
306
+ function plannedLockfile(info, source, manifest, descriptor) {
164
307
  const artifact = source.artifact;
165
308
  return {
166
- schema_version: 1,
309
+ schema_version: 3,
167
310
  rigorloop: {
168
311
  package: info.name,
169
312
  version: info.version,
@@ -173,26 +316,39 @@ function plannedLockfile(info, source, manifest) {
173
316
  sha256: sha256NormalizedText(manifest),
174
317
  },
175
318
  generated: {
176
- adapters: [
177
- {
178
- adapter: ADAPTER,
179
- release: releaseForPackage(info.version),
180
- source: source.type,
181
- archive: source.type === "local-archive" ? basename(source.archive) : source.archive,
182
- archive_sha256: artifact?.sha256 ?? "<planned>",
183
- installed_root: INSTALL_ROOT,
184
- tree_hash_algorithm: "rigorloop-tree-hash-v1",
185
- tree_sha256: artifact?.tree_sha256 ?? "<planned-after-install>",
186
- file_count: "<planned-after-install>",
187
- },
188
- ],
319
+ targets: [lockfileEntryForAdapter(info, source, artifact, descriptor)],
189
320
  },
190
321
  };
191
322
  }
192
323
 
193
- function lockfileForVerifiedInstall(info, source, manifest, artifact, treeHash, fileCount) {
324
+ function existingLockfileEntries(selectedAdapter) {
325
+ const lockfileAbsolutePath = resolve(process.cwd(), LOCKFILE_PATH);
326
+ if (!existsSync(lockfileAbsolutePath)) {
327
+ return [];
328
+ }
329
+ const parsed = parseLockfile(readFileSync(lockfileAbsolutePath, "utf8"));
330
+ if (!parsed.ok) {
331
+ return [];
332
+ }
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
+ });
343
+ }
344
+
345
+ function lockfileForVerifiedInstall(info, source, manifest, artifact, rootHashes, descriptor) {
346
+ const adapters = [
347
+ ...existingLockfileEntries(descriptor.name),
348
+ lockfileEntryForAdapter(info, source, artifact, descriptor, rootHashes),
349
+ ];
194
350
  return {
195
- schema_version: 1,
351
+ schema_version: 3,
196
352
  rigorloop: {
197
353
  package: info.name,
198
354
  version: info.version,
@@ -202,28 +358,19 @@ function lockfileForVerifiedInstall(info, source, manifest, artifact, treeHash,
202
358
  sha256: sha256NormalizedText(manifest),
203
359
  },
204
360
  generated: {
205
- adapters: [
206
- {
207
- adapter: ADAPTER,
208
- release: releaseForPackage(info.version),
209
- source: source.type,
210
- archive: source.type === "local-archive" ? basename(source.archive) : source.archive,
211
- archive_sha256: artifact.sha256,
212
- installed_root: INSTALL_ROOT,
213
- tree_hash_algorithm: "rigorloop-tree-hash-v1",
214
- tree_sha256: treeHash,
215
- file_count: fileCount,
216
- },
217
- ],
361
+ targets: adapters,
218
362
  },
219
363
  };
220
364
  }
221
365
 
222
- function compatibleManifest(content) {
366
+ function compatibleManifest(content, descriptor, artifact) {
367
+ if (descriptor.name === "opencode" && !rootsForArtifact(descriptor, artifact).commands && content.includes(".opencode/commands")) {
368
+ return false;
369
+ }
223
370
  return (
224
371
  content.includes("schema_version: 1") &&
225
- content.includes("name: codex") &&
226
- content.includes(`install_root: "${INSTALL_ROOT}"`)
372
+ content.includes(`name: ${descriptor.name}`) &&
373
+ content.includes(`"${Object.values(rootsForArtifact(descriptor, artifact))[0]}"`)
227
374
  );
228
375
  }
229
376
 
@@ -234,8 +381,11 @@ function pathState(path) {
234
381
  return statSync(path).isDirectory() ? "directory" : "file";
235
382
  }
236
383
 
237
- function directoryKind(path) {
238
- return path === AGENTS_ROOT ? "codex-agent-root" : "codex-install-root";
384
+ function directoryKind(path, descriptor, artifact) {
385
+ if (Object.values(rootsForArtifact(descriptor, artifact)).includes(path)) {
386
+ return `${descriptor.name}-install-root`;
387
+ }
388
+ return `${descriptor.name}-adapter-root`;
239
389
  }
240
390
 
241
391
  function sha256(bytes) {
@@ -338,11 +488,77 @@ function loadJsonFile(path) {
338
488
  async function fetchBytes(url) {
339
489
  const response = await fetch(url);
340
490
  if (!response.ok) {
341
- throw new Error(`HTTP ${response.status}`);
491
+ throw Object.assign(new Error(`HTTP ${response.status}`), { downloadFailureClass: "http-status" });
342
492
  }
343
493
  return Buffer.from(await response.arrayBuffer());
344
494
  }
345
495
 
496
+ const PROXY_ENV_VAR_ALLOWLIST = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"];
497
+ const DOWNLOAD_FAILURE_CLASSES = new Set(["dns", "tls", "timeout", "http-status", "proxy", "network", "unknown"]);
498
+
499
+ function detectedProxyEnvVars(env = process.env) {
500
+ return PROXY_ENV_VAR_ALLOWLIST.filter((name) => Object.prototype.hasOwnProperty.call(env, name) && env[name]);
501
+ }
502
+
503
+ function nodeEnvProxyStatus(env = process.env, execArgv = process.execArgv) {
504
+ const nodeOptions = String(env.NODE_OPTIONS ?? "");
505
+ const useEnvProxy = String(env.NODE_USE_ENV_PROXY ?? "").toLowerCase();
506
+ // CR-M4-R1-F1: Node can enable fetch env-proxy via env vars or the runtime flag.
507
+ if (nodeOptions.includes("--use-env-proxy") || execArgv.includes("--use-env-proxy") || ["1", "true", "yes"].includes(useEnvProxy)) {
508
+ return "enabled";
509
+ }
510
+ if (detectedProxyEnvVars(env).length > 0) {
511
+ return "disabled";
512
+ }
513
+ return "unknown";
514
+ }
515
+
516
+ function downloadFailureClass(error) {
517
+ if (DOWNLOAD_FAILURE_CLASSES.has(error?.downloadFailureClass)) {
518
+ return error.downloadFailureClass;
519
+ }
520
+ const code = String(error?.code ?? error?.cause?.code ?? "").toUpperCase();
521
+ const message = String(error?.message ?? "").toLowerCase();
522
+ if (["ENOTFOUND", "EAI_AGAIN"].includes(code)) {
523
+ return "dns";
524
+ }
525
+ if (code.includes("CERT") || code.includes("TLS") || message.includes("certificate") || message.includes("tls")) {
526
+ return "tls";
527
+ }
528
+ if (code.includes("TIMEOUT") || code === "ABORT_ERR" || message.includes("timeout") || message.includes("timed out")) {
529
+ return "timeout";
530
+ }
531
+ if (code.includes("PROXY") || message.includes("proxy")) {
532
+ return "proxy";
533
+ }
534
+ if (["ECONNRESET", "ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"].includes(code) || message.includes("fetch failed")) {
535
+ return "network";
536
+ }
537
+ return "unknown";
538
+ }
539
+
540
+ function downloadFailureDiagnostics(error, artifact, descriptor, metadata) {
541
+ return {
542
+ adapter: descriptor.name,
543
+ release: metadata.release.version,
544
+ archive_url: artifact.url,
545
+ download_failure_class: downloadFailureClass(error),
546
+ node_env_proxy_status: nodeEnvProxyStatus(),
547
+ proxy_env_vars_detected: detectedProxyEnvVars(),
548
+ };
549
+ }
550
+
551
+ function downloadFailureBlocker(error, artifact, descriptor, metadata) {
552
+ const diagnostics = downloadFailureDiagnostics(error, artifact, descriptor, metadata);
553
+ return {
554
+ code: "release-download-failed",
555
+ message: `Network download failed for adapter ${descriptor.name} release ${metadata.release.version} (failure class ${diagnostics.download_failure_class}).`,
556
+ path: artifact.url,
557
+ next_action: `Download ${artifact.url} and rerun with --from-archive ./${artifact.archive}.`,
558
+ diagnostics,
559
+ };
560
+ }
561
+
346
562
  function parseVerifiedMetadataBytes(bytes, expectedSha256) {
347
563
  const actualSha256 = sha256(bytes);
348
564
  if (actualSha256 !== expectedSha256) {
@@ -382,7 +598,15 @@ function isSha256(value) {
382
598
  return typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
383
599
  }
384
600
 
385
- function validateMetadata(metadata, info) {
601
+ function releaseListedForSkillsOnlyCompatibility(marker, release) {
602
+ if (!marker) {
603
+ return false;
604
+ }
605
+ const releases = Array.isArray(marker) ? marker : marker.releases;
606
+ return Array.isArray(releases) && releases.includes(release);
607
+ }
608
+
609
+ function validateMetadata(metadata, info, descriptor) {
386
610
  const release = releaseForPackage(info.version);
387
611
  if (!metadata || metadata.schema_version !== 1) {
388
612
  return { error: { code: "metadata-invalid", message: "Adapter metadata schema_version must be 1." } };
@@ -407,26 +631,76 @@ function validateMetadata(metadata, info) {
407
631
  if (!isNonEmptyString(metadata.validation?.command)) {
408
632
  return { error: { code: "metadata-invalid", message: "Adapter metadata validation command is missing." } };
409
633
  }
410
- const artifact = metadata.artifacts?.find((entry) => entry.adapter === ADAPTER);
634
+ const artifact = metadata.artifacts?.find((entry) => entry.adapter === descriptor.name);
411
635
  if (!artifact) {
412
- return { blocker: metadataBlocker("adapter-unknown", "Adapter metadata does not include Codex.") };
636
+ return { blocker: metadataBlocker("metadata-unavailable", `Adapter metadata does not include ${descriptor.displayName}.`) };
413
637
  }
414
- if (
415
- !isNonEmptyString(artifact.archive) ||
416
- !isNonEmptyString(artifact.url) ||
417
- !isSha256(artifact.sha256) ||
418
- !Number.isInteger(artifact.size_bytes) ||
419
- artifact.size_bytes < 0 ||
420
- !isSha256(artifact.tree_sha256)
421
- ) {
422
- return { error: { code: "metadata-invalid", message: "Codex adapter artifact metadata is incomplete." } };
423
- }
424
- if ((artifact.install_root ?? "").replace(/\/$/, "") !== INSTALL_ROOT) {
425
- return { error: { code: "metadata-invalid", message: "Codex adapter install root is not .agents/skills." } };
638
+ if (!isNonEmptyString(artifact.archive) || !isNonEmptyString(artifact.url) || !isSha256(artifact.sha256) || !Number.isInteger(artifact.size_bytes) || artifact.size_bytes < 0) {
639
+ return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter artifact metadata is incomplete.` } };
426
640
  }
427
641
  if (artifact.tree_hash_algorithm && artifact.tree_hash_algorithm !== "rigorloop-tree-hash-v1") {
428
642
  return { error: { code: "metadata-invalid", message: "Unsupported tree hash algorithm in adapter metadata." } };
429
643
  }
644
+ if (artifact.install_roots || artifact.root_hashes) {
645
+ if (!artifact.install_roots || !artifact.root_hashes) {
646
+ return { error: { code: "metadata-invalid", message: `${descriptor.displayName} multi-root metadata is incomplete.` } };
647
+ }
648
+ for (const [role, root] of Object.entries(artifact.install_roots)) {
649
+ if (descriptor.installRoots[role] !== root) {
650
+ return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter install root for ${role} is not supported.` } };
651
+ }
652
+ const rootHash = artifact.root_hashes[role];
653
+ if (!rootHash || !isSha256(rootHash.tree_sha256) || !Number.isInteger(rootHash.file_count) || rootHash.file_count < 0) {
654
+ return { error: { code: "metadata-invalid", message: `${descriptor.displayName} root hash metadata is incomplete.` } };
655
+ }
656
+ }
657
+ } else {
658
+ if (!isSha256(artifact.tree_sha256) || !Number.isInteger(artifact.file_count) || artifact.file_count < 0) {
659
+ return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter single-root metadata is incomplete.` } };
660
+ }
661
+ if ((artifact.install_root ?? "").replace(/\/$/, "") !== descriptor.primaryInstallRoot()) {
662
+ return { error: { code: "metadata-invalid", message: `${descriptor.displayName} adapter install root is not ${descriptor.primaryInstallRoot()}.` } };
663
+ }
664
+ }
665
+ if (artifact.command_aliases?.opencode) {
666
+ if (descriptor.name !== "opencode" || !artifact.install_roots?.commands) {
667
+ return { error: { code: "metadata-invalid", message: "opencode command alias metadata requires the opencode commands root." } };
668
+ }
669
+ const aliasPaths = opencodeCommandAliasPaths(artifact);
670
+ if (
671
+ !Number.isInteger(artifact.command_aliases.opencode.count) ||
672
+ !Array.isArray(aliasPaths) ||
673
+ artifact.command_aliases.opencode.count !== aliasPaths.length ||
674
+ aliasPaths.some((aliasPath) => !isNonEmptyString(aliasPath) || !aliasPath.startsWith(`${descriptor.installRoots.commands}/`))
675
+ ) {
676
+ return { error: { code: "metadata-invalid", message: "opencode command alias metadata is incomplete." } };
677
+ }
678
+ }
679
+ // CR-M3-R2-F1: opencode commands root is valid only with declared command aliases.
680
+ if (descriptor.name === "opencode" && artifact.install_roots?.commands && !artifact.command_aliases?.opencode) {
681
+ return {
682
+ blocker: metadataBlocker(
683
+ "opencode-command-aliases-missing",
684
+ "Opencode commands root metadata requires command_aliases.opencode.",
685
+ artifact.archive,
686
+ "Use opencode metadata that declares command aliases, or use explicitly compatible skills-only metadata without the commands root.",
687
+ ),
688
+ };
689
+ }
690
+ // CR-M3-R1-F1: skills-only opencode compatibility must be explicit in trusted metadata.
691
+ if (descriptor.name === "opencode" && !artifact.command_aliases?.opencode && !rootsForArtifact(descriptor, artifact).commands) {
692
+ const marker = artifact.skills_only_compatibility ?? metadata.compatibility?.opencode_skills_only;
693
+ if (!releaseListedForSkillsOnlyCompatibility(marker, release)) {
694
+ return {
695
+ blocker: metadataBlocker(
696
+ "opencode-skills-only-compatibility-unmarked",
697
+ "Opencode skills-only archive metadata is not explicitly marked compatible.",
698
+ artifact.archive,
699
+ "Use an opencode archive with command alias metadata or bundled trusted skills-only compatibility metadata.",
700
+ ),
701
+ };
702
+ }
703
+ }
430
704
  return { artifact };
431
705
  }
432
706
 
@@ -500,36 +774,66 @@ function parseZipEntries(buffer) {
500
774
  return entries;
501
775
  }
502
776
 
503
- function unsafePathCode(name) {
777
+ function unsafePathCode(name, descriptor, artifact) {
504
778
  if (!name || name.startsWith("/") || name.startsWith("\\") || /^[A-Za-z]:/.test(name) || name.includes("\\")) {
505
779
  return "archive-path-traversal";
506
780
  }
507
781
  if (name.split("/").some((part) => part === ".." || part === "")) {
508
782
  return "archive-path-traversal";
509
783
  }
510
- if (!name.startsWith(`${INSTALL_ROOT}/`)) {
784
+ const allowedRoots = Object.values(rootsForArtifact(descriptor, artifact));
785
+ if (!allowedRoots.some((root) => name.startsWith(`${root}/`))) {
511
786
  return "archive-install-root-invalid";
512
787
  }
513
788
  return undefined;
514
789
  }
515
790
 
516
791
  function isArchiveSupportEntry(name) {
517
- return name === "AGENTS.md";
792
+ return name === "AGENTS.md" || name === "CLAUDE.md";
518
793
  }
519
794
 
520
- function fileRowsForTree(entries) {
795
+ function fileRowsForTreeRoot(entries, installRoot) {
521
796
  return entries
522
- .filter((entry) => !entry.directory)
797
+ .filter((entry) => !entry.directory && entry.name.startsWith(`${installRoot}/`))
523
798
  .map((entry) => {
524
- const relativePath = entry.name.slice(`${INSTALL_ROOT}/`.length);
799
+ const relativePath = entry.name.slice(`${installRoot}/`.length);
525
800
  const bytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
526
801
  return [relativePath, sha256(bytes)];
527
802
  })
528
803
  .sort(([left], [right]) => left.localeCompare(right));
529
804
  }
530
805
 
531
- function treeHashForEntries(entries) {
532
- return treeHashForRows(fileRowsForTree(entries));
806
+ function opencodeCommandAliasPaths(artifact) {
807
+ const aliases = artifact?.command_aliases?.opencode;
808
+ if (!aliases) {
809
+ return undefined;
810
+ }
811
+ if (Array.isArray(aliases.paths)) {
812
+ return aliases.paths;
813
+ }
814
+ if (aliases.aliases && typeof aliases.aliases === "object") {
815
+ return Object.values(aliases.aliases);
816
+ }
817
+ return [];
818
+ }
819
+
820
+ function treeHashForEntries(entries, descriptor) {
821
+ return treeHashForRows(fileRowsForTreeRoot(entries, descriptor.primaryInstallRoot()));
822
+ }
823
+
824
+ function rootHashesForEntries(entries, descriptor, artifact) {
825
+ return Object.fromEntries(
826
+ Object.entries(rootsForArtifact(descriptor, artifact)).map(([role, root]) => {
827
+ const rows = fileRowsForTreeRoot(entries, root);
828
+ return [
829
+ role,
830
+ {
831
+ tree_sha256: treeHashForRows(rows),
832
+ file_count: rows.length,
833
+ },
834
+ ];
835
+ }),
836
+ );
533
837
  }
534
838
 
535
839
  function treeHashForRows(rows) {
@@ -573,22 +877,26 @@ function treeHashForFilesystem(root) {
573
877
  };
574
878
  }
575
879
 
576
- function currentCodexLockfileEntry() {
880
+ function currentLockfileEntries() {
577
881
  const lockfileAbsolutePath = resolve(process.cwd(), LOCKFILE_PATH);
578
882
  if (!existsSync(lockfileAbsolutePath)) {
579
- return undefined;
883
+ return [];
580
884
  }
581
885
  const parsed = parseLockfile(readFileSync(lockfileAbsolutePath, "utf8"));
582
886
  if (!parsed.ok) {
583
- return undefined;
887
+ return [];
584
888
  }
585
- return parsed.lockfile.generated.adapters.find((entry) => entry.adapter === ADAPTER && entry.installed_root === INSTALL_ROOT);
889
+ return parsed.lockfile.generated.targets ?? parsed.lockfile.generated.adapters;
890
+ }
891
+
892
+ function currentLockfileEntry(descriptor) {
893
+ return currentLockfileEntries().find((entry) => (entry.target ?? entry.adapter) === descriptor.name);
586
894
  }
587
895
 
588
896
  function installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) {
589
897
  return {
590
898
  code: "installed-tree-mismatch",
591
- message: "Installed Codex adapter tree does not match trusted metadata.",
899
+ message: "Installed adapter tree does not match trusted metadata.",
592
900
  expected_tree_sha256: expectedTreeHash,
593
901
  actual_tree_sha256: actualTree.treeHash,
594
902
  expected_file_count: expectedFileCount,
@@ -596,23 +904,29 @@ function installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCo
596
904
  };
597
905
  }
598
906
 
599
- function verifyInstalledTree(entries, artifact, { allowMissingOrEmpty = false } = {}) {
600
- const expectedRows = fileRowsForTree(entries);
601
- const expectedTreeHash = artifact.tree_sha256;
602
- const expectedFileCount = expectedRows.length;
907
+ function verifyInstalledTree(entries, artifact, descriptor, { allowMissingOrEmpty = false } = {}) {
908
+ const expectedRootHashes = rootHashesForEntries(entries, descriptor, artifact);
909
+ for (const [role, root] of Object.entries(rootsForArtifact(descriptor, artifact))) {
910
+ const expectedRows = fileRowsForTreeRoot(entries, root);
911
+ const expectedTreeHash = artifact.root_hashes?.[role]?.tree_sha256 ?? artifact.tree_sha256;
912
+ const expectedFileCount = artifact.root_hashes?.[role]?.file_count ?? expectedRows.length;
603
913
 
604
- if (!existsSync(resolve(process.cwd(), INSTALL_ROOT))) {
605
- return allowMissingOrEmpty ? { ok: true, expectedRows, expectedFileCount } : { error: installedTreeMismatchError({ treeHash: "<missing>", fileCount: 0 }, expectedTreeHash, expectedFileCount) };
606
- }
914
+ if (!existsSync(resolve(process.cwd(), root))) {
915
+ if (allowMissingOrEmpty) {
916
+ continue;
917
+ }
918
+ return { error: installedTreeMismatchError({ treeHash: "<missing>", fileCount: 0 }, expectedTreeHash, expectedFileCount) };
919
+ }
607
920
 
608
- const actualTree = treeHashForFilesystem(INSTALL_ROOT);
609
- if (allowMissingOrEmpty && actualTree.fileCount === 0) {
610
- return { ok: true, expectedRows, expectedFileCount };
611
- }
612
- if (!rowsEqual(actualTree.rows, expectedRows) || actualTree.treeHash !== expectedTreeHash) {
613
- return { error: installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) };
921
+ const actualTree = treeHashForFilesystem(root);
922
+ if (allowMissingOrEmpty && actualTree.fileCount === 0) {
923
+ continue;
924
+ }
925
+ if (!rowsEqual(actualTree.rows, expectedRows) || actualTree.treeHash !== expectedTreeHash || actualTree.fileCount !== expectedFileCount) {
926
+ return { error: installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) };
927
+ }
614
928
  }
615
- return { ok: true, expectedRows, expectedFileCount, treeHash: actualTree.treeHash };
929
+ return { ok: true, rootHashes: expectedRootHashes, expectedFileCount: expectedRootHashes.skills?.file_count ?? 0, treeHash: expectedRootHashes.skills?.tree_sha256 };
616
930
  }
617
931
 
618
932
  function generatedOutputConflictBlocker(entries) {
@@ -655,13 +969,29 @@ function lockfileDriftBlocker(lockfileEntry) {
655
969
  if (!lockfileEntry) {
656
970
  return undefined;
657
971
  }
972
+ if (lockfileEntry.installed_roots) {
973
+ for (const [role, root] of Object.entries(lockfileEntry.installed_roots)) {
974
+ const rootHash = lockfileEntry.root_hashes[role];
975
+ const blocker = lockfileDriftBlocker({
976
+ adapter: lockfileEntry.adapter,
977
+ target: lockfileEntry.target,
978
+ installed_root: root,
979
+ tree_sha256: rootHash.tree_sha256,
980
+ file_count: rootHash.file_count,
981
+ });
982
+ if (blocker) {
983
+ return blocker;
984
+ }
985
+ }
986
+ return undefined;
987
+ }
658
988
 
659
989
  const rootState = pathState(resolve(process.cwd(), lockfileEntry.installed_root));
660
990
  if (rootState === "absent") {
661
991
  return {
662
992
  code: "generated-output-missing",
663
993
  message: "Codex generated output recorded in rigorloop.lock is missing.",
664
- adapter: lockfileEntry.adapter,
994
+ target: lockfileEntry.target ?? lockfileEntry.adapter,
665
995
  installed_root: lockfileEntry.installed_root,
666
996
  expected_tree_sha256: lockfileEntry.tree_sha256,
667
997
  actual_tree_sha256: null,
@@ -682,7 +1012,7 @@ function lockfileDriftBlocker(lockfileEntry) {
682
1012
  return {
683
1013
  code: "generated-output-drift",
684
1014
  message: "Codex generated output differs from rigorloop.lock.",
685
- adapter: lockfileEntry.adapter,
1015
+ target: lockfileEntry.target ?? lockfileEntry.adapter,
686
1016
  installed_root: lockfileEntry.installed_root,
687
1017
  expected_tree_sha256: lockfileEntry.tree_sha256,
688
1018
  actual_tree_sha256: actualTree.treeHash,
@@ -695,7 +1025,127 @@ function lockfileDriftBlocker(lockfileEntry) {
695
1025
  return undefined;
696
1026
  }
697
1027
 
698
- function inspectArchive(archiveBytes, artifact) {
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
+
1138
+ function firstLockfileDriftBlocker() {
1139
+ for (const entry of currentLockfileEntries()) {
1140
+ const blocker = lockfileDriftBlocker(entry);
1141
+ if (blocker) {
1142
+ return blocker;
1143
+ }
1144
+ }
1145
+ return undefined;
1146
+ }
1147
+
1148
+ function inspectArchive(archiveBytes, artifact, descriptor) {
699
1149
  if (artifact.size_bytes !== undefined && archiveBytes.length !== artifact.size_bytes) {
700
1150
  return { error: { code: "archive-size-mismatch", message: "Archive size does not match metadata." } };
701
1151
  }
@@ -716,7 +1166,7 @@ function inspectArchive(archiveBytes, artifact) {
716
1166
  if (isArchiveSupportEntry(entry.name)) {
717
1167
  continue;
718
1168
  }
719
- const pathCode = unsafePathCode(entry.name);
1169
+ const pathCode = unsafePathCode(entry.name, descriptor, artifact);
720
1170
  if (pathCode) {
721
1171
  return { error: { code: pathCode, message: `Archive entry is not allowed: ${entry.name}`, path: entry.name } };
722
1172
  }
@@ -727,14 +1177,34 @@ function inspectArchive(archiveBytes, artifact) {
727
1177
  }
728
1178
 
729
1179
  const files = installEntries.filter((entry) => !entry.directory);
730
- const treeHash = treeHashForEntries(files);
731
- if (artifact.tree_sha256 && treeHash !== artifact.tree_sha256) {
732
- return { error: { code: "tree-hash-mismatch", message: "Installed tree hash does not match metadata." } };
1180
+ const rootHashes = rootHashesForEntries(files, descriptor, artifact);
1181
+ for (const [role, hash] of Object.entries(rootHashes)) {
1182
+ const expected = artifact.root_hashes?.[role] ?? { tree_sha256: artifact.tree_sha256, file_count: artifact.file_count };
1183
+ if (expected.tree_sha256 && hash.tree_sha256 !== expected.tree_sha256) {
1184
+ return { error: { code: "tree-hash-mismatch", message: "Installed tree hash does not match metadata." } };
1185
+ }
1186
+ if (expected.file_count !== undefined && hash.file_count !== expected.file_count) {
1187
+ return { error: { code: "tree-hash-mismatch", message: "Installed tree file count does not match metadata." } };
1188
+ }
733
1189
  }
734
- return { entries: files, archiveHash, treeHash, fileCount: files.length };
1190
+ if (descriptor.name === "opencode") {
1191
+ for (const aliasPath of opencodeCommandAliasPaths(artifact) ?? []) {
1192
+ if (!files.some((entry) => entry.name === aliasPath)) {
1193
+ return {
1194
+ error: {
1195
+ code: "opencode-command-alias-missing",
1196
+ message: "Declared opencode command alias is missing from archive.",
1197
+ path: aliasPath,
1198
+ },
1199
+ };
1200
+ }
1201
+ }
1202
+ }
1203
+ return { entries: files, archiveHash, rootHashes, treeHash: rootHashes.skills?.tree_sha256, fileCount: rootHashes.skills?.file_count ?? files.length };
735
1204
  }
736
1205
 
737
- function addArchiveActions(plan, entries) {
1206
+ function addArchiveActions(plan, entries, descriptor) {
1207
+ const installRoot = descriptor.primaryInstallRoot();
738
1208
  const directories = new Set();
739
1209
  for (const entry of entries) {
740
1210
  const parts = entry.name.split("/");
@@ -770,7 +1240,7 @@ function addArchiveActions(plan, entries) {
770
1240
  const state = pathState(resolve(process.cwd(), entry.name));
771
1241
  let existingMatches = false;
772
1242
  if (state === "file") {
773
- const relativePath = entry.name.slice(`${INSTALL_ROOT}/`.length);
1243
+ const relativePath = entry.name.slice(`${installRoot}/`.length);
774
1244
  const existingBytes = relativePath.endsWith(".md")
775
1245
  ? normalizeText(readFileSync(resolve(process.cwd(), entry.name)))
776
1246
  : readFileSync(resolve(process.cwd(), entry.name));
@@ -783,9 +1253,9 @@ function addArchiveActions(plan, entries) {
783
1253
  status: state === "absent" ? "pending" : existingMatches ? "skipped" : "blocked",
784
1254
  reason:
785
1255
  state === "absent"
786
- ? "Install verified Codex adapter file."
1256
+ ? `Install verified ${descriptor.displayName} adapter file.`
787
1257
  : existingMatches
788
- ? `${entry.name} already matches verified Codex adapter content.`
1258
+ ? `${entry.name} already matches verified ${descriptor.displayName} adapter content.`
789
1259
  : `${entry.name} already exists.`,
790
1260
  });
791
1261
  plan.artifacts.push({
@@ -804,23 +1274,47 @@ function addArchiveActions(plan, entries) {
804
1274
  }
805
1275
  }
806
1276
 
807
- function writeArchiveEntries(entries) {
1277
+ function writeArchiveEntries(entries, descriptor) {
1278
+ const installRoot = descriptor.primaryInstallRoot();
808
1279
  for (const entry of entries) {
809
1280
  const outputPath = resolve(process.cwd(), entry.name);
810
1281
  mkdirSync(dirname(outputPath), { recursive: true });
811
- const relativePath = entry.name.slice(`${INSTALL_ROOT}/`.length);
1282
+ const relativePath = entry.name.slice(`${installRoot}/`.length);
812
1283
  const bytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
813
1284
  writeFileSync(outputPath, bytes);
814
1285
  }
815
1286
  }
816
1287
 
817
- function planDirectoryActions(flags) {
1288
+ function initWarnings(descriptor, artifact) {
1289
+ if (
1290
+ descriptor.name === "opencode" &&
1291
+ artifact &&
1292
+ !artifact.command_aliases?.opencode &&
1293
+ !rootsForArtifact(descriptor, artifact).commands
1294
+ ) {
1295
+ return [
1296
+ {
1297
+ code: "opencode-command-aliases-not-declared",
1298
+ message: "Selected opencode archive metadata does not declare command aliases; only skills were installed.",
1299
+ },
1300
+ ];
1301
+ }
1302
+ return [];
1303
+ }
1304
+
1305
+ function directoryPlanForRoots(roots) {
1306
+ return [...new Set(Object.values(roots).flatMap((root) => [root.split("/").slice(0, -1).join("/"), root]).filter(Boolean))];
1307
+ }
1308
+
1309
+ function planDirectoryActions(flags, descriptor, artifact) {
818
1310
  const actions = [];
819
1311
  const artifacts = [];
820
1312
  const blockers = [];
821
1313
  let parentBlocked = false;
822
1314
 
823
- for (const relativePath of DIRECTORY_PLAN) {
1315
+ const directoryPlan = directoryPlanForRoots(rootsForArtifact(descriptor, artifact));
1316
+ const rootParent = directoryPlan[0];
1317
+ for (const relativePath of directoryPlan) {
824
1318
  const state = parentBlocked ? "blocked-by-parent" : pathState(resolve(process.cwd(), relativePath));
825
1319
  if (state === "absent") {
826
1320
  actions.push({
@@ -831,7 +1325,7 @@ function planDirectoryActions(flags) {
831
1325
  });
832
1326
  artifacts.push({
833
1327
  path: relativePath,
834
- kind: directoryKind(relativePath),
1328
+ kind: directoryKind(relativePath, descriptor, artifact),
835
1329
  status: flags.dryRun ? "planned" : "pending",
836
1330
  });
837
1331
  } else if (state === "directory") {
@@ -843,7 +1337,7 @@ function planDirectoryActions(flags) {
843
1337
  });
844
1338
  artifacts.push({
845
1339
  path: relativePath,
846
- kind: directoryKind(relativePath),
1340
+ kind: directoryKind(relativePath, descriptor, artifact),
847
1341
  status: "existing",
848
1342
  });
849
1343
  } else {
@@ -853,12 +1347,12 @@ function planDirectoryActions(flags) {
853
1347
  status: "blocked",
854
1348
  reason:
855
1349
  state === "blocked-by-parent"
856
- ? `${relativePath} cannot be created because ${AGENTS_ROOT} is not a directory.`
1350
+ ? `${relativePath} cannot be created because ${rootParent} is not a directory.`
857
1351
  : `${relativePath} exists and is not a directory.`,
858
1352
  });
859
1353
  artifacts.push({
860
1354
  path: relativePath,
861
- kind: directoryKind(relativePath),
1355
+ kind: directoryKind(relativePath, descriptor, artifact),
862
1356
  status: "blocked",
863
1357
  });
864
1358
  if (state !== "blocked-by-parent") {
@@ -869,7 +1363,7 @@ function planDirectoryActions(flags) {
869
1363
  next_action: `Move the existing file before running init.`,
870
1364
  });
871
1365
  }
872
- if (relativePath === AGENTS_ROOT) {
1366
+ if (relativePath === rootParent) {
873
1367
  parentBlocked = true;
874
1368
  }
875
1369
  }
@@ -887,7 +1381,7 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
887
1381
  status: flags.dryRun ? "planned" : "pending",
888
1382
  reason: flags.dryRun
889
1383
  ? "Plan durable lockfile content."
890
- : "Write durable lockfile after verified Codex adapter install.",
1384
+ : "Write durable lockfile after verified target install.",
891
1385
  });
892
1386
  artifacts.push({
893
1387
  path: LOCKFILE_PATH,
@@ -905,7 +1399,7 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
905
1399
  status: flags.dryRun ? "planned" : "pending",
906
1400
  reason: flags.dryRun
907
1401
  ? "Plan update to supported rigorloop.lock."
908
- : "Update supported rigorloop.lock after verified Codex adapter install.",
1402
+ : "Update supported rigorloop.lock after verified target install.",
909
1403
  });
910
1404
  artifacts.push({
911
1405
  path: LOCKFILE_PATH,
@@ -944,15 +1438,16 @@ function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
944
1438
  }
945
1439
  }
946
1440
 
947
- function buildInitPlan(flags, artifact) {
1441
+ function buildInitPlan(flags, descriptor, artifact) {
948
1442
  const info = packageInfo();
949
- const source = sourceForFlags(flags, info);
1443
+ const source = sourceForFlags(flags, info, descriptor);
950
1444
  if (artifact) {
951
1445
  source.artifact = artifact;
952
1446
  }
953
1447
  const manifestPath = "rigorloop.yaml";
954
1448
  const manifestAbsolutePath = resolve(process.cwd(), manifestPath);
955
- const manifest = manifestContent(info, source);
1449
+ const existingManifest = existsSync(manifestAbsolutePath) ? readFileSync(manifestAbsolutePath, "utf8") : undefined;
1450
+ const manifest = flags.writeState ? manifestContent(info, source, descriptor, existingManifest) : undefined;
956
1451
  const actions = [];
957
1452
  const artifacts = [];
958
1453
  const blockers = [];
@@ -963,59 +1458,67 @@ function buildInitPlan(flags, artifact) {
963
1458
  code: "invalid-archive-path",
964
1459
  message: "Missing required value for --from-archive.",
965
1460
  path: "--from-archive",
966
- next_action: "Provide an existing Codex adapter archive path or omit --from-archive.",
1461
+ next_action: `Provide an existing ${descriptor.displayName} adapter archive path or omit --from-archive.`,
967
1462
  });
968
1463
  } else if (flags.fromArchiveProvided && !existsSync(resolve(process.cwd(), flags.fromArchive))) {
969
1464
  errors.push({
970
1465
  code: "invalid-archive-path",
971
1466
  message: `Local archive path does not exist: ${flags.fromArchive}`,
972
1467
  path: flags.fromArchive,
973
- next_action: "Provide an existing Codex adapter archive path or omit --from-archive.",
1468
+ next_action: `Provide an existing ${descriptor.displayName} adapter archive path or omit --from-archive.`,
974
1469
  });
975
1470
  }
976
1471
 
977
- const directoryPlan = planDirectoryActions(flags);
1472
+ const directoryPlan = planDirectoryActions(flags, descriptor, artifact);
978
1473
  actions.push(...directoryPlan.actions);
979
1474
  artifacts.push(...directoryPlan.artifacts);
980
1475
  blockers.push(...directoryPlan.blockers);
981
1476
 
982
- if (existsSync(manifestAbsolutePath)) {
983
- const existingManifest = readFileSync(manifestAbsolutePath, "utf8");
984
- if (compatibleManifest(existingManifest)) {
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
+ }
985
1495
  actions.push({
986
1496
  type: "write",
987
1497
  path: manifestPath,
988
- status: flags.dryRun ? "planned" : "skipped",
989
- reason: "Compatible rigorloop.yaml already exists.",
1498
+ status: flags.dryRun ? "planned" : "pending",
1499
+ reason: `Write target-oriented rigorloop.yaml for ${descriptor.displayName} support.`,
990
1500
  });
991
1501
  artifacts.push({
992
1502
  path: manifestPath,
993
1503
  kind: "project-manifest",
994
- status: "existing",
1504
+ status: flags.dryRun ? "planned" : "pending",
995
1505
  });
996
1506
  } else {
997
- errors.push({
998
- code: "invalid-config",
999
- message: "Existing rigorloop.yaml is not compatible with the first-slice Codex init contract.",
1507
+ actions.push({
1508
+ type: "write",
1000
1509
  path: manifestPath,
1001
- next_action: "Review or move the existing file before running init.",
1510
+ status: flags.dryRun ? "planned" : "pending",
1511
+ reason: "Create target-oriented RigorLoop project manifest.",
1512
+ });
1513
+ artifacts.push({
1514
+ path: manifestPath,
1515
+ kind: "project-manifest",
1516
+ status: flags.dryRun ? "planned" : "pending",
1002
1517
  });
1003
1518
  }
1004
- } else {
1005
- actions.push({
1006
- type: "write",
1007
- path: manifestPath,
1008
- status: flags.dryRun ? "planned" : "pending",
1009
- reason: "Create first-slice RigorLoop project manifest.",
1010
- });
1011
- artifacts.push({
1012
- path: manifestPath,
1013
- kind: "project-manifest",
1014
- status: flags.dryRun ? "planned" : "pending",
1015
- });
1016
- }
1017
1519
 
1018
- addLockfilePlan(flags, actions, artifacts, blockers, errors);
1520
+ addLockfilePlan(flags, actions, artifacts, blockers, errors);
1521
+ }
1019
1522
 
1020
1523
  return {
1021
1524
  info,
@@ -1025,7 +1528,7 @@ function buildInitPlan(flags, artifact) {
1025
1528
  artifacts,
1026
1529
  blockers,
1027
1530
  errors,
1028
- planned_lockfile: plannedLockfile(info, source, manifest),
1531
+ planned_lockfile: flags.writeState ? plannedLockfile(info, source, manifest, descriptor) : undefined,
1029
1532
  };
1030
1533
  }
1031
1534
 
@@ -1111,19 +1614,19 @@ function invalidArchivePath(message, flags) {
1111
1614
  code: "invalid-archive-path",
1112
1615
  message,
1113
1616
  path: flags.fromArchive,
1114
- next_action: "Provide an existing Codex adapter archive path or omit --from-archive.",
1617
+ next_action: "Provide an existing supported adapter archive path or omit --from-archive.",
1115
1618
  });
1116
1619
  }
1117
1620
 
1118
1621
  function unsupportedAdapter(adapter, flags) {
1119
1622
  const result = envelope("init", flags, {
1120
1623
  status: "blocked",
1121
- summary: `Adapter '${adapter}' is not supported in this slice.`,
1624
+ summary: `Target '${adapter}' is not supported.`,
1122
1625
  blockers: [
1123
1626
  {
1124
- code: "adapter-unsupported",
1125
- message: `Adapter '${adapter}' is not supported in this slice.`,
1126
- next_action: "Use --adapter codex.",
1627
+ code: "target-unknown",
1628
+ message: `Target '${adapter}' is not supported.`,
1629
+ next_action: `Use one of: ${supportedAdapterNames().join(", ")}.`,
1127
1630
  },
1128
1631
  ],
1129
1632
  });
@@ -1131,11 +1634,32 @@ function unsupportedAdapter(adapter, flags) {
1131
1634
  if (flags.json) {
1132
1635
  writeJson(result);
1133
1636
  } else {
1134
- process.stderr.write(`${result.summary}\nUse --adapter codex.\n`);
1637
+ process.stderr.write(`${result.summary}\nUse one of: ${supportedAdapterNames().join(", ")}.\n`);
1135
1638
  }
1136
1639
  return exitCodeForResult({ ...result, exit_class: "blocked" });
1137
1640
  }
1138
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
+
1139
1663
  function writeBlockedResult(flags, plan, summary, blockers, exitClass = "blocked") {
1140
1664
  for (const action of plan.actions) {
1141
1665
  if (action.status === "pending") {
@@ -1148,18 +1672,26 @@ function writeBlockedResult(flags, plan, summary, blockers, exitClass = "blocked
1148
1672
  artifact.status = "blocked";
1149
1673
  }
1150
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
+ : {};
1151
1684
  const result = envelope("init", flags, {
1152
1685
  status: "blocked",
1153
1686
  summary,
1154
1687
  actions: plan.actions,
1155
1688
  artifacts: plan.artifacts,
1156
1689
  blockers,
1157
- planned_manifest: {
1158
- path: "rigorloop.yaml",
1159
- content: plan.manifest,
1160
- },
1161
- planned_lockfile: plan.planned_lockfile,
1690
+ ...statePlan,
1162
1691
  });
1692
+ if (blockers[0]?.diagnostics) {
1693
+ result.diagnostics = { ...result.diagnostics, ...blockers[0].diagnostics };
1694
+ }
1163
1695
  if (flags.json) {
1164
1696
  writeJson(result);
1165
1697
  } else {
@@ -1184,17 +1716,22 @@ function writeValidationErrorResult(flags, plan, error) {
1184
1716
  artifact.status = "blocked";
1185
1717
  }
1186
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
+ : {};
1187
1728
  const result = envelope("init", flags, {
1188
1729
  status: "error",
1189
1730
  summary: error.message,
1190
1731
  actions: plan.actions,
1191
1732
  artifacts: plan.artifacts,
1192
1733
  errors: [error],
1193
- planned_manifest: {
1194
- path: "rigorloop.yaml",
1195
- content: plan.manifest,
1196
- },
1197
- planned_lockfile: plan.planned_lockfile,
1734
+ ...statePlan,
1198
1735
  });
1199
1736
  if (flags.json) {
1200
1737
  writeJson(result);
@@ -1204,18 +1741,27 @@ function writeValidationErrorResult(flags, plan, error) {
1204
1741
  return exitCodeForResult({ ...result, exit_class: "validation_failed" });
1205
1742
  }
1206
1743
 
1207
- async function archiveWorkForInit(flags, info) {
1208
- if (flags.dryRun) {
1744
+ async function archiveWorkForInit(flags, info, descriptor) {
1745
+ if (flags.dryRun && !flags.fromArchiveProvided) {
1209
1746
  return {};
1210
1747
  }
1211
1748
 
1212
1749
  const bundledMetadata = loadVerifiedBundledMetadata(info);
1213
1750
  if (bundledMetadata.blocker || bundledMetadata.error) {
1751
+ if (flags.dryRun) {
1752
+ return {};
1753
+ }
1214
1754
  return bundledMetadata;
1215
1755
  }
1216
1756
  const metadata = bundledMetadata.metadata;
1217
- const validation = validateMetadata(metadata, info);
1757
+ const validation = validateMetadata(metadata, info, descriptor);
1218
1758
  if (validation.blocker || validation.error) {
1759
+ if (
1760
+ flags.dryRun &&
1761
+ !["opencode-command-aliases-missing", "opencode-skills-only-compatibility-unmarked"].includes(validation.blocker?.code)
1762
+ ) {
1763
+ return {};
1764
+ }
1219
1765
  return validation;
1220
1766
  }
1221
1767
  const artifact = validation.artifact;
@@ -1223,17 +1769,31 @@ async function archiveWorkForInit(flags, info) {
1223
1769
  if (flags.fromArchiveProvided) {
1224
1770
  const archiveName = basename(flags.fromArchive);
1225
1771
  if (archiveName !== artifact.archive || !archiveName.includes(metadata.release.version)) {
1772
+ if (!archiveName.startsWith(`rigorloop-adapter-${descriptor.name}-`)) {
1773
+ return {
1774
+ error: {
1775
+ code: "adapter-archive-mismatch",
1776
+ message: `Local archive ${archiveName} is not a ${descriptor.displayName} adapter archive.`,
1777
+ path: flags.fromArchive,
1778
+ },
1779
+ artifact,
1780
+ };
1781
+ }
1226
1782
  return {
1227
1783
  blocker: metadataBlocker(
1228
1784
  "release-version-incompatible",
1229
1785
  `Local archive ${archiveName} is not compatible with ${metadata.release.version}.`,
1230
1786
  flags.fromArchive,
1231
- "Use the Codex adapter archive matching the installed CLI package version.",
1787
+ `Use the ${descriptor.displayName} adapter archive matching the installed CLI package version.`,
1232
1788
  ),
1233
1789
  };
1234
1790
  }
1791
+ // CR-M2-R2-F1: dry-run planning must use trusted metadata roots without reading or extracting archive bytes.
1792
+ if (flags.dryRun) {
1793
+ return { artifact };
1794
+ }
1235
1795
  const archiveBytes = readFileSync(resolve(process.cwd(), flags.fromArchive));
1236
- const inspected = inspectArchive(archiveBytes, artifact);
1796
+ const inspected = inspectArchive(archiveBytes, artifact, descriptor);
1237
1797
  if (inspected.error) {
1238
1798
  return { error: inspected.error, artifact };
1239
1799
  }
@@ -1258,29 +1818,29 @@ async function archiveWorkForInit(flags, info) {
1258
1818
  }
1259
1819
  try {
1260
1820
  archiveBytes = await fetchBytes(artifact.url);
1261
- } catch {
1821
+ } catch (error) {
1262
1822
  return {
1263
- blocker: metadataBlocker(
1264
- "release-unavailable",
1265
- "Official Codex adapter archive is unavailable.",
1266
- artifact.url,
1267
- "Retry later or use --from-archive with a compatible local archive.",
1268
- ),
1823
+ blocker: downloadFailureBlocker(error, artifact, descriptor, metadata),
1269
1824
  };
1270
1825
  }
1271
- const inspected = inspectArchive(archiveBytes, artifact);
1826
+ const inspected = inspectArchive(archiveBytes, artifact, descriptor);
1272
1827
  if (inspected.error) {
1273
1828
  return { error: inspected.error, artifact };
1274
1829
  }
1275
1830
  return { artifact, entries: inspected.entries, archiveHash: inspected.archiveHash, treeHash: inspected.treeHash };
1276
1831
  }
1277
1832
 
1278
- async function handleInit(flags) {
1279
- if (!flags.adapter) {
1280
- return invalidUsage("Missing required option: --adapter codex.", 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");
1281
1839
  }
1282
- if (flags.adapter !== "codex") {
1283
- return unsupportedAdapter(flags.adapter, flags);
1840
+ const target = initArgs[0];
1841
+ const descriptor = adapterDescriptor(target);
1842
+ if (!descriptor) {
1843
+ return unsupportedAdapter(target, flags);
1284
1844
  }
1285
1845
  if (flags.fromArchiveProvided && (!flags.fromArchive || flags.fromArchive.startsWith("--"))) {
1286
1846
  return invalidArchivePath("Missing required value for --from-archive.", flags);
@@ -1290,7 +1850,7 @@ async function handleInit(flags) {
1290
1850
  }
1291
1851
 
1292
1852
  const info = packageInfo();
1293
- const plan = buildInitPlan(flags);
1853
+ let plan = buildInitPlan(flags, descriptor);
1294
1854
  if (plan.errors.length > 0) {
1295
1855
  const result = envelope("init", flags, {
1296
1856
  status: "error",
@@ -1298,11 +1858,15 @@ async function handleInit(flags) {
1298
1858
  actions: plan.actions,
1299
1859
  artifacts: plan.artifacts,
1300
1860
  errors: plan.errors,
1301
- planned_manifest: {
1302
- path: "rigorloop.yaml",
1303
- content: plan.manifest,
1304
- },
1305
- 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
+ : {}),
1306
1870
  });
1307
1871
  if (flags.json) {
1308
1872
  writeJson(result);
@@ -1311,21 +1875,40 @@ async function handleInit(flags) {
1311
1875
  }
1312
1876
  return exitCodeForResult({ ...result, exit_class: "invalid_usage" });
1313
1877
  }
1314
- if (plan.blockers.length > 0) {
1878
+ const deferrableRootBlockers =
1879
+ descriptor.name === "opencode" &&
1880
+ plan.blockers.length > 0 &&
1881
+ plan.blockers.every((blocker) => String(blocker.path ?? "").startsWith(".opencode/commands"));
1882
+ if (plan.blockers.length > 0 && !deferrableRootBlockers) {
1315
1883
  return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
1316
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
+ }
1317
1891
 
1318
- const archiveWork = await archiveWorkForInit(flags, info);
1319
- if (archiveWork.artifact) {
1320
- plan.source.artifact = archiveWork.artifact;
1321
- plan.planned_lockfile = plannedLockfile(plan.info, plan.source, plan.manifest);
1892
+ const archiveWork = await archiveWorkForInit(flags, info, descriptor);
1893
+ if (archiveWork.artifact && !archiveWork.blocker && !archiveWork.error) {
1894
+ plan = buildInitPlan(flags, descriptor, archiveWork.artifact);
1895
+ if (plan.errors.length > 0) {
1896
+ return writeValidationErrorResult(flags, plan, plan.errors[0]);
1897
+ }
1898
+ if (plan.blockers.length > 0) {
1899
+ return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
1900
+ }
1322
1901
  }
1323
1902
  if (archiveWork.entries) {
1324
1903
  const conflict = generatedOutputConflictBlocker(archiveWork.entries);
1325
1904
  if (conflict) {
1326
1905
  return writeBlockedResult(flags, plan, conflict.message, [conflict], "mutation_conflict");
1327
1906
  }
1328
- const drift = lockfileDriftBlocker(currentCodexLockfileEntry());
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;
1329
1912
  if (drift) {
1330
1913
  return writeBlockedResult(
1331
1914
  flags,
@@ -1335,11 +1918,11 @@ async function handleInit(flags) {
1335
1918
  drift.code === "overwrite-refused" ? "mutation_conflict" : "blocked",
1336
1919
  );
1337
1920
  }
1338
- const installedTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, { allowMissingOrEmpty: true });
1921
+ const installedTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, descriptor, { allowMissingOrEmpty: true });
1339
1922
  if (installedTree.error) {
1340
1923
  return writeValidationErrorResult(flags, plan, installedTree.error);
1341
1924
  }
1342
- addArchiveActions(plan, archiveWork.entries);
1925
+ addArchiveActions(plan, archiveWork.entries, descriptor);
1343
1926
  }
1344
1927
 
1345
1928
  if (archiveWork.blocker) {
@@ -1353,7 +1936,7 @@ async function handleInit(flags) {
1353
1936
  }
1354
1937
 
1355
1938
  if (!flags.dryRun) {
1356
- const manifestAction = plan.actions.find((action) => action.path === "rigorloop.yaml");
1939
+ const manifestAction = flags.writeState ? plan.actions.find((action) => action.path === "rigorloop.yaml") : undefined;
1357
1940
  const directoryActions = plan.actions.filter((action) => action.type === "create-dir" && action.status === "pending");
1358
1941
  for (const directoryAction of directoryActions) {
1359
1942
  mkdirSync(resolve(process.cwd(), directoryAction.path));
@@ -1370,7 +1953,7 @@ async function handleInit(flags) {
1370
1953
  const pendingCopyPaths = new Set(
1371
1954
  plan.actions.filter((action) => action.type === "copy" && action.status === "pending").map((action) => action.path),
1372
1955
  );
1373
- writeArchiveEntries(archiveWork.entries.filter((entry) => pendingCopyPaths.has(entry.name)));
1956
+ writeArchiveEntries(archiveWork.entries.filter((entry) => pendingCopyPaths.has(entry.name)), descriptor);
1374
1957
  for (const action of plan.actions.filter((action) => action.type === "copy" && action.status === "pending")) {
1375
1958
  action.status = "done";
1376
1959
  plan.artifacts.find((artifact) => artifact.path === action.path).status = "created";
@@ -1388,11 +1971,15 @@ async function handleInit(flags) {
1388
1971
  partial_state: "scaffold files may have been written; adapter files may be incomplete.",
1389
1972
  },
1390
1973
  ],
1391
- planned_manifest: {
1392
- path: "rigorloop.yaml",
1393
- content: plan.manifest,
1394
- },
1395
- 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
+ : {}),
1396
1983
  });
1397
1984
  if (flags.json) {
1398
1985
  writeJson(result);
@@ -1402,12 +1989,12 @@ async function handleInit(flags) {
1402
1989
  return exitCodeForResult({ ...result, exit_class: "internal" });
1403
1990
  }
1404
1991
  }
1405
- if (archiveWork.entries) {
1992
+ if (flags.writeState && archiveWork.entries) {
1406
1993
  const lockfileAction = plan.actions.find((action) => action.path === LOCKFILE_PATH);
1407
1994
  const lockfileArtifact = plan.artifacts.find((artifact) => artifact.path === LOCKFILE_PATH);
1408
1995
  if (lockfileAction?.status === "pending") {
1409
1996
  const lockfilePreviouslyExists = existsSync(resolve(process.cwd(), LOCKFILE_PATH));
1410
- const verifiedInstalledTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact);
1997
+ const verifiedInstalledTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, descriptor);
1411
1998
  if (verifiedInstalledTree.error) {
1412
1999
  return writeValidationErrorResult(flags, plan, verifiedInstalledTree.error);
1413
2000
  }
@@ -1416,15 +2003,15 @@ async function handleInit(flags) {
1416
2003
  plan.source,
1417
2004
  plan.manifest,
1418
2005
  archiveWork.artifact,
1419
- archiveWork.artifact.tree_sha256,
1420
- verifiedInstalledTree.expectedFileCount,
2006
+ verifiedInstalledTree.rootHashes,
2007
+ descriptor,
1421
2008
  );
1422
2009
  writeFileSync(resolve(process.cwd(), LOCKFILE_PATH), serializeLockfile(lockfile), "utf8");
1423
2010
  plan.planned_lockfile = lockfile;
1424
2011
  lockfileAction.status = "done";
1425
2012
  lockfileAction.reason = lockfilePreviouslyExists
1426
- ? "Updated durable lockfile for verified Codex adapter install."
1427
- : "Wrote durable lockfile for verified Codex adapter install.";
2013
+ ? `Updated durable lockfile for verified ${descriptor.displayName} adapter install.`
2014
+ : `Wrote durable lockfile for verified ${descriptor.displayName} adapter install.`;
1428
2015
  if (lockfileArtifact) {
1429
2016
  lockfileArtifact.status = lockfilePreviouslyExists ? "updated" : "created";
1430
2017
  }
@@ -1432,22 +2019,29 @@ async function handleInit(flags) {
1432
2019
  }
1433
2020
  }
1434
2021
 
1435
- const warnings = [];
2022
+ const warnings = initWarnings(descriptor, archiveWork.artifact);
1436
2023
  const result = envelope("init", flags, {
1437
2024
  status: warnings.length > 0 ? "warning" : "success",
1438
2025
  summary: flags.dryRun
1439
2026
  ? "RigorLoop init dry run completed. No files were written."
1440
2027
  : archiveWork.entries
1441
- ? "RigorLoop initialized with verified Codex adapter files."
1442
- : "RigorLoop initialized with Codex scaffold.",
2028
+ ? `RigorLoop initialized with verified ${descriptor.displayName} target support.`
2029
+ : `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
1443
2030
  actions: plan.actions,
1444
2031
  artifacts: plan.artifacts,
1445
2032
  warnings,
1446
- planned_manifest: {
1447
- path: "rigorloop.yaml",
1448
- content: plan.manifest,
1449
- },
1450
- 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
+ : {}),
1451
2045
  });
1452
2046
 
1453
2047
  if (flags.json) {
@@ -1457,10 +2051,13 @@ async function handleInit(flags) {
1457
2051
  ? ["RigorLoop init dry run completed.", "No files were written."]
1458
2052
  : [
1459
2053
  archiveWork.entries
1460
- ? "RigorLoop initialized with verified Codex adapter files."
1461
- : "RigorLoop initialized with Codex scaffold.",
1462
- archiveWork.entries ? "rigorloop.lock was written." : "No adapter files were installed.",
2054
+ ? `RigorLoop initialized with verified ${descriptor.displayName} target support.`
2055
+ : `RigorLoop initialized with ${descriptor.displayName} scaffold.`,
2056
+ flags.writeState ? "rigorloop.yaml and rigorloop.lock were written." : "State files were not written; use --write-state to write them.",
1463
2057
  ];
2058
+ for (const warning of warnings) {
2059
+ lines.push(`warning ${warning.code}: ${warning.message}`);
2060
+ }
1464
2061
  writeHuman(`${lines.join("\n")}\n`, flags);
1465
2062
  }
1466
2063
  return exitCodeForResult({ ...result, exit_class: "success" });
@@ -1483,7 +2080,7 @@ async function main() {
1483
2080
  return handleVersion(flags);
1484
2081
  }
1485
2082
  if (command === "init") {
1486
- return handleInit(flags);
2083
+ return handleInit(flags, positional.slice(1));
1487
2084
  }
1488
2085
  if (command === "new-change") {
1489
2086
  return handleNewChange(rawArgs.slice(rawArgs.indexOf("new-change") + 1));