loadout-ai 0.5.5 → 0.5.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.6 - 2026-07-21
6
+
7
+ ### Changed
8
+
9
+ - Resolve each repository's default-branch commit with a lightweight Git metadata
10
+ request before downloading files, deduplicate shared sources, and reuse exact
11
+ cached changed revisions for safety review.
12
+ - Give changed repositories a bounded 120-second review window while keeping remote
13
+ commit checks at 30 seconds, so large reviewed sources do not produce permanent
14
+ false connectivity warnings under four-way concurrency.
15
+
16
+ ### Fixed
17
+
18
+ - Complete update checks for large sources such as Scientific Agent Skills and
19
+ Awesome Copilot without weakening static diff analysis, activating disabled skills,
20
+ or treating a timed-out clone as a GitHub outage.
21
+
5
22
  ## 0.5.5 - 2026-07-21
6
23
 
7
24
  ### Changed
package/MASTER_PLAN.md CHANGED
@@ -328,6 +328,14 @@ merged `codex/relatable-readme-hero` remote branch remains safe to delete after
328
328
  managed drift, and no active adopted-skill update. Release this correction
329
329
  before the remaining Graphify removal, discovery, uninstall, and reinstall
330
330
  acceptance steps.
331
+ - [x] `P18-46 [TERRA]` Eliminate the repeated large-repository timeout exposed by the
332
+ published `0.5.5` founder check. Resolve remote HEAD metadata before fetching
333
+ content, download only changed revisions that require static safety analysis,
334
+ reuse immutable cached snapshots, and allow those bounded reviews 120 seconds.
335
+ The real 40-record Maximum state now completes with 19 disabled-library
336
+ changes, 21 current packages, zero active updates, and zero unavailable checks;
337
+ both previously failing large repositories complete without changing or
338
+ activating any agent-visible skill.
331
339
 
332
340
  ### Release 0.3 lifecycle hardening
333
341
 
package/README.md CHANGED
@@ -26,7 +26,7 @@
26
26
  </p>
27
27
 
28
28
  > [!IMPORTANT]
29
- > Installation is version-pinned so the code you test matches these docs. The commands below target `loadout-ai@0.5.5`; review the preview before every apply.
29
+ > Installation is version-pinned so the code you test matches these docs. The commands below target `loadout-ai@0.5.6`; review the preview before every apply.
30
30
 
31
31
  ## How it works
32
32
 
@@ -76,7 +76,7 @@ Skills, plugins, MCP servers, and agent settings tend to accumulate one experime
76
76
  You need Node.js 20 or newer and Git.
77
77
 
78
78
  ```bash
79
- npm install --global loadout-ai@0.5.5
79
+ npm install --global loadout-ai@0.5.6
80
80
  loadout --version
81
81
  loadout guide
82
82
  ```
package/dist/src/cli.js CHANGED
@@ -264,7 +264,7 @@ async function runSetup(options) {
264
264
  reader?.close();
265
265
  }
266
266
  }
