loadout-ai 0.7.0 → 0.9.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/README.md +160 -315
  3. package/SECURITY.md +21 -1
  4. package/catalog/discovered.json +31269 -26003
  5. package/catalog/packages.json +4 -4
  6. package/dist/src/cli.js +13 -0
  7. package/dist/src/commands/agents.js +3 -1
  8. package/dist/src/commands/catalog-candidate.js +150 -0
  9. package/dist/src/commands/catalog-workflows.js +147 -0
  10. package/dist/src/commands/catalog.js +9 -367
  11. package/dist/src/commands/coordinate.js +586 -0
  12. package/dist/src/commands/coordination-discussions.js +194 -0
  13. package/dist/src/commands/coordination-sessions.js +197 -0
  14. package/dist/src/commands/inventory.js +4 -2
  15. package/dist/src/core/agents/agent-inspection.js +26 -4
  16. package/dist/src/core/{routing → agents}/model-config.js +7 -2
  17. package/dist/src/core/catalog/catalog.js +1 -0
  18. package/dist/src/core/catalog/registry.js +44 -10
  19. package/dist/src/core/catalog/safety.js +36 -7
  20. package/dist/src/core/coordination/adapters/claude-code.js +154 -0
  21. package/dist/src/core/coordination/adapters/codex.js +137 -0
  22. package/dist/src/core/coordination/adapters/types.js +64 -0
  23. package/dist/src/core/coordination/auth.js +59 -0
  24. package/dist/src/core/coordination/bridge-lease.js +79 -0
  25. package/dist/src/core/coordination/conflict-preview.js +167 -0
  26. package/dist/src/core/coordination/contract-diff.js +106 -0
  27. package/dist/src/core/coordination/coordinator.js +552 -0
  28. package/dist/src/core/coordination/crash-recovery.js +169 -0
  29. package/dist/src/core/coordination/daemon.js +667 -0
  30. package/dist/src/core/coordination/discussion.js +340 -0
  31. package/dist/src/core/coordination/events.js +161 -0
  32. package/dist/src/core/coordination/http-api.js +118 -0
  33. package/dist/src/core/coordination/interrupt-policy.js +89 -0
  34. package/dist/src/core/coordination/lock.js +110 -0
  35. package/dist/src/core/coordination/mcp-server.js +424 -0
  36. package/dist/src/core/coordination/redaction.js +85 -0
  37. package/dist/src/core/coordination/replay.js +188 -0
  38. package/dist/src/core/coordination/retention.js +128 -0
  39. package/dist/src/core/coordination/runtime.js +21 -0
  40. package/dist/src/core/coordination/session-manager.js +366 -0
  41. package/dist/src/core/coordination/watcher.js +128 -0
  42. package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
  43. package/dist/src/core/{routing → delegation}/handoff.js +130 -63
  44. package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
  45. package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
  46. package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
  47. package/dist/src/core/discovery/candidate-intelligence.js +3 -214
  48. package/dist/src/core/discovery/community.js +12 -4
  49. package/dist/src/core/discovery/github-discovery.js +6 -2
  50. package/dist/src/core/discovery/private-discovery.js +6 -2
  51. package/dist/src/core/install/reconcile.js +1 -1
  52. package/dist/src/core/install/source.js +21 -7
  53. package/dist/src/core/install/uninstall.js +39 -3
  54. package/dist/src/core/reporting/cli-guide.js +10 -6
  55. package/dist/src/core/reporting/completion.js +61 -109
  56. package/dist/src/core/reporting/doctor.js +4 -6
  57. package/dist/src/core/runtime/bounded-json.js +65 -0
  58. package/dist/src/core/runtime/github.js +30 -16
  59. package/dist/src/core/runtime/mcp-recipes.js +1 -1
  60. package/dist/src/core/workspace/active-policy.js +1 -1
  61. package/docs/CANDIDATE_INTELLIGENCE.md +9 -2
  62. package/docs/CATALOG.md +1 -1
  63. package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +1 -1
  64. package/docs/DEMO_SCRIPT.md +19 -23
  65. package/docs/DISCOVERED.md +251 -250
  66. package/docs/FEATURE_TEST_MATRIX.md +10 -263
  67. package/docs/GITHUB_AUTHORIZATION.md +5 -0
  68. package/docs/LIVE_COLLABORATION.md +234 -0
  69. package/docs/PROVENANCE_AND_COMPARISON.md +1 -1
  70. package/docs/REFERENCE.md +163 -0
  71. package/docs/RELEASE_REVIEW.md +1 -2
  72. package/docs/TESTING.md +17 -1
  73. package/docs/USER_TEST_GUIDE.md +138 -3
  74. package/docs/assets/loadout-discover-activate.webp +0 -0
  75. package/docs/assets/loadout-handoff-coordinate.webp +0 -0
  76. package/docs/assets/loadout-social-preview.png +0 -0
  77. package/docs/decisions/001-coordination-jsonl-locking.md +35 -0
  78. package/docs/decisions/002-local-daemon-authentication.md +32 -0
  79. package/docs/decisions/003-bounded-agent-discussions.md +91 -0
  80. package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
  81. package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
  82. package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
  83. package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -0
  84. package/package.json +18 -7
  85. package/skills/loadout-handoff/SKILL.md +238 -0
  86. package/MASTER_PLAN.md +0 -2207
  87. package/dist/src/core/routing/route.js +0 -539
  88. package/docs/ACTIVE_SET.md +0 -53
  89. package/docs/COMPATIBILITY_POLICY.md +0 -22
  90. package/docs/CONVERSION_AND_SANDBOX.md +0 -27
  91. package/docs/EVALUATION_PROTOCOL_V1.md +0 -300
  92. package/docs/HEAD_TO_HEAD_EVALUATION.md +0 -79
  93. package/docs/PROVIDER_CONFIGURATION.md +0 -45
  94. package/docs/README_RESEARCH.md +0 -36
  95. package/docs/REPOSITORY_STABILIZATION.md +0 -190
  96. package/docs/SAFE_UPDATE_DEMO.md +0 -25
  97. package/docs/SCHEMA_DECISIONS.md +0 -25
  98. package/docs/SUBMISSION_COPY.md +0 -90
  99. package/docs/TEAM_POLICY.md +0 -18
  100. package/docs/assets/loadout-workflow.png +0 -0
  101. package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +0 -283
  102. package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +0 -116
  103. package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +0 -469
  104. package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +0 -80
  105. package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +0 -55
  106. package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +0 -228
  107. package/skills/loadout-router/SKILL.md +0 -120
  108. /package/dist/src/core/{routing → agents}/credentials.js +0 -0
