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.
- package/CHANGELOG.md +148 -1
- package/README.md +160 -315
- package/SECURITY.md +21 -1
- package/catalog/discovered.json +31269 -26003
- package/catalog/packages.json +4 -4
- package/dist/src/cli.js +13 -0
- package/dist/src/commands/agents.js +3 -1
- package/dist/src/commands/catalog-candidate.js +150 -0
- package/dist/src/commands/catalog-workflows.js +147 -0
- package/dist/src/commands/catalog.js +9 -367
- package/dist/src/commands/coordinate.js +586 -0
- package/dist/src/commands/coordination-discussions.js +194 -0
- package/dist/src/commands/coordination-sessions.js +197 -0
- package/dist/src/commands/inventory.js +4 -2
- package/dist/src/core/agents/agent-inspection.js +26 -4
- package/dist/src/core/{routing → agents}/model-config.js +7 -2
- package/dist/src/core/catalog/catalog.js +1 -0
- package/dist/src/core/catalog/registry.js +44 -10
- package/dist/src/core/catalog/safety.js +36 -7
- package/dist/src/core/coordination/adapters/claude-code.js +154 -0
- package/dist/src/core/coordination/adapters/codex.js +137 -0
- package/dist/src/core/coordination/adapters/types.js +64 -0
- package/dist/src/core/coordination/auth.js +59 -0
- package/dist/src/core/coordination/bridge-lease.js +79 -0
- package/dist/src/core/coordination/conflict-preview.js +167 -0
- package/dist/src/core/coordination/contract-diff.js +106 -0
- package/dist/src/core/coordination/coordinator.js +552 -0
- package/dist/src/core/coordination/crash-recovery.js +169 -0
- package/dist/src/core/coordination/daemon.js +667 -0
- package/dist/src/core/coordination/discussion.js +340 -0
- package/dist/src/core/coordination/events.js +161 -0
- package/dist/src/core/coordination/http-api.js +118 -0
- package/dist/src/core/coordination/interrupt-policy.js +89 -0
- package/dist/src/core/coordination/lock.js +110 -0
- package/dist/src/core/coordination/mcp-server.js +424 -0
- package/dist/src/core/coordination/redaction.js +85 -0
- package/dist/src/core/coordination/replay.js +188 -0
- package/dist/src/core/coordination/retention.js +128 -0
- package/dist/src/core/coordination/runtime.js +21 -0
- package/dist/src/core/coordination/session-manager.js +366 -0
- package/dist/src/core/coordination/watcher.js +128 -0
- package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
- package/dist/src/core/{routing → delegation}/handoff.js +130 -63
- package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
- package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
- package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
- package/dist/src/core/discovery/candidate-intelligence.js +3 -214
- package/dist/src/core/discovery/community.js +12 -4
- package/dist/src/core/discovery/github-discovery.js +6 -2
- package/dist/src/core/discovery/private-discovery.js +6 -2
- package/dist/src/core/install/reconcile.js +1 -1
- package/dist/src/core/install/source.js +21 -7
- package/dist/src/core/install/uninstall.js +39 -3
- package/dist/src/core/reporting/cli-guide.js +10 -6
- package/dist/src/core/reporting/completion.js +61 -109
- package/dist/src/core/reporting/doctor.js +4 -6
- package/dist/src/core/runtime/bounded-json.js +65 -0
- package/dist/src/core/runtime/github.js +30 -16
- package/dist/src/core/runtime/mcp-recipes.js +1 -1
- package/dist/src/core/workspace/active-policy.js +1 -1
- package/docs/CANDIDATE_INTELLIGENCE.md +9 -2
- package/docs/CATALOG.md +1 -1
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +1 -1
- package/docs/DEMO_SCRIPT.md +19 -23
- package/docs/DISCOVERED.md +251 -250
- package/docs/FEATURE_TEST_MATRIX.md +10 -263
- package/docs/GITHUB_AUTHORIZATION.md +5 -0
- package/docs/LIVE_COLLABORATION.md +234 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +1 -1
- package/docs/REFERENCE.md +163 -0
- package/docs/RELEASE_REVIEW.md +1 -2
- package/docs/TESTING.md +17 -1
- package/docs/USER_TEST_GUIDE.md +138 -3
- package/docs/assets/loadout-discover-activate.webp +0 -0
- package/docs/assets/loadout-handoff-coordinate.webp +0 -0
- package/docs/assets/loadout-social-preview.png +0 -0
- package/docs/decisions/001-coordination-jsonl-locking.md +35 -0
- package/docs/decisions/002-local-daemon-authentication.md +32 -0
- package/docs/decisions/003-bounded-agent-discussions.md +91 -0
- package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
- package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
- package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
- package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -0
- package/package.json +18 -7
- package/skills/loadout-handoff/SKILL.md +238 -0
- package/MASTER_PLAN.md +0 -2207
- package/dist/src/core/routing/route.js +0 -539
- package/docs/ACTIVE_SET.md +0 -53
- package/docs/COMPATIBILITY_POLICY.md +0 -22
- package/docs/CONVERSION_AND_SANDBOX.md +0 -27
- package/docs/EVALUATION_PROTOCOL_V1.md +0 -300
- package/docs/HEAD_TO_HEAD_EVALUATION.md +0 -79
- package/docs/PROVIDER_CONFIGURATION.md +0 -45
- package/docs/README_RESEARCH.md +0 -36
- package/docs/REPOSITORY_STABILIZATION.md +0 -190
- package/docs/SAFE_UPDATE_DEMO.md +0 -25
- package/docs/SCHEMA_DECISIONS.md +0 -25
- package/docs/SUBMISSION_COPY.md +0 -90
- package/docs/TEAM_POLICY.md +0 -18
- package/docs/assets/loadout-workflow.png +0 -0
- package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +0 -283
- package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +0 -116
- package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +0 -469
- package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +0 -80
- package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +0 -55
- package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +0 -228
- package/skills/loadout-router/SKILL.md +0 -120
- /package/dist/src/core/{routing → agents}/credentials.js +0 -0
|
@@ -1,139 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 =
|
|
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
|
-
|
|
93
|
-
elif [[ COMP_CWORD -eq 2
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
commands=(${
|
|
107
|
-
|
|
108
|
-
|
|
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 ))
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
...
|
|
123
|
-
...
|
|
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 =
|
|
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}
|
|
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
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
description: typeof
|
|
68
|
-
defaultBranch: typeof
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
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 "../
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
package/docs/DEMO_SCRIPT.md
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
|
116
|
-
loadout
|
|
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
|
-
>
|
|
122
|
-
>
|
|
123
|
-
>
|
|
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:
|
|
127
|
+
### 2:28 to 2:48: close
|
|
126
128
|
|
|
127
|
-
**Screen:**
|
|
129
|
+
**Screen:** Return to the README hero and the handoff row in the workflow image.
|
|
128
130
|
|
|
129
131
|
**Say:**
|
|
130
132
|
|
|
131
|
-
>
|
|
132
|
-
>
|
|
133
|
-
>
|
|
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
|
|
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
|
|
151
|
-
- [ ]
|
|
152
|
-
- [ ]
|
|
153
|
-
- [ ]
|
|
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.
|