@inixiative/config 0.5.79 → 0.5.81

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
@@ -43,6 +43,7 @@ One version of `@inixiative/config` names one coherent ecosystem state:
43
43
  - `ecosystem` — the primitives lane's blessed, mutually-verified `@inixiative/*` set, plus this package
44
44
  - `agentic` — the agentic lane's blessed set
45
45
  - `consumers` — private repos that follow the blessed sets but never publish, keyed by GitHub `owner/repo`: `lanes` names the lanes each consumes, `upstream` the repo it syncs from
46
+ - `candidates` — published lane versions newer than their blessed one, waiting for every dependent to adopt them (see `train`); absent when there are none
46
47
 
47
48
  ```jsonc
48
49
  "consumers": {
@@ -71,7 +72,7 @@ bunx @inixiative/config ports [project]
71
72
 
72
73
  `ports` prints each project's local ports from `ports.json` (also exported as `@inixiative/config/ports.json`). A project's port for a service is the service's usual default plus 100 times the project's block, so no two projects collide and none sits on a default: Archive (block 7) serves HTTP on 4700 with Postgres on 6132; Kingdom (block 2) has web 3200, API 8200, Postgres 5632, Redis 6579. Reserved ports (macOS AirPlay on 5000 and 7000) are never assigned. Give a new project the next free block, and a new kind of service its usual default; the tests fail on any collision.
73
74
 
74
- `check` is read-only and exits non-zero on drift — run it in CI after a frozen-lockfile install. It verifies toolchain pins — declared once, at the root, pinned exactly: a workspace member declaring one is an error, and `bun.lock` must resolve each only to its pin (no second copy) — toolchain peer ranges (they must admit the pin and no older major; `sync` writes `>=<pin major>.<pin minor>.0`), `"latest"` ranges, `packageManager`/`.bun-version`, legacy `bun.lockb`, a committed (tracked, un-ignored) `bun.lock`, required scripts (`check`/`typecheck`/`lint`/`test`), lefthook (dep + `lefthook.yml` extending the shared hooks + `prepare` script; git repos only), stub `extends`, presence of this package, and for every ecosystem dependency — in the root or any workspace member, of either lane — that the declared range admits the blessed version and the lockfile actually resolves to it (the stale-lockfile class). A range that does not admit the blessed version is rewritten to `^<blessed>`, except an exact pin, which stays exact and moves to the blessed version. An npm alias (`"@inixiative/session-archive": "npm:@inixiative/archive@^0.2.1"`) is held to its target's blessed version and rewritten as an alias; `workspace:`, `file:` and `link:` ranges are a repo linking itself and are left alone.
75
+ `check` is read-only and exits non-zero on drift — run it in CI after a frozen-lockfile install. It verifies toolchain pins — declared once, at the root, pinned exactly: a workspace member declaring one is an error, and `bun.lock` must resolve each only to its pin (no second copy) — toolchain peer ranges (they must admit the pin and no older major; `sync` writes `>=<pin major>.<pin minor>.0`), `"latest"` ranges, `packageManager`/`.bun-version`, legacy `bun.lockb`, a committed (tracked, un-ignored) `bun.lock`, required scripts (`check`/`typecheck`/`lint`/`test`), lefthook (dep + `lefthook.yml` extending the shared hooks + `prepare` script; git repos only), stub `extends`, presence of this package, and for every ecosystem dependency — in the root or any workspace member, of either lane — that the declared range admits the blessed version and the lockfile actually resolves to it (the stale-lockfile class). A package with a candidate also accepts the candidate and anything between: a range admitting either passes, and so does a lock from the blessed version up to the candidate. A range that does not admit the blessed version is rewritten to `^<blessed>`, except an exact pin, which stays exact and moves to the blessed version. An npm alias (`"@inixiative/session-archive": "npm:@inixiative/archive@^0.2.1"`) is held to its target's blessed version and rewritten as an alias; `workspace:`, `file:` and `link:` ranges are a repo linking itself and are left alone.
75
76
 
76
77
  `sync` applies every fix (deleting member toolchain entries and their stale member links), re-locks via `bun install` (`--force` when a second toolchain copy survives), and runs `bun update` on stale ecosystem entries in the directory that declares them. It refuses to run on a dirty working tree without `--force`. The react preset is inferred from a `react` dependency; override with `--preset`. Missing scripts are filled with defaults; existing script bodies are never touched.
77
78
 
@@ -79,11 +80,23 @@ bunx @inixiative/config ports [project]
79
80
 
80
81
  `train` is the release cascade, run from a git checkout of this repo; it refuses to train from a copy that isn't one (a bunx or npm cache). One train at a time per machine: it takes one machine-wide lock, `~/.inixiative/train.lock`, shared by every clone, worktree and lane, and refuses to start while another train holds it, naming the holder and the lock path. The lock is created atomically and records the holder's pid, hostname and process start time. A lock is stale, and taken over, when its process is gone, its pid now belongs to a different process, or the file can't be parsed. A lock from another host is never judged stale; if its train is truly gone, delete the file. The lock is released on exit and on SIGINT, SIGTERM or SIGHUP. `--dry-run` takes no lock. It walks every lane in order (primitives, then agentic), or only the lane `--lane` names. Origin is the truth: it refuses to run from a config checkout behind origin, and it fast-forwards clean checkouts to origin before working (dirty or diverged repos still skip). It only ships the default branch: a checkout on a feature branch is skipped whole — never re-locked, committed to, or published from.
81
82
 
82
- Lanes stand alone. A lane with a package missing a checkout, or claimed by ambiguous checkouts, does not run; a lane whose check, commit or publish fails stops there and leaves its BOM entries as they were. Neither holds back the other lane. Within a lane it walks checkouts in dependency order; per checkout it: bumps ecosystem ranges to the blessed sets → re-locks → runs the repo's `check` → commits only its own changes (`package.json` files + `bun.lock`) → publishes each lane package whose local version is ahead of npm, in dependency order, and waits until the registry serves it (up to 15 minutes, since npm holds new versions for processing) → records it in its lane's section of the BOM. A root package publishes with `npm publish`; a workspace member is packed with `bun pm pack`, which rewrites `workspace:` ranges to the versions being released, and the tarball is published with `npm publish`, so OTP prompts pass through either way. A package already on npm at its local version but not yet blessed — published by hand, or by a train whose lane later failed — is blessed on the next run. Likewise, an `@inixiative/config` version committed ahead of npm (a release whose publish failed) is published by the next run, even when it blesses nothing new.
83
+ Lanes stand alone. A lane with a package missing a checkout, or claimed by ambiguous checkouts, does not run; a lane whose own check (not one against candidates, below), commit or publish fails stops there: what it already published stays a candidate, and none of its candidates is blessed that run. Neither holds back the other lane. Within a lane it walks checkouts in dependency order; per checkout it: bumps ecosystem ranges to the blessed sets with their candidates in place → re-locks → runs the repo's `check` → commits only its own changes (`package.json` files + `bun.lock`) → publishes each lane package whose local version is ahead of npm, in dependency order, and waits until the registry serves it (up to 15 minutes, since npm holds new versions for processing) → records it as a candidate in the BOM.
83
84
 
84
- Consumers run once, after every selected lane, upstreams first, against the full blessed set: a consumer of both lanes ends coherent with both whichever lane ran. They get the same bump → re-lock → `check`, never a publish, and their changes go through review: the train commits them on a `train/ecosystem-sync-<date>` branch and leaves the checkout there until it is merged.
85
+ Blessing is two-phase, so a dependent that needs work never deadlocks a release:
85
86
 
86
- When anything was blessed it then ships this package last, so the BOM names the new state: bumps its own patch version if npm already has the current one, blesses that version in the BOM, brings the `test/fixtures` consumers onto the blessed set, runs `check`, commits, publishes. Then every checkout the train re-locked (lane packages on their default branch, consumers on their review branch) is re-locked onto that new `@inixiative/config` and committed, so none is left pinning the previous release. It never pushes unless asked: pass `--push` to push every repo it committed to and open a PR for each consumer branch; otherwise it prints the commands. `--dry-run` walks the same plan and prints each step it would take (range rewrites, re-locks and checks, commits, review branches, publishes, BOM blessings, pushes) without running any of them; it only fetches and reads git state and `npm view`.
87
+ 1. **Candidate.** A published version is recorded under `candidates`, and the config release that ends the train carries it, so `check` in any repo's CI accepts it from then on.
88
+ 2. **Adopt.** A repo (lane checkout or consumer) whose check fails against the candidates it was moved onto doesn't stop the train. Its checkout is put back as it was, and the train opens `train/adopt-<pkg>-<version>` in a worktree (with the repo's root `.env*` files copied in): the same bump and re-lock, then the repo's `train:adopt` script if its package.json declares one (Foundry's re-records cassettes with `bun run test:live`), all committed, and re-locked onto the new config release so the PR's CI judges it against the candidates. A lane checkout that adopts publishes nothing this run. A consumer that passes against candidates gets the same branch name: its default branch is still on the previous versions until the branch merges. If the adoption's commit is refused (its hooks typecheck), the branch is deleted and the summary says to adopt by hand.
89
+ 3. **Promote.** At the end of each run a candidate is blessed (moved into its lane's section) unless something holds it: its lane stopped; a repo has an adoption branch for it that is not yet on its default branch (opened this run, unmerged, or committed but never pushed); or a repo that declares it was not walked to an outcome this run — another lane's checkout, a skipped or failed one, a consumer without a checkout. A later train finds a waiting adoption branch and opens nothing new; once it merges, the repo is on the candidate and the train promotes it. A candidate every dependent already accepts is blessed in the run that published it. An adoption PR closed without merging holds its candidates until its branch is deleted.
90
+
91
+ The summary lists what was blessed, every candidate with what holds it, and each pending adoption branch with its PR when `gh` can find one.
92
+
93
+ A root package publishes with `npm publish`; a workspace member is packed with `bun pm pack`, which rewrites `workspace:` ranges to the versions being released, and the tarball is published with `npm publish`, so OTP prompts pass through either way. A package already on npm at its local version but neither blessed nor a candidate — published by hand, or by a train whose lane later failed — becomes a candidate on the next run. Likewise, an `@inixiative/config` version committed ahead of npm (a release whose publish failed) is published by the next run, even when it blesses nothing new.
94
+
95
+ Consumers run once, after every selected lane, upstreams first, against the full blessed set: a consumer of both lanes ends coherent with both whichever lane ran. They get the same bump → re-lock → `check`, never a publish, and their changes go through review: the train commits them on a `train/ecosystem-sync-<date>` branch, in a worktree, and puts the checkout back on its default branch, clean.
96
+
97
+ A train never leaves a checkout dirty or off its default branch. It only works in a checkout it found clean, so a checkout that then fails is reset to HEAD (tracked and untracked files; ignored ones such as node_modules and `.env` stay) and re-installed from its lockfile. Every branch it makes is committed in a temporary worktree, removed when the train exits.
98
+
99
+ When anything was recorded or blessed it then ships this package last, so the BOM names the new state: bumps its own patch version if npm already has the current one, blesses that version in the BOM, brings the `test/fixtures` consumers onto the blessed set, runs `check`, commits, publishes. Then every checkout the train re-locked (lane packages on their default branch, consumer and adoption branches in their worktrees) is re-locked onto that new `@inixiative/config` and committed, so none is left pinning the previous release. It never pushes unless asked: pass `--push` to push every repo it committed to and open a PR for each consumer and adoption branch; otherwise it prints the commands. `--dry-run` walks the same plan and prints each step it would take (range rewrites, re-locks and checks, commits, review branches, publishes, BOM blessings, pushes) without running any of them; it only fetches and reads git state and `npm view`.
87
100
 
88
101
  Each command accepts only its own flags. An unknown or misplaced flag (`--dryrun`, `--push` on `sync`) is a usage error and exits 2 before anything runs.
89
102
 
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { spawnSync as spawnSync2 } from "child_process";
5
5
  import { existsSync as existsSync2, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
6
6
  import { hostname as hostname2, tmpdir } from "os";
7
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
7
+ import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
8
8
 
9
9
  // src/args.ts
10
10
  import { parseArgs } from "util";
@@ -44,6 +44,33 @@ var blessedVersions = (manifest2) => ({
44
44
  ...manifest2.ecosystem,
45
45
  ...manifest2.agentic
46
46
  });
47
+ var laneOf = (manifest2, name) => LANES.find((lane2) => name in lanePackages(manifest2, lane2)) ?? null;
48
+ var adoptionTarget = (manifest2) => {
49
+ const candidates = manifest2.candidates ?? {};
50
+ const overlay = (section) => Object.fromEntries(
51
+ Object.entries(section).map(([name, version]) => [name, candidates[name] ?? version])
52
+ );
53
+ return {
54
+ ...manifest2,
55
+ ecosystem: overlay(manifest2.ecosystem),
56
+ agentic: overlay(manifest2.agentic ?? {})
57
+ };
58
+ };
59
+ var promoteCandidates = (manifest2, names) => {
60
+ const promoted = [];
61
+ for (const name of names) {
62
+ const version = manifest2.candidates?.[name];
63
+ const lane2 = laneOf(manifest2, name);
64
+ if (!version || !lane2 || !manifest2.candidates) continue;
65
+ laneSection(manifest2, lane2)[name] = version;
66
+ delete manifest2.candidates[name];
67
+ promoted.push(`${name}@${version}`);
68
+ }
69
+ if (manifest2.candidates && Object.keys(manifest2.candidates).length === 0) {
70
+ delete manifest2.candidates;
71
+ }
72
+ return promoted;
73
+ };
47
74
  var consumersFor = (manifest2, lanes2) => {
48
75
  const consumers = manifest2.consumers ?? {};
49
76
  const selected = Object.keys(consumers).filter((repo) => consumers[repo].lanes.some((lane2) => lanes2.includes(lane2))).sort();
@@ -296,6 +323,10 @@ var orderBy = (items, key, deps) => {
296
323
  }
297
324
  return order;
298
325
  };
326
+ var declaredDependencies = (dir2) => {
327
+ const pkg = readJson(join(dir2, "package.json"));
328
+ return pkg ? dependencyNames(dir2, ["package.json", ...workspacePackagePaths(dir2, pkg)]) : /* @__PURE__ */ new Set();
329
+ };
299
330
  var publishOrder = (checkout) => {
300
331
  const names = new Set(checkout.packages.map((entry) => entry.name));
301
332
  const deps = new Map(
@@ -681,6 +712,7 @@ function inspect(dir2, manifest2, presetOverride) {
681
712
  }
682
713
  }
683
714
  const blessedSet = blessedVersions(manifest2);
715
+ const candidateSet = manifest2.candidates ?? {};
684
716
  const staleKeys = /* @__PURE__ */ new Set();
685
717
  for (const target of rangeTargets) {
686
718
  for (const field of DEP_FIELDS) {
@@ -693,14 +725,19 @@ function inspect(dir2, manifest2, presetOverride) {
693
725
  const blessed = blessedSet[name];
694
726
  if (!blessed || name === target.pkg.name || !deps) continue;
695
727
  const rewrite = (next) => name === key ? next : `npm:${name}@${next}`;
696
- const ok = admits(range, blessed);
728
+ const candidate = candidateSet[name];
729
+ const okBlessed = admits(range, blessed);
730
+ const okCandidate = candidate ? admits(range, candidate) : false;
731
+ const ok = okBlessed === true || okCandidate === true ? true : okBlessed;
732
+ const accepted = candidate === blessed ? `candidate ${candidate}` : `blessed ${blessed}${candidate ? ` or candidate ${candidate}` : ""}`;
697
733
  if (ok === false) {
698
734
  const next = /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(range) ? blessed : `^${blessed}`;
699
735
  findings.push({
700
736
  level: "error",
701
737
  kind: "ecosystem-range",
702
738
  name,
703
- message: `${target.label}${key} ${field} range ${spec} does not admit blessed ${blessed} \u2192 ${rewrite(next)}`,
739
+ target: name,
740
+ message: `${target.label}${key} ${field} range ${spec} does not admit ${accepted} \u2192 ${rewrite(next)}`,
704
741
  fix: () => {
705
742
  deps[key] = rewrite(next);
706
743
  target.mark();
@@ -710,28 +747,31 @@ function inspect(dir2, manifest2, presetOverride) {
710
747
  if (ok === null) {
711
748
  findings.push({
712
749
  level: "warn",
713
- message: `${target.label}${key} range ${spec} not understood; verify manually against ${blessed}`
750
+ message: `${target.label}${key} range ${spec} not understood; verify manually against ${accepted}`
714
751
  });
715
752
  }
716
753
  if (!lockText || field === "peerDependencies" || staleKeys.has(key)) continue;
717
754
  const locked = lockedVersion(lockText, key, name);
755
+ const lockAccepted = locked === blessed || !!locked && locked === candidate || !!locked && !!candidate && compare(locked, blessed) >= 0 && compare(locked, candidate) <= 0;
718
756
  if (locked && admits(range, locked) === false) {
719
757
  staleKeys.add(key);
720
758
  findings.push({
721
759
  level: "error",
722
760
  kind: "stale-lock",
723
761
  name: key,
762
+ target: name,
724
763
  path: target.path,
725
764
  message: `${target.label}${key} locked at ${locked}, violating declared ${spec} (stale lockfile) \u2192 re-lock`
726
765
  });
727
- } else if (locked && locked !== blessed) {
766
+ } else if (locked && !lockAccepted) {
728
767
  staleKeys.add(key);
729
768
  findings.push({
730
769
  level: "error",
731
770
  kind: "stale-lock",
732
771
  name: key,
772
+ target: name,
733
773
  path: target.path,
734
- message: `${target.label}${key} locked at ${locked}, blessed is ${blessed} \u2192 re-lock`
774
+ message: `${target.label}${key} locked at ${locked}, ${accepted.replace(/^(blessed|candidate) /, "$1 is ")} \u2192 re-lock`
735
775
  });
736
776
  }
737
777
  }
@@ -1119,6 +1159,9 @@ if (command === "scan") {
1119
1159
  const missing = [...plan.lanes.flatMap((lane2) => lane2.missing), ...plan.missingConsumers];
1120
1160
  const ambiguous = [...plan.lanes.flatMap((lane2) => lane2.ambiguous), ...plan.ambiguousConsumers];
1121
1161
  const drifted = [];
1162
+ for (const [name, version] of Object.entries(manifest.candidates ?? {})) {
1163
+ console.log(`candidate: ${name}@${version} (blessed ${blessedVersions(manifest)[name]})`);
1164
+ }
1122
1165
  for (const checkout of checkouts) {
1123
1166
  console.log(`