@@ -1,139 +1,91 @@
1
- const commands = [
2
- "guide",
3
- "advanced",
4
- "setup",
5
- "upgrade",
6
- "init",
7
- "lock",
8
- "export",
9
- "import",
10
- "audit",
11
- "create",
12
- "pack",
13
- "publish",
14
- "search",
15
- "add",
16
- "unadd",
17
- "list",
18
- "scan",
19
- "reconcile",
20
- "status",
21
- "versions",
22
- "skill-audit",
23
- "doctor",
24
- "health",
25
- "compare",
26
- "optimize",
27
- "activate",
28
- "enable",
29
- "disable",
30
- "adopt",
31
- "library",
32
- "update",
33
- "rollback",
34
- "discover",
35
- "candidate",
36
- "review-queue",
37
- "review",
38
- "alerts",
39
- "alert-ignore",
40
- "alert-pin",
41
- "alert-unpin",
42
- "alert-pins",
43
- "mcp",
44
- "mcp-recipe",
45
- "mcp-config",
46
- "models",
47
- "credentials",
48
- "schedule",
49
- "unschedule",
50
- "autopilot",
51
- "tool",
52
- "report",
53
- "share",
54
- "card",
55
- "compare-loadouts",
56
- "badge",
57
- "inspect",
58
- "evaluate",
59
- "watch",
60
- "sandbox-run",
61
- "codex-mcp-config",
62
- "plan",
63
- "install",
64
- "convert",
65
- "canary",
66
- "capabilities",
67
- "catalog",
68
- "profiles",
69
- "recommend",
70
- "improve",
71
- "improve-feedback",
72
- "sync",
73
- "outcome",
74
- "outcomes",
75
- "remove",
76
- "uninstall",
77
- "serve",
78
- "completion",
79
- ];
80
- const modelCommands = ["status", "set", "verify"];
81
- const credentialCommands = ["status", "set", "check", "delete"];
82
- const candidateCommands = ["list", "inspect", "propose"];
1
+ let registry = [];
2
+ /** Called once at startup with the assembled program. */
3
+ export function registerCompletionCommands(tree) {
4
+ registry = tree
5
+ .filter((entry) => entry.name && entry.name !== "help")
6
+ .sort((a, b) => a.name.localeCompare(b.name));
7
+ }
8
+ export function completionCommands() {
9
+ return registry.map((entry) => entry.name);
10
+ }
11
+ export function completionSubcommands(name) {
12
+ return registry.find((entry) => entry.name === name)?.subcommands ?? [];
13
+ }
14
+ /** Every executable top-level and parent/child command path. */
15
+ export function completionCommandPaths() {
16
+ return registry.flatMap((entry) => [
17
+ entry.name,
18
+ ...entry.subcommands.map((subcommand) => `${entry.name} ${subcommand}`),
19
+ ]);
20
+ }
83
21
  export function renderShellCompletion(shell) {
84
- const words = commands.join(" ");
22
+ const words = completionCommands().join(" ");
23
+ const parents = registry.filter((entry) => entry.subcommands.length > 0);
85
24
  switch (shell) {
86
- case "bash":
25
+ case "bash": {
26
+ const bashCases = parents
27
+ .map((entry) => ` "${entry.name}") candidates="${entry.subcommands.join(" ")}" ;;`)
28
+ .join("\n");
87
29
  return `# Loadout command completion
88
30
  _loadout() {
89
31
  local current="\${COMP_WORDS[COMP_CWORD]}"
90
32
  local commands="${words}"
33
+ local candidates=""
91
34
  if [[ COMP_CWORD -eq 1 ]]; then
92
- COMPREPLY=( $(compgen -W "$commands" -- "$current") )
93
- elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "models" ]]; then
94
- COMPREPLY=( $(compgen -W "${modelCommands.join(" ")}" -- "$current") )
95
- elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "credentials" ]]; then
96
- COMPREPLY=( $(compgen -W "${credentialCommands.join(" ")}" -- "$current") )
97
- elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "candidate" ]]; then
98
- COMPREPLY=( $(compgen -W "${candidateCommands.join(" ")}" -- "$current") )
35
+ candidates="$commands"
36
+ elif [[ COMP_CWORD -eq 2 ]]; then
37
+ case "\${COMP_WORDS[1]}" in
38
+ ${bashCases}
39
+ esac
99
40
  fi
41
+ COMPREPLY=( $(compgen -W "$candidates" -- "$current") )
100
42
  }
101
43
  complete -F _loadout loadout
102
44
  `;
103
- case "zsh":
45
+ }
46
+ case "zsh": {
47
+ const zshCases = parents
48
+ .map((entry) => ` '${entry.name}') _values '${entry.name} command' ${entry.subcommands.map((command) => `'${command}'`).join(" ")} ;;`)
49
+ .join("\n");
104
50
  return `#compdef loadout
105
- typeset -a commands model_commands credential_commands candidate_commands
106
- commands=(${commands.map((command) => `'${command}'`).join(" ")})
107
- model_commands=(${modelCommands.map((command) => `'${command}'`).join(" ")})
108
- credential_commands=(${credentialCommands.map((command) => `'${command}'`).join(" ")})
109
- candidate_commands=(${candidateCommands.map((command) => `'${command}'`).join(" ")})
51
+ typeset -a commands
52
+ commands=(${completionCommands()
53
+ .map((command) => `'${command}'`)
54
+ .join(" ")})
110
55
  if (( CURRENT == 2 )); then
111
56
  _describe 'command' commands
112
- elif (( CURRENT == 3 )) && [[ $words[2] == models ]]; then
113
- _describe 'models command' model_commands
114
- elif (( CURRENT == 3 )) && [[ $words[2] == credentials ]]; then
115
- _describe 'credentials command' credential_commands
116
- elif (( CURRENT == 3 )) && [[ $words[2] == candidate ]]; then
117
- _describe 'candidate command' candidate_commands
57
+ elif (( CURRENT == 3 )); then
58
+ case $words[2] in
59
+ ${zshCases}
60
+ esac
118
61
  fi
119
62
  `;
63
+ }
120
64
  case "fish":
121
65
  return [
122
- ...commands.map((command) => `complete -c loadout -f -n '__fish_use_subcommand' -a ${command}`),
123
- ...modelCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from models' -a ${command}`),
124
- ...credentialCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from credentials' -a ${command}`),
125
- ...candidateCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from candidate' -a ${command}`),
66
+ ...completionCommands().map((command) => `complete -c loadout -f -n '__fish_use_subcommand' -a ${command}`),
67
+ ...parents.flatMap((entry) => entry.subcommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from ${entry.name}' -a ${command}`)),
126
68
  ]
127
69
  .join("\n")
128
70
  .concat("\n");
129
- case "powershell":
71
+ case "powershell": {
72
+ const powershellCases = parents
73
+ .map((entry, index) => `${index === 0 ? "if" : "elseif"} ($elements.Count -ge 2 -and $elements[1] -eq '${entry.name}') { @(${entry.subcommands.map((command) => `'${command}'`).join(", ")}) }`)
74
+ .join(" ");
75
+ const topLevel = `@(${completionCommands()
76
+ .map((command) => `'${command}'`)
77
+ .join(", ")})`;
78
+ const powershellCandidates = powershellCases
79
+ ? `${powershellCases} else { ${topLevel} }`
80
+ : topLevel;
130
81
  return `Register-ArgumentCompleter -Native -CommandName loadout -ScriptBlock {
131
82
  param($wordToComplete, $commandAst, $cursorPosition)
132
83
  $elements = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text })
133
- $candidates = if ($elements.Count -ge 2 -and $elements[1] -eq 'models') { @(${modelCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'credentials') { @(${credentialCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'candidate') { @(${candidateCommands.map((command) => `'${command}'`).join(", ")}) } else { @(${commands.map((command) => `'${command}'`).join(", ")}) }
84
+ $candidates = ${powershellCandidates}
134
85
  $candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
135
86
  }
136
87
  `;
88
+ }
137
89
  }
138
90
  }
139
91
  export function parseCompletionShell(value) {
@@ -1,7 +1,7 @@
1
1
  import { access, constants } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  import { detectAgents, directoryExists, loadoutHome, userHome, } from "../agents/paths.js";
4
- import { inspectAgents } from "../agents/agent-inspection.js";
4
+ import { countComponentUnits, inspectAgents, } from "../agents/agent-inspection.js";
5
5
  async function writable(path) {
6
6
  // A not-yet-created directory is healthy when any existing ancestor is
7
7
  // writable; this handles first-run paths such as ~/.agents/skills.
@@ -83,13 +83,11 @@ export function formatDoctorReport(report, options = {}) {
83
83
  if (installed.length) {
84
84
  lines.push("DETECTED AGENTS");
85
85
  for (const entry of installed) {
86
- const skillCount = entry.inventory.components
87
- .filter((c) => c.scanned && c.directoryExists)
88
- .reduce((sum, c) => sum + c.entries.length, 0);
86
+ const skillCount = entry.inventory.components.reduce((sum, component) => sum + countComponentUnits(component), 0);
89
87
  const supported = entry.inventory.components
90
88
  .filter((c) => c.compatibility === "native" || c.compatibility === "adapted")
91
89
  .map((c) => c.type);
92
- lines.push(` ✓ ${entry.agent.displayName}`, ` ${entry.agent.skillsDirectory}`, ` ${skillCount} items | supports: ${supported.join(", ")}`);
90
+ lines.push(` ✓ ${entry.agent.displayName}`, ` ${entry.agent.skillsDirectory}`, ` ${skillCount} skills | supports: ${supported.join(", ")}`);
93
91
  if (entry.issues.length) {
94
92
  for (const issue of entry.issues)
95
93
  lines.push(` ⚠ ${issue}`);
@@ -129,7 +127,7 @@ export function formatDoctorReport(report, options = {}) {
129
127
  lines.push(" Install an AI coding agent (Claude Code, Codex, Cursor, etc.),", " then run `loadout doctor` again.");
130
128
  }
131
129
  else {
132
- lines.push(" loadout status see what's managed", " loadout setup install a curated skill set", " loadout route find the right model for your task", " loadout health check for updates");
130
+ lines.push(" loadout status see what's managed", " loadout setup install a curated skill set", " loadout health check for updates");
133
131
  }
134
132
  return lines.join("\n");
135
133
  }
@@ -0,0 +1,65 @@
1
+ function combinedSignal(caller, timeoutMs) {
2
+ const timeout = AbortSignal.timeout(timeoutMs);
3
+ return caller ? AbortSignal.any([caller, timeout]) : timeout;
4
+ }
5
+ async function readBoundedBody(response, maxBytes, label) {
6
+ const length = response.headers.get("content-length");
7
+ if (length !== null && /^\d+$/.test(length) && Number(length) > maxBytes) {
8
+ throw new Error(`${label} response exceeds the size limit`);
9
+ }
10
+ if (!response.body) {
11
+ const bytes = new Uint8Array(await response.arrayBuffer());
12
+ if (bytes.byteLength > maxBytes)
13
+ throw new Error(`${label} response exceeds the size limit`);
14
+ return bytes;
15
+ }
16
+ const reader = response.body.getReader();
17
+ const chunks = [];
18
+ let total = 0;
19
+ try {
20
+ while (true) {
21
+ const { done, value } = await reader.read();
22
+ if (done)
23
+ break;
24
+ total += value.byteLength;
25
+ if (total > maxBytes) {
26
+ await reader.cancel(`${label} response exceeds the size limit`);
27
+ throw new Error(`${label} response exceeds the size limit`);
28
+ }
29
+ chunks.push(value);
30
+ }
31
+ }
32
+ finally {
33
+ reader.releaseLock();
34
+ }
35
+ const bytes = new Uint8Array(total);
36
+ let offset = 0;
37
+ for (const chunk of chunks) {
38
+ bytes.set(chunk, offset);
39
+ offset += chunk.byteLength;
40
+ }
41
+ return bytes;
42
+ }
43
+ /** Fetch and parse untrusted JSON without allowing an unbounded wait or body. */
44
+ export async function boundedJson(fetcher, input, init, limits) {
45
+ if (!Number.isFinite(limits.timeoutMs) || limits.timeoutMs <= 0)
46
+ throw new Error("JSON request timeout must be greater than zero");
47
+ if (!Number.isSafeInteger(limits.maxBytes) || limits.maxBytes <= 0)
48
+ throw new Error("JSON response byte limit must be a positive integer");
49
+ const response = await fetcher(input, {
50
+ ...init,
51
+ signal: combinedSignal(init.signal, limits.timeoutMs),
52
+ });
53
+ const body = await readBoundedBody(response, limits.maxBytes, limits.label);
54
+ if (!response.ok)
55
+ return { response, value: undefined };
56
+ try {
57
+ return {
58
+ response,
59
+ value: JSON.parse(new TextDecoder().decode(body)),
60
+ };
61
+ }
62
+ catch {
63
+ throw new Error(`${limits.label} returned malformed JSON`);
64
+ }
65
+ }
@@ -2,6 +2,7 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
4
  import { loadoutHome } from "../agents/paths.js";
5
+ import { boundedJson } from "./bounded-json.js";
5
6
  function normalizeRepository(input) {
6
7
  const value = input
7
8
  .trim()
@@ -44,10 +45,13 @@ export async function fetchGitHubMetadata(input, options = {}) {
44
45
  if (process.env.GITHUB_TOKEN)
45
46
  headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
46
47
  let response;
48
+ let value;
47
49
  try {
48
- response = await fetcher(`https://api.github.com/repos/${repository}`, {
49
- headers,
50
- });
50
+ ({ response, value } = await boundedJson(fetcher, `https://api.github.com/repos/${repository}`, { headers }, {
51
+ timeoutMs: 30_000,
52
+ maxBytes: 4 * 1024 * 1024,
53
+ label: "GitHub metadata",
54
+ }));
51
55
  }
52
56
  catch (error) {
53
57
  if (cached)
@@ -60,21 +64,27 @@ export async function fetchGitHubMetadata(input, options = {}) {
60
64
  return cached;
61
65
  throw new Error(`GitHub metadata request failed (${response.status}${rateLimited ? ", rate limit exceeded" : ""}) for ${repository}`);
62
66
  }
63
- const value = (await response.json());
67
+ if (!value || typeof value !== "object")
68
+ throw new Error(`GitHub metadata response is invalid for ${repository}`);
69
+ const record = value;
64
70
  const metadata = {
65
71
  repository,
66
- stars: typeof value.stargazers_count === "number" ? value.stargazers_count : 0,
67
- description: typeof value.description === "string" ? value.description : "",
68
- defaultBranch: typeof value.default_branch === "string" ? value.default_branch : "main",
69
- topics: Array.isArray(value.topics)
70
- ? value.topics.filter((topic) => typeof topic === "string")
72
+ stars: typeof record.stargazers_count === "number" ? record.stargazers_count : 0,
73
+ description: typeof record.description === "string" ? record.description : "",
74
+ defaultBranch: typeof record.default_branch === "string"
75
+ ? record.default_branch
76
+ : "main",
77
+ topics: Array.isArray(record.topics)
78
+ ? record.topics.filter((topic) => typeof topic === "string")
71
79
  : [],
72
- openIssues: typeof value.open_issues_count === "number" ? value.open_issues_count : 0,
73
- archived: value.archived === true,
74
- lastUpdatedAt: typeof value.updated_at === "string"
75
- ? value.updated_at
80
+ openIssues: typeof record.open_issues_count === "number"
81
+ ? record.open_issues_count
82
+ : 0,
83
+ archived: record.archived === true,
84
+ lastUpdatedAt: typeof record.updated_at === "string"
85
+ ? record.updated_at
76
86
  : new Date().toISOString(),
77
- pushedAt: typeof value.pushed_at === "string" ? value.pushed_at : null,
87
+ pushedAt: typeof record.pushed_at === "string" ? record.pushed_at : null,
78
88
  fetchedAt: new Date().toISOString(),
79
89
  };
80
90
  await mkdir(join(loadoutHome(), "cache", "github-metadata"), {
@@ -107,8 +117,13 @@ export async function fetchGitHubReleaseMetadata(input, options = {}) {
107
117
  if (process.env.GITHUB_TOKEN)
108
118
  headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
109
119
  let response;
120
+ let releases;
110
121
  try {
111
- response = await fetcher(`https://api.github.com/repos/${repository}/releases?per_page=1`, { headers });
122
+ ({ response, value: releases } = await boundedJson(fetcher, `https://api.github.com/repos/${repository}/releases?per_page=1`, { headers }, {
123
+ timeoutMs: 30_000,
124
+ maxBytes: 4 * 1024 * 1024,
125
+ label: "GitHub releases",
126
+ }));
112
127
  }
113
128
  catch (error) {
114
129
  if (cached)
@@ -120,7 +135,6 @@ export async function fetchGitHubReleaseMetadata(input, options = {}) {
120
135
  return cached;
121
136
  throw new Error(`GitHub release request failed (${response.status}) for ${repository}`);
122
137
  }
123
- const releases = (await response.json());
124
138
  const release = Array.isArray(releases) ? releases[0] : undefined;
125
139
  const record = release && typeof release === "object" ? release : {};
126
140
  const assets = Array.isArray(record.assets)
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
- import { createCredentialResolver } from "../routing/credentials.js";
4
+ import { createCredentialResolver } from "../agents/credentials.js";
5
5
  import { planMcpConfig } from "./mcp.js";
6
6
  /**
7
7
  * Small, source-linked recipes. These configure a connection only; authorizing
@@ -378,7 +378,7 @@ export async function planProjectActivation(projectPath, options = {}) {
378
378
  }
379
379
  export async function applyProjectActivation(plan) {
380
380
  if (!plan.activation || !plan.selected.length)
381
- throw new Error("No reviewed library skills were selected for activation");
381
+ return null;
382
382
  return applyActivationChange(plan.activation, {
383
383
  preflight: async () => {
384
384
  const requested = plan.agentPlans.map((item) => item.agent);
@@ -93,10 +93,16 @@ powers, platform behavior, category, and catalog policy.
93
93
 
94
94
  ## Distribute a trusted catalog release
95
95
 
96
- Maintainers sign a technically screened full catalog array with an Ed25519 key kept outside the
96
+ > **Not implemented.** The signing flow below is a design sketch. `loadout keygen`,
97
+ > `loadout catalog-sign`, and `loadout catalog-update` do not exist in the CLI
98
+ > today; the catalog ships in the package and is verified by the evidence gate
99
+ > instead. Kept here as the intended shape, not as instructions.
100
+
101
+ Maintainers would sign a technically screened full catalog array with an Ed25519 key kept outside the
97
102
  repository:
98
103
 
99
104
  ```bash
105
+ # Design sketch — these commands do not exist yet.
100
106
  loadout keygen --private-key /secure/catalog-private.pem \
101
107
  --public-key ./catalog-public.pem
102
108
  loadout catalog-sign --catalog ./catalog/packages.json \
@@ -104,9 +110,10 @@ loadout catalog-sign --catalog ./catalog/packages.json \
104
110
  --output ./catalog.signed.json
105
111
  ```
106
112
 
107
- Users preview the verified release before trusting it:
113
+ Users would preview the verified release before trusting it:
108
114
 
109
115
  ```bash
116
+ # Design sketch — these commands do not exist yet.
110
117
  loadout catalog-update --source ./catalog.signed.json \
111
118
  --public-key ./catalog-public.pem
112
119
  loadout catalog-update --source ./catalog.signed.json \
package/docs/CATALOG.md CHANGED
@@ -12,7 +12,7 @@ Thank you to every maintainer and contributor whose work appears here. Loadout d
12
12
  - **License** is the SPDX identifier recorded in `catalog/packages.json` from GitHub metadata. **Review required** corresponds to `NOASSERTION`: no SPDX identifier was reported, so users must inspect the upstream terms instead of assuming permission.
13
13
  - **Reviewed revision** links to the immutable commit inspected for catalog admission. It is not necessarily the newest upstream commit.
14
14
 
15
- The machine-readable catalog remains the source of truth. Run `loadout catalog --coverage --json` to inspect coverage and `loadout capabilities` to inspect what each local agent adapter can actually manage.
15
+ The machine-readable catalog remains the source of truth. Run `loadout catalog --coverage --json` to inspect coverage and `loadout doctor --verbose` to inspect what each local agent adapter can actually manage.
16
16
 
17
17
  ## All 53 credited repositories
18
18
 
@@ -26,6 +26,6 @@ verification restores the snapshot and quarantines the candidate commit. A user
26
26
  revoke a policy at any time; revocation prevents future mutations but never deletes
27
27
  existing snapshots.
28
28
 
29
- The local `loadout canary` command implements the non-mutating static gate. A
29
+ Update safety runs as a non-mutating static gate during `loadout upgrade` previews. A
30
30
  transaction layer must provide verification and promotion callbacks before a
31
31
  candidate can be promoted; the command itself never installs a candidate.
@@ -10,7 +10,7 @@ not show the retired dashboard. The final YouTube upload must be public.
10
10
  2. Confirm the release and product health:
11
11
 
12
12
  ```bash
13
- npm install --global loadout-ai@0.5.9
13
+ npm install --global loadout-ai@0.9.0
14
14
  hash -r
15
15
  loadout --version
16
16
  loadout health
@@ -107,33 +107,32 @@ loadout install --mode custom --package humanizer --agents codex
107
107
  > skill such as Humanizer can be installed directly through Custom mode. Each path
108
108
  > discloses permissions and credentials before approval.
109
109
 
110
- ### 2:05 to 2:25: today and tomorrow
110
+ ### 2:05 to 2:28: hand work from Claude Code to Codex
111
111
 
112
112
  **Screen:**
113
113
 
114
114
  ```bash
115
- loadout update
116
- loadout candidate list --limit 5
115
+ loadout skills install loadout-handoff --yes
116
+ loadout handoff codex "write tests for the auth module" --context "see src/auth.ts"
117
+ loadout handoff
117
118
  ```
118
119
 
119
120
  **Say:**
120
121
 
121
- > Update checks managed pinned sources without silently changing them. The discovery
122
- > feed watches a wider ecosystem and ranks new projects for review. A repository can
123
- > be noticed quickly without being trusted or installed blindly.
122
+ > Loadout also gives Claude Code and Codex a tiny shared handoff log. Claude can leave
123
+ > a bounded task with context, and Codex sees it when the next session checks its
124
+ > inbox. It is intentionally a local file workflow—not a background service or a live
125
+ > chat channel.
124
126
 
125
- ### 2:25 to 2:48: Codex, GPT-5.6, and close
127
+ ### 2:28 to 2:48: close
126
128
 
127
- **Screen:** README section “Built with Codex and GPT-5.6”, then return to the hero.
129
+ **Screen:** Return to the README hero and the handoff row in the workflow image.
128
130
 
129
131
  **Say:**
130
132
 
131
- > We used Codex and GPT-5.6 throughout the build: architecture, threat modelling,
132
- > cross-platform implementation, repo-wide debugging, tests, and the real founder
133
- > acceptance loop. Those tests caught rollback, project-scope, inventory, and update
134
- > bugs on my actual Codex and Claude profiles. Loadout itself stays local and does not
135
- > require an OpenAI API key. One command gives your AI agents a loadout you can
136
- > understand, improve, and undo.
133
+ > Loadout stays local and does not require an LLM API key to manage skills. One CLI
134
+ > gives your coding agents a setup you can inspect, improve, pass work through, and
135
+ > undo. Agent extensions, under control.
137
136
 
138
137
  ## Final edit and upload
139
138
 
@@ -142,15 +141,12 @@ loadout candidate list --limit 5
142
141
  - Add a small title card: **Loadout: Agent extensions, under control.**
143
142
  - Export under three minutes and upload publicly to YouTube.
144
143
  - Watch the uploaded video once, confirm audio and text are legible, then paste the
145
- exact public URL into README and Devpost.
144
+ exact public URL into the README.
146
145
 
147
146
  ## Submission checklist
148
147
 
149
148
  - [ ] Public YouTube video is under three minutes and its URL opens signed out.
150
- - [ ] Voiceover explains the product, Codex usage, and GPT-5.6 usage.
151
- - [ ] `/feedback` was run in Codex and the session ID is in Devpost.
152
- - [ ] Repository URL is accessible to Devpost and OpenAI.
153
- - [ ] README has npm setup and the judge testing path.
154
- - [ ] Every teammate has accepted the Devpost invitation.
155
- - [ ] Category is **Developer Tools**.
156
- - [ ] Submission is submitted, not left as a draft.
149
+ - [ ] Voiceover shows preview, rollback, and Claude Code ↔ Codex handoff.
150
+ - [ ] Repository and npm links open in a signed-out browser.
151
+ - [ ] README installation and user-test paths match the published version.
152
+ - [ ] No terminal frame exposes tokens, usernames, or private repository names.