267
- const LOADOUT_VERSION = "0.5.5";
267
+ const LOADOUT_VERSION = "0.5.6";
268
268
  function durableSchedulerLauncher() {
269
269
  return [
270
270
  join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
@@ -909,7 +909,7 @@ program
909
909
  .option("--agents <ids>", "limit explained scores to selected agent ids")
910
910
  .action(async (options) => {
911
911
  if (options.updates && !options.json)
912
- console.error("Checking managed repositories (4 at a time, 30s per network operation)…");
912
+ console.error("Checking repository commits (4 at a time; changed sources may take up to 120s for safety review)…");
913
913
  const report = await buildHealthReport({
914
914
  updates: options.updates
915
915
  ? () => buildUpdatePlan(undefined, {
@@ -2857,7 +2857,7 @@ program
2857
2857
  profile = await evaluateInstalledProfile(catalog);
2858
2858
  }
2859
2859
  if (!options.json)
2860
- console.error("Checking managed repositories (4 at a time, 30s per network operation)…");
2860
+ console.error("Checking repository commits (4 at a time; changed sources may take up to 120s for safety review)…");
2861
2861
  let plans = await buildUpdatePlan(undefined, {
2862
2862
  packageId: options.package,
2863
2863
  onProgress: options.json
@@ -144,6 +144,36 @@ export function normalizeRepository(input) {
144
144
  }
145
145
  return value;
146
146
  }
147
+ export function parseRepositoryHead(repository, output) {
148
+ const commit = output
149
+ .split(/\r?\n/)
150
+ .map((line) => line.split(/\s+/))
151
+ .find((parts) => parts[1] === "HEAD" && /^[0-9a-f]{40}$/i.test(parts[0]))?.[0];
152
+ if (!commit)
153
+ throw new Error(`Git returned an invalid default-branch HEAD for ${repository}`);
154
+ return commit;
155
+ }
156
+ /** Resolve the public default-branch commit without downloading repository files. */
157
+ export async function resolveRepositoryHead(input, options = {}) {
158
+ const repository = normalizeRepository(input);
159
+ const gitEnvironment = await isolatedGitEnvironment(loadoutHome());
160
+ const url = `https://github.com/${repository}.git`;
161
+ try {
162
+ const { stdout } = await execFileAsync("git", ["ls-remote", "--symref", "--", url, "HEAD"], {
163
+ maxBuffer: 1024 * 1024,
164
+ timeout: options.timeoutMs,
165
+ env: gitEnvironment,
166
+ });
167
+ return {
168
+ repository,
169
+ commit: parseRepositoryHead(repository, stdout),
170
+ };
171
+ }
172
+ catch (error) {
173
+ const message = error instanceof Error ? error.message : String(error);
174
+ throw new Error(`Could not resolve ${repository}: ${message}`);
175
+ }
176
+ }
147
177
  function normalizeRef(ref) {
148
178
  if (!/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(ref) ||
149
179
  ref.includes("..") ||
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
- import { fetchRepositorySnapshot } from "./source.js";
3
+ import { fetchRepositorySnapshot, resolveRepositoryHead } from "./source.js";
4
4
  import { repositoryCachePath } from "./source.js";
5
5
  import { diffRepositorySnapshots } from "./diff.js";
6
6
  import { hashDirectory, readInstallState } from "./state.js";
@@ -85,21 +85,39 @@ async function analyzeManagedUpdate(oldRoot, newRoot, unitIds) {
85
85
  };
86
86
  }
87
87
  /** Builds a read-only update plan from persisted installs and live GitHub snapshots. */
88
- export async function buildUpdatePlan(resolver = async (repository) => fetchRepositorySnapshot(repository, { timeoutMs: 30_000 }), options = {}) {
88
+ export async function buildUpdatePlan(resolver, options = {}) {
89
89
  const state = await readInstallState();
90
90
  const records = options.packageId
91
91
  ? state.installs.filter((record) => record.packageId === options.packageId)
92
92
  : state.installs;
93
93
  const results = new Array(records.length);
94
+ const lightweightResolver = resolver ??
95
+ options.resolveHead ??
96
+ ((repository) => resolveRepositoryHead(repository, { timeoutMs: 30_000 }));
94
97
  const resolutions = new Map();
95
98
  const resolveOnce = (repository) => {
96
99
  const existing = resolutions.get(repository);
97
100
  if (existing)
98
101
  return existing;
99
- const pending = resolver(repository);
102
+ const pending = lightweightResolver(repository);
100
103
  resolutions.set(repository, pending);
101
104
  return pending;
102
105
  };
106
+ const changedSnapshots = new Map();
107
+ const fetchChangedOnce = (repository, commit) => {
108
+ const key = `${repository}\0${commit.toLowerCase()}`;
109
+ const existing = changedSnapshots.get(key);
110
+ if (existing)
111
+ return existing;
112
+ const pending = options.fetchChangedSnapshot
113
+ ? options.fetchChangedSnapshot(repository)
114
+ : fetchRepositorySnapshot(repository, {
115
+ ref: commit,
116
+ timeoutMs: 120_000,
117
+ });
118
+ changedSnapshots.set(key, pending);
119
+ return pending;
120
+ };
103
121
  let cursor = 0;
104
122
  let completed = 0;
105
123
  const workers = Array.from({
@@ -139,9 +157,16 @@ export async function buildUpdatePlan(resolver = async (repository) => fetchRepo
139
157
  let diff;
140
158
  let safetyFindings;
141
159
  let approvalRequired = false;
142
- if (!same && current.path) {
160
+ let currentPath = current.path;
161
+ if (!same && !currentPath && !resolver) {
162
+ const fetched = await fetchChangedOnce(record.repository, current.commit);
163
+ if (fetched.commit.toLowerCase() !== current.commit.toLowerCase())
164
+ throw new Error(`Resolved ${current.commit}, but fetched ${fetched.commit} for safety review`);
165
+ currentPath = fetched.path;
166
+ }
167
+ if (!same && currentPath) {
143
168
  const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
144
- const analysis = await analyzeManagedUpdate(oldPath, current.path, managedUnitIds(state, record.packageId));
169
+ const analysis = await analyzeManagedUpdate(oldPath, currentPath, managedUnitIds(state, record.packageId));
145
170
  diff = analysis.diff;
146
171
  safetyFindings = analysis.safetyFindings;
147
172
  approvalRequired = analysis.approvalRequired;
@@ -211,7 +211,7 @@ cleanup deliberately deletes Loadout's snapshots, so it is the last lifecycle te
211
211
  ## Troubleshooting and recovery
212
212
 
213
213
  - **`loadout` is not found after installation:** confirm `npm install --global
214
- loadout-ai@0.5.5` completed, run `hash -r`, and confirm npm's global binary
214
+ loadout-ai@0.5.6` completed, run `hash -r`, and confirm npm's global binary
215
215
  directory is on `PATH`. For a source checkout, run `npm run build` and `npm link`.
216
216
  - **A preview asks for `--approve-risk`:** read the reported scripts, domains,
217
217
  credentials, binaries, or instruction findings. If you accept that specific plan,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadout-ai",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Universal upgrade manager for AI coding agents",