1124
1167
  ${checkout.name} \u2014 ${checkout.dir} (${laneLabel(checkout)})`);
@@ -1152,6 +1195,8 @@ scanned ${checkouts.length} repos (${lanes.join(" + ")}): ${drifted.length === 0
1152
1195
  }
1153
1196
  var Abort = class extends Error {
1154
1197
  };
1198
+ var CheckFailed = class extends Abort {
1199
+ };
1155
1200
  if (command === "train") {
1156
1201
  const effect = (description, planned, run) => {
1157
1202
  if (!dryRun) return run();
@@ -1179,9 +1224,9 @@ if (command === "train") {
1179
1224
  checkout: packageRoot
1180
1225
  });
1181
1226
  if (!lock.ok) {
1182
- const { pid, hostname: host, startedAt, lanes: held, root, checkout } = lock.heldBy;
1227
+ const { pid, hostname: host, startedAt, lanes: held2, root, checkout } = lock.heldBy;
1183
1228
  console.error(
1184
- `\u2717 another train is running (pid ${pid} on ${host}, lanes ${held.join(" + ")}, root ${root}, from ${checkout}, since ${startedAt}) \u2014 wait for it to finish; one train at a time. If that train is truly gone, delete ${lock.path}`
1229
+ `\u2717 another train is running (pid ${pid} on ${host}, lanes ${held2.join(" + ")}, root ${root}, from ${checkout}, since ${startedAt}) \u2014 wait for it to finish; one train at a time. If that train is truly gone, delete ${lock.path}`
1185
1230
  );
1186
1231
  process.exit(1);
1187
1232
  }
@@ -1212,12 +1257,21 @@ if (command === "train") {
1212
1257
  console.error(`\u2717 no ecosystem repos found under ${dir}`);
1213
1258
  process.exit(1);
1214
1259
  }
1215
- const blessed = [];
1260
+ const recorded = [];
1216
1261
  const skipped = [];
1217
1262
  const failed = [];
1263
+ const failedLanes = /* @__PURE__ */ new Set();
1218
1264
  const committed = /* @__PURE__ */ new Set();
1219
1265
  const reviewBranches = [];
1220
- let bomDirty = false;
1266
+ const waiting = [];
1267
+ const walked = /* @__PURE__ */ new Set();
1268
+ const worktrees = [];
1269
+ process.on("exit", () => {
1270
+ for (const { repo, path } of worktrees.splice(0)) {
1271
+ git(repo, "worktree", "remove", "--force", path);
1272
+ }
1273
+ });
1274
+ const target = () => adoptionTarget(manifest);
1221
1275
  const ready = (checkout) => {
1222
1276
  const porcelain = git(checkout.dir, "status", "--porcelain");
1223
1277
  if (porcelain.status !== 0) {
@@ -1263,14 +1317,18 @@ if (command === "train") {
1263
1317
  return true;
1264
1318
  };
1265
1319
  const bumpRanges = (checkout) => {
1266
- const inspection = inspect(checkout.dir, manifest);
1320
+ const inspection = inspect(checkout.dir, target());
1267
1321
  const bumps = inspection.findings.filter((finding) => finding.kind === "ecosystem-range");
1268
1322
  for (const bump of bumps) console.log(` ${bump.message}`);
1269
1323
  if (bumps.length > 0) {
1270
- effect(`rewrite ${bumps.length} ecosystem range(s) to the blessed set`, void 0, () => {
1271
- for (const bump of bumps) bump.fix?.();
1272
- inspection.flush();
1273
- });
1324
+ effect(
1325
+ `rewrite ${bumps.length} ecosystem range(s) to the blessed set and its candidates`,
1326
+ void 0,
1327
+ () => {
1328
+ for (const bump of bumps) bump.fix?.();
1329
+ inspection.flush();
1330
+ }
1331
+ );
1274
1332
  }
1275
1333
  return {
1276
1334
  inspection,
@@ -1284,32 +1342,170 @@ if (command === "train") {
1284
1342
  }
1285
1343
  for (const { cwd, names } of staleUpdates(
1286
1344
  checkout.dir,
1287
- inspect(checkout.dir, manifest).findings
1345
+ inspect(checkout.dir, target()).findings
1288
1346
  )) {
1289
1347
  spawnSync2("bun", ["update", ...names], { cwd, stdio: "inherit" });
1290
1348
  }
1291
1349
  if (check && spawnSync2("bun", ["run", "check"], { cwd: checkout.dir, stdio: "inherit" }).status !== 0) {
1292
- throw new Abort(`check failed in ${checkout.name}`);
1350
+ throw new CheckFailed(`check failed in ${checkout.name}`);
1293
1351
  }
1294
1352
  });
1295
1353
  const followers = [];
1296
- const commitOwned = (checkout, packagePaths, changes) => {
1354
+ const restore = (checkout) => effect(`restore ${checkout.name} to HEAD`, void 0, () => {
1355
+ if (git(checkout.dir, "status", "--porcelain").stdout.trim() === "") return;
1356
+ git(checkout.dir, "reset", "--quiet", "--hard", "HEAD");
1357
+ git(checkout.dir, "clean", "--quiet", "-fd");
1358
+ spawnSync2("bun", ["install", "--frozen-lockfile"], { cwd: checkout.dir, stdio: "inherit" });
1359
+ });
1360
+ const movesOf = (checkout) => {
1361
+ const candidates2 = manifest.candidates ?? {};
1362
+ const names = inspect(checkout.dir, target()).findings.flatMap(
1363
+ (finding) => (finding.kind === "ecosystem-range" || finding.kind === "stale-lock") && finding.target && finding.target in candidates2 ? [finding.target] : []
1364
+ );
1365
+ return [...new Set(names)].sort();
1366
+ };
1367
+ const adoptionBranch = (moving) => `train/adopt-${moving.map((name) => `${name.replace(/^@inixiative\//, "")}-${manifest.candidates?.[name]}`).join("-")}`;
1368
+ const ahead = (cwd, ref) => {
1369
+ if (git(cwd, "rev-parse", "--verify", "--quiet", ref).status !== 0) return null;
1370
+ return Number(git(cwd, "rev-list", "--count", `HEAD..${ref}`).stdout.trim());
1371
+ };
1372
+ const pendingAdoption = (checkout, moving) => {
1373
+ if (moving.length === 0) return false;
1374
+ const branch = adoptionBranch(moving);
1375
+ const remote = ahead(checkout.dir, `refs/remotes/origin/${branch}`);
1376
+ const local = ahead(checkout.dir, `refs/heads/${branch}`);
1377
+ if (!remote && !local) return false;
1378
+ const state = remote ? "unmerged" : "unpushed";
1379
+ console.log(
1380
+ `\u23F3 adopting ${moving.join(", ")} on ${branch} \u2014 ${state === "unmerged" ? "waiting for it to merge" : "never pushed: push it and open its PR"}`
1381
+ );
1382
+ waiting.push({ name: checkout.name, dir: checkout.dir, moving, state, branch });
1383
+ walked.add(checkout.dir);
1384
+ return true;
1385
+ };
1386
+ const worktree = (checkout, branch) => {
1387
+ const path = join2(mkdtempSync(join2(tmpdir(), "inixiative-train-")), basename(checkout.dir));
1388
+ if (git(checkout.dir, "worktree", "add", "--quiet", "-b", branch, path, "HEAD").status !== 0) {
1389
+ throw new Abort(`could not create ${branch} in ${checkout.name}`);
1390
+ }
1391
+ worktrees.push({ repo: checkout.dir, path });
1392
+ for (const name of readdirSync2(checkout.dir)) {
1393
+ if (/^\.env(\..+)?$/.test(name) && !existsSync2(join2(path, name))) {
1394
+ writeFileSync2(join2(path, name), readFileSync2(join2(checkout.dir, name)));
1395
+ }
1396
+ }
1397
+ return { ...checkout, dir: path };
1398
+ };
1399
+ const dropBranch = (checkout, branch) => {
1400
+ const entry = worktrees.find(
1401
+ (tree) => tree.repo === checkout.dir && currentBranch(tree.path) === branch
1402
+ );
1403
+ if (entry) {
1404
+ git(checkout.dir, "worktree", "remove", "--force", entry.path);
1405
+ worktrees.splice(worktrees.indexOf(entry), 1);
1406
+ }
1407
+ git(checkout.dir, "branch", "-D", branch);
1408
+ };
1409
+ const defaultBranch = (cwd) => offDefaultBranch(cwd)?.main ?? currentBranch(cwd);
1410
+ const reviewBranch = (checkout, packagePaths, moving) => {
1411
+ const branch = moving.length > 0 ? adoptionBranch(moving) : freeBranch(checkout.dir, `train/ecosystem-sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
1412
+ const base = defaultBranch(checkout.dir);
1413
+ const opened = effect(
1414
+ `create review branch ${branch} in ${checkout.name} (in a worktree; the checkout stays on ${base})`,
1415
+ true,
1416
+ () => {
1417
+ const branchCheckout = worktree(checkout, branch);
1418
+ for (const path of [...packagePaths, "bun.lock"]) {
1419
+ if (existsSync2(join2(checkout.dir, path))) {
1420
+ writeFileSync2(join2(branchCheckout.dir, path), readFileSync2(join2(checkout.dir, path)));
1421
+ }
1422
+ }
1423
+ restore(checkout);
1424
+ try {
1425
+ relockAndCheck(branchCheckout, false);
1426
+ const message = moving.length > 0 ? `chore: adopt ${moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ")}` : void 0;
1427
+ if (!commitOwned(branchCheckout, packagePaths, true, message)) {
1428
+ dropBranch(checkout, branch);
1429
+ return false;
1430
+ }
1431
+ } catch (error) {
1432
+ dropBranch(checkout, branch);
1433
+ throw error;
1434
+ }
1435
+ followers.push({ checkout: branchCheckout, onBranch: true });
1436
+ console.log(`committed on ${branch} for review; ${checkout.name} stays on ${base}`);
1437
+ return true;
1438
+ }
1439
+ );
1440
+ if (!opened) return;
1441
+ reviewBranches.push({ dir: checkout.dir, branch, base });
1442
+ if (moving.length > 0) {
1443
+ waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
1444
+ }
1445
+ };
1446
+ const adopt = (checkout, moving) => {
1447
+ const branch = adoptionBranch(moving);
1448
+ const base = defaultBranch(checkout.dir);
1449
+ const label = moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ");
1450
+ const script = checkout.pkg.scripts?.["train:adopt"];
1451
+ try {
1452
+ effect(
1453
+ `open ${branch} in ${checkout.name}: re-lock onto ${label}${script ? ", run train:adopt" : ""}, commit`,
1454
+ void 0,
1455
+ () => {
1456
+ const branchCheckout = worktree(checkout, branch);
1457
+ try {
1458
+ bumpRanges(branchCheckout);
1459
+ relockAndCheck(branchCheckout, false);
1460
+ if (script && spawnSync2("bun", ["run", "train:adopt"], {
1461
+ cwd: branchCheckout.dir,
1462
+ stdio: "inherit"
1463
+ }).status !== 0) {
1464
+ console.log(
1465
+ `\u26A0 train:adopt failed in ${checkout.name} \u2014 the PR needs work before it merges`
1466
+ );
1467
+ }
1468
+ git(branchCheckout.dir, "add", "-A");
1469
+ const commit = spawnSync2("git", ["commit", "-m", `chore: adopt ${label}`], {
1470
+ cwd: branchCheckout.dir,
1471
+ stdio: "inherit"
1472
+ });
1473
+ if (commit.status !== 0) {
1474
+ throw new Abort(
1475
+ `commit failed on ${branch} in ${checkout.name} (its hooks reject the adoption)`
1476
+ );
1477
+ }
1478
+ } catch (error) {
1479
+ dropBranch(checkout, branch);
1480
+ throw error;
1481
+ }
1482
+ followers.push({ checkout: branchCheckout, onBranch: true });
1483
+ }
1484
+ );
1485
+ reviewBranches.push({ dir: checkout.dir, branch, base });
1486
+ waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
1487
+ } catch (error) {
1488
+ if (!(error instanceof Abort)) throw error;
1489
+ failed.push(checkout.name);
1490
+ waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "failed" });
1491
+ console.error(`\u2717 ${error.message} \u2014 adopt ${label} in ${checkout.name} by hand`);
1492
+ }
1493
+ };
1494
+ const commitOwned = (checkout, packagePaths, changes, message = "chore: sync ecosystem deps to blessed set") => {
1297
1495
  if (dryRun && !changes) return false;
1298
1496
  return effect(`commit its package.json files + bun.lock in ${checkout.name}`, true, () => {
1299
- const tracked = [...packagePaths, "bun.lock"];
1497
+ const tracked = [...packagePaths, "bun.lock"].filter(
1498
+ (path) => existsSync2(join2(checkout.dir, path))
1499
+ );
1300
1500
  const mutated = git(checkout.dir, "status", "--porcelain", "--", ...tracked);
1301
1501
  if (mutated.status !== 0 || mutated.stdout.trim().length === 0) return false;
1302
1502
  if (git(checkout.dir, "add", "--", ...tracked).status !== 0) {
1303
1503
  throw new Abort(`git add failed in ${checkout.name} \u2014 is bun.lock ignored?`);
1304
1504
  }
1305
- const commit = spawnSync2(
1306
- "git",
1307
- ["commit", "-m", "chore: sync ecosystem deps to blessed set"],
1308
- {
1309
- cwd: checkout.dir,
1310
- stdio: "inherit"
1311
- }
1312
- );
1505
+ const commit = spawnSync2("git", ["commit", "-m", message], {
1506
+ cwd: checkout.dir,
1507
+ stdio: "inherit"
1508
+ });
1313
1509
  if (commit.status !== 0) throw new Abort(`commit failed in ${checkout.name}`);
1314
1510
  return true;
1315
1511
  });
@@ -1350,8 +1546,7 @@ if (command === "train") {
1350
1546
  \u2550 ${lane2} lane: ${checkouts.map((checkout) => checkout.name).join(" \u2192 ") || "(none)"}`
1351
1547
  );
1352
1548
  const section = laneSection(manifest, lane2);
1353
- const before = { ...section };
1354
- const laneBlessed = [];
1549
+ const laneRecorded = [];
1355
1550
  try {
1356
1551
  if (missing.length > 0) {
1357
1552
  throw new Abort(
@@ -1370,7 +1565,6 @@ if (command === "train") {
1370
1565
  skipped.push(checkout.name);
1371
1566
  continue;
1372
1567
  }
1373
- const { inspection, bumped, staleLock } = bumpRanges(checkout);
1374
1568
  const releases = checkout.packages.map((entry) => {
1375
1569
  const local = JSON.parse(readFileSync2(join2(checkout.dir, entry.path), "utf8")).version ?? "0.0.0";
1376
1570
  const remote = npmLatest(entry.name);
@@ -1386,47 +1580,50 @@ if (command === "train") {
1386
1580
  skipped.push(checkout.name);
1387
1581
  continue;
1388
1582
  }
1583
+ const moving = movesOf(checkout);
1584
+ if (pendingAdoption(checkout, moving)) continue;
1585
+ const { inspection, bumped, staleLock } = bumpRanges(checkout);
1389
1586
  const relocked = bumped || staleLock || releases.some((release) => release.ahead);
1390
- if (relocked) relockAndCheck(checkout);
1391
- if (commitOwned(checkout, inspection.packagePaths, relocked)) committed.add(checkout.dir);
1392
- followers.push(checkout);
1393
- for (const { entry, local, remote, ahead } of releases) {
1394
- if (ahead) {
1587
+ try {
1588
+ if (relocked) relockAndCheck(checkout);
1589
+ if (commitOwned(checkout, inspection.packagePaths, relocked)) committed.add(checkout.dir);
1590
+ } catch (error) {
1591
+ if (!(error instanceof Abort)) throw error;
1592
+ restore(checkout);
1593
+ if (!(error instanceof CheckFailed) || moving.length === 0) throw error;
1594
+ console.log(`\u2717 ${error.message} against the candidates \u2014 it adopts them on a branch`);
1595
+ walked.add(checkout.dir);
1596
+ adopt(checkout, moving);
1597
+ continue;
1598
+ }
1599
+ walked.add(checkout.dir);
1600
+ followers.push({ checkout, onBranch: false });
1601
+ for (const { entry, local, remote, ahead: ahead2 } of releases) {
1602
+ if (ahead2) {
1395
1603
  publish(checkout, entry, local, remote);
1396
1604
  } else if (section[entry.name] === local) {
1397
1605
  console.log(`\u2713 ${entry.name}@${local} already on npm and blessed`);
1398
1606
  continue;
1607
+ } else if (manifest.candidates?.[entry.name] === local) {
1608
+ console.log(`\u2713 ${entry.name}@${local} already on npm and a candidate`);
1609
+ continue;
1399
1610
  } else {
1400
- console.log(`\u2713 ${entry.name}@${local} already on npm \u2014 blessing it`);
1611
+ console.log(`\u2713 ${entry.name}@${local} already on npm \u2014 recording it as a candidate`);
1401
1612
  }
1402
- section[entry.name] = local;
1403
- laneBlessed.push(`${entry.name}@${local}`);
1613
+ manifest.candidates = { ...manifest.candidates, [entry.name]: local };
1614
+ laneRecorded.push(`${entry.name}@${local}`);
1404
1615
  }
1405
1616
  }
1406
- if (laneBlessed.length > 0) {
1407
- effect(
1408
- `bless ${laneBlessed.join(", ")} in versions.json`,
1409
- void 0,
1410
- () => writeManifest(manifest)
1411
- );
1412
- bomDirty = true;
1413
- blessed.push(...laneBlessed);
1414
- }
1415
1617
  console.log(
1416
- `${lane2} lane blessed: ${laneBlessed.length === 0 ? "nothing new" : laneBlessed.join(", ")}`
1618
+ `${lane2} lane candidates: ${laneRecorded.length === 0 ? "nothing new" : laneRecorded.join(", ")}`
1417
1619
  );
1418
1620
  } catch (error) {
1419
1621
  if (!(error instanceof Abort)) throw error;
1420
- for (const name of Object.keys(section)) delete section[name];
1421
- Object.assign(section, before);
1622
+ failedLanes.add(lane2);
1422
1623
  failed.push(`${lane2} lane`);
1423
- console.error(`\u2717 ${error.message} \u2014 ${lane2} lane stopped; its BOM entries are unchanged`);
1424
- if (laneBlessed.length > 0) {
1425
- console.error(
1426
- ` published but not blessed: ${laneBlessed.join(", ")} \u2014 the next train blesses them`
1427
- );
1428
- }
1624
+ console.error(`\u2717 ${error.message} \u2014 ${lane2} lane stopped; none of its candidates is blessed`);
1429
1625
  }
1626
+ recorded.push(...laneRecorded);
1430
1627
  }
1431
1628
  if (plan.consumers.length > 0 || plan.missingConsumers.length > 0) {
1432
1629
  console.log(
@@ -1449,39 +1646,97 @@ if (command === "train") {
1449
1646
  skipped.push(checkout.name);
1450
1647
  continue;
1451
1648
  }
1649
+ const moving = movesOf(checkout);
1650
+ if (pendingAdoption(checkout, moving)) continue;
1651
+ const { inspection, bumped, staleLock } = bumpRanges(checkout);
1652
+ if (!bumped && !staleLock) {
1653
+ walked.add(checkout.dir);
1654
+ console.log("\u2713 already on the blessed set");
1655
+ continue;
1656
+ }
1452
1657
  try {
1453
- const { inspection, bumped, staleLock } = bumpRanges(checkout);
1454
- if (!bumped && !staleLock) {
1455
- console.log("\u2713 already on the blessed set");
1456
- continue;
1457
- }
1458
1658
  relockAndCheck(checkout);
1459
- const base = offDefaultBranch(checkout.dir)?.main ?? currentBranch(checkout.dir);
1460
- const branch = freeBranch(
1461
- checkout.dir,
1462
- `train/ecosystem-sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
1463
- );
1464
- const switched = effect(
1465
- `create review branch ${branch} in ${checkout.name}`,
1466
- true,
1467
- () => git(checkout.dir, "switch", "-c", branch).status === 0
1468
- );
1469
- if (!switched) throw new Abort(`could not create ${branch} in ${checkout.name}`);
1470
- followers.push(checkout);
1471
- if (commitOwned(checkout, inspection.packagePaths, true)) {
1472
- reviewBranches.push({ dir: checkout.dir, branch, base });
1473
- if (!dryRun) {
1474
- console.log(`committed on ${branch} for review; the checkout stays on it until merged`);
1475
- }
1476
- }
1659
+ reviewBranch(checkout, inspection.packagePaths, moving);
1660
+ walked.add(checkout.dir);
1477
1661
  } catch (error) {
1478
1662
  if (!(error instanceof Abort)) throw error;
1663
+ restore(checkout);
1664
+ if (error instanceof CheckFailed && moving.length > 0) {
1665
+ console.log(`\u2717 ${error.message} against the candidates \u2014 it adopts them on a branch`);
1666
+ walked.add(checkout.dir);
1667
+ adopt(checkout, moving);
1668
+ continue;
1669
+ }
1479
1670
  failed.push(checkout.name);
1480
- console.error(`\u2717 ${error.message} \u2014 its working tree is left as is for inspection`);
1671
+ console.error(`\u2717 ${error.message} \u2014 its checkout is back on its default branch`);
1481
1672
  }
1482
1673
  }
1674
+ const candidateNames = Object.keys(manifest.candidates ?? {});
1675
+ if (candidateNames.length > 0) {
1676
+ const everything = trainPlan(dir, manifest, LANES);
1677
+ for (const checkout of [
1678
+ ...everything.lanes.flatMap((entry) => entry.checkouts),
1679
+ ...everything.consumers
1680
+ ]) {
1681
+ if (walked.has(checkout.dir)) continue;
1682
+ const declared = declaredDependencies(checkout.dir);
1683
+ const moving = candidateNames.filter((name) => declared.has(name));
1684
+ if (moving.length > 0) {
1685
+ waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "unverified" });
1686
+ }
1687
+ }
1688
+ for (const repo of [
1689
+ ...everything.missingConsumers,
1690
+ ...everything.ambiguousConsumers.map((entry) => entry.key)
1691
+ ]) {
1692
+ const consumed = manifest.consumers?.[repo]?.lanes ?? [];
1693
+ const moving = candidateNames.filter((name) => {
1694
+ const lane2 = laneOf(manifest, name);
1695
+ return !!lane2 && consumed.includes(lane2);
1696
+ });
1697
+ if (moving.length > 0) waiting.push({ name: repo, dir, moving, state: "unverified" });
1698
+ }
1699
+ }
1700
+ const held = new Set(waiting.flatMap((entry) => entry.moving));
1701
+ const promotable = candidateNames.filter((name) => {
1702
+ const lane2 = laneOf(manifest, name);
1703
+ return !!lane2 && !failedLanes.has(lane2) && !held.has(name);
1704
+ });
1705
+ const promoted = promoteCandidates(manifest, promotable);
1706
+ const bomDirty = recorded.length > 0 || promoted.length > 0;
1707
+ if (bomDirty) {
1708
+ effect(
1709
+ "write the candidates and blessings to versions.json",
1710
+ void 0,
1711
+ () => writeManifest(manifest)
1712
+ );
1713
+ }
1714
+ const why = {
1715
+ opened: "opened this run",
1716
+ unmerged: "waiting for its PR to merge",
1717
+ unpushed: "committed but never pushed",
1718
+ failed: "the train could not commit it \u2014 adopt by hand",
1719
+ unverified: "not verified this run"
1720
+ };
1483
1721
  console.log(`
1484
- blessed: ${blessed.length === 0 ? "nothing" : blessed.join(", ")}`);
1722
+ blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
1723
+ const candidates = Object.entries(manifest.candidates ?? {});
1724
+ if (candidates.length > 0) {
1725
+ console.log("candidates (check accepts them; not yet blessed):");
1726
+ for (const [name, version] of candidates) {
1727
+ const on = waiting.filter((entry) => entry.moving.includes(name)).map((entry) => `${entry.name} (${entry.branch ?? why[entry.state]})`);
1728
+ console.log(` ${name}@${version}${on.length === 0 ? "" : ` \u2014 waiting on ${on.join(", ")}`}`);
1729
+ }
1730
+ }
1731
+ if (waiting.length > 0) {
1732
+ console.log("pending (merge each adoption PR, then run the train again to bless):");
1733
+ for (const entry of waiting) {
1734
+ const url = entry.state === "unmerged" && !dryRun && entry.branch ? prUrl(entry.dir, entry.branch) : null;
1735
+ console.log(
1736
+ ` ${entry.name}: ${entry.branch ? `${entry.branch} \u2014 ` : ""}${why[entry.state]}${url ? ` ${url}` : ""}`
1737
+ );
1738
+ }
1739
+ }
1485
1740
  if (skipped.length > 0) console.log(`skipped: ${skipped.join(", ")}`);
1486
1741
  if (failed.length > 0) console.log(`failed: ${failed.join(", ")}`);
1487
1742
  const remoteVersion = npmLatest("@inixiative/config");
@@ -1520,26 +1775,25 @@ blessed: ${blessed.length === 0 ? "nothing" : blessed.join(", ")}`);
1520
1775
  process.exit(1);
1521
1776
  }
1522
1777
  });
1523
- effect(
1524
- `commit "chore: bless ${blessed.join(", ")} \u2014 ${version}" in @inixiative/config`,
1525
- void 0,
1526
- () => {
1527
- spawnSync2("git", ["add", "--", "package.json", "versions.json", "test/fixtures"], {
1528
- cwd: packageRoot
1529
- });
1530
- if (spawnSync2("git", ["diff", "--cached", "--quiet"], { cwd: packageRoot }).status === 0)
1531
- return;
1532
- const commit = spawnSync2(
1533
- "git",
1534
- ["commit", "-m", `chore: bless ${blessed.join(", ")} \u2014 ${version}`],
1535
- { cwd: packageRoot, stdio: "inherit" }
1536
- );
1537
- if (commit.status !== 0) {
1538
- console.error("\u2717 commit failed in @inixiative/config \u2014 aborting before publish");
1539
- process.exit(1);
1540
- }
1778
+ const message = `chore: ${[
1779
+ promoted.length > 0 ? `bless ${promoted.join(", ")}` : "",
1780
+ recorded.length > 0 ? `candidate ${recorded.join(", ")}` : ""
1781
+ ].filter(Boolean).join("; ") || "release"} \u2014 ${version}`;
1782
+ effect(`commit "${message}" in @inixiative/config`, void 0, () => {
1783
+ spawnSync2("git", ["add", "--", "package.json", "versions.json", "test/fixtures"], {
1784
+ cwd: packageRoot
1785
+ });
1786
+ if (spawnSync2("git", ["diff", "--cached", "--quiet"], { cwd: packageRoot }).status === 0)
1787
+ return;
1788
+ const commit = spawnSync2("git", ["commit", "-m", message], {
1789
+ cwd: packageRoot,
1790
+ stdio: "inherit"
1791
+ });
1792
+ if (commit.status !== 0) {
1793
+ console.error("\u2717 commit failed in @inixiative/config \u2014 aborting before publish");
1794
+ process.exit(1);
1541
1795
  }
1542
- );
1796
+ });
1543
1797
  effect(`publish @inixiative/config@${version} to npm`, void 0, () => {
1544
1798
  const publish2 = spawnSync2("npm", ["publish"], { cwd: packageRoot, stdio: "inherit" });
1545
1799
  if (publish2.status !== 0 || !servedByRegistry("@inixiative/config", version)) {
@@ -1551,12 +1805,12 @@ blessed: ${blessed.length === 0 ? "nothing" : blessed.join(", ")}`);
1551
1805
  committed.add(packageRoot);
1552
1806
  if (followers.length > 0) console.log(`
1553
1807
  \u2550 re-lock onto @inixiative/config@${version}`);
1554
- for (const checkout of followers) {
1808
+ for (const { checkout, onBranch } of followers) {
1555
1809
  try {
1556
1810
  const { inspection, bumped, staleLock } = bumpRanges(checkout);
1557
1811
  if (!bumped && !staleLock) continue;
1558
1812
  relockAndCheck(checkout, false);
1559
- if (commitOwned(checkout, inspection.packagePaths, true) && !checkout.consumer)
1813
+ if (commitOwned(checkout, inspection.packagePaths, true) && !onBranch)
1560
1814
  committed.add(checkout.dir);
1561
1815
  if (!dryRun) console.log(`\u2713 ${checkout.name} locked to @inixiative/config@${version}`);
1562
1816
  } catch (error) {
@@ -1603,6 +1857,13 @@ dry run complete \u2014 a real train would commit in ${committed.size} repo(s) a
1603
1857
  }
1604
1858
  process.exit(failed.length > 0 ? 1 : 0);
1605
1859
  }
1860
+ function prUrl(cwd, branch) {
1861
+ const view = spawnSync2("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"], {
1862
+ cwd,
1863
+ encoding: "utf8"
1864
+ });
1865
+ return view.status === 0 ? view.stdout.trim() || null : null;
1866
+ }
1606
1867
  function currentBranch(cwd) {
1607
1868
  return git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.trim();
1608
1869
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/config",
3
- "version": "0.5.79",
3
+ "version": "0.5.81",
4
4
  "description": "Shared toolchain for the inixiative ecosystem: tsconfig/biome/tsup presets, a version manifest (BOM), and a sync/check CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/versions.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "@types/bun"
14
14
  ],
15
15
  "ecosystem": {
16
- "@inixiative/config": "0.5.79",
16
+ "@inixiative/config": "0.5.81",
17
17
  "@inixiative/json-rules": "3.5.4",
18
18
  "@inixiative/permissions": "0.4.0",
19
19
  "@inixiative/transitions": "0.5.0",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "agentic": {
26
26
  "@inixiative/signet": "0.4.0",
27
- "@inixiative/archive": "0.17.0",
27
+ "@inixiative/archive": "0.18.0",
28
28
  "@inixiative/agent-session": "0.6.3",
29
29
  "@inixiative/foundry-core": "0.5.1",
30
30
  "@inixiative/foundry": "0.5.3",
@@ -48,5 +48,8 @@
48
48
  "agentic"
49
49
  ]
50
50
  }
51
+ },
52
+ "candidates": {
53
+ "@inixiative/archive": "0.19.0"
51
54
  }
52
55
  }