loadout-ai 0.1.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/LICENSE +21 -0
- package/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- package/package.json +82 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,2735 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, CommanderError } from "commander";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { explainCatalogScore, loadEffectiveCatalog, loadCatalog, rankCatalog, refreshCatalog, } from "./core/catalog.js";
|
|
6
|
+
import { detectAgents, parseAgentSelection } from "./core/paths.js";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { buildSkillPlan, applySkillInstall, installedAgents, } from "./core/install.js";
|
|
10
|
+
import { listSnapshotIds, readSnapshot, restoreSnapshot, } from "./core/snapshot.js";
|
|
11
|
+
import { fetchRepositorySnapshot } from "./core/source.js";
|
|
12
|
+
import { discoverMcpManifests, summarizeMcpManifest, planMcpConfig, summarizeMcpConfigPlan, applyMcpConfigPlan, } from "./core/mcp.js";
|
|
13
|
+
import { REVIEWED_MCP_RECIPES, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "./core/mcp-recipes.js";
|
|
14
|
+
import { runDoctor, formatDoctorReport } from "./core/doctor.js";
|
|
15
|
+
import { applyPackageUpdate, buildUpdatePlan, formatUpdatePlan, } from "./core/update.js";
|
|
16
|
+
import { startApiServer } from "./core/api.js";
|
|
17
|
+
import { inspectPackage, formatPackageInspection } from "./core/package.js";
|
|
18
|
+
import { addManifestPackage, applyProfileToManifest, initManifest, readManifest, removeManifestPackage, writeLockfile, } from "./core/manifest.js";
|
|
19
|
+
import { buildHealthReport, formatHealthReport } from "./core/health.js";
|
|
20
|
+
import { readInstallState } from "./core/state.js";
|
|
21
|
+
import { applyRemove, planRemove } from "./core/remove.js";
|
|
22
|
+
import { formatRecommendations, personalizeRecommendations, profileManifestPackages, recommendPackages, scanProject, TESTED_PROFILES, } from "./core/recommend.js";
|
|
23
|
+
import { buildImprovementCycle, formatImprovementCycle, recordImprovementOutcome, writeImprovementCycle, } from "./core/improve.js";
|
|
24
|
+
import { applySyncPlan, buildSyncPlan } from "./core/sync.js";
|
|
25
|
+
import { createPackage, packPackage, publishLocalPackage, publishRemotePackage, searchLocalRegistry, } from "./core/registry.js";
|
|
26
|
+
import { startRegistryServer } from "./core/registry-api.js";
|
|
27
|
+
import { auditLoadout, formatAuditReport } from "./core/audit.js";
|
|
28
|
+
import { ADAPTER_CAPABILITIES, buildAdapterCapabilityGaps, formatAdapterCapabilityGaps, formatCapabilityMatrix, } from "./core/adapters.js";
|
|
29
|
+
import { formatAgentInventory, inspectAgents, } from "./core/agent-inspection.js";
|
|
30
|
+
import { generateSigningKeys, signJsonFile, verifyJsonFile, } from "./core/signing.js";
|
|
31
|
+
import { applyPortableImport, exportPortableLoadout, planPortableImport, } from "./core/portable.js";
|
|
32
|
+
import { applyCodexMcpConfigPlan, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "./core/codex-mcp.js";
|
|
33
|
+
import { formatDemoResult, runIsolatedDemo } from "./core/demo.js";
|
|
34
|
+
import { catalogTrustStage, resolveCatalogProfile } from "./core/profiles.js";
|
|
35
|
+
import { discoverHackerNewsRepositories } from "./core/community.js";
|
|
36
|
+
import { discoverPrivateRepositories } from "./core/private-discovery.js";
|
|
37
|
+
import { defaultGitHubDiscoveryQueries, discoverGitHubRepositories, } from "./core/github-discovery.js";
|
|
38
|
+
import { formatStarHistory, readCatalogObservations, } from "./core/observations.js";
|
|
39
|
+
import { evaluatePackage, formatPackageEvaluation } from "./core/evaluate.js";
|
|
40
|
+
import { checkForUpdates, startUpdateWatcher } from "./core/update-watch.js";
|
|
41
|
+
import { runDisposableSandbox } from "./core/sandbox.js";
|
|
42
|
+
import { compileConversion, } from "./core/conversion.js";
|
|
43
|
+
import { writeFileAtomically } from "./core/atomic-file.js";
|
|
44
|
+
import { formatCanaryResult, runCanary } from "./core/canary.js";
|
|
45
|
+
import { startDashboardServer } from "./dashboard.js";
|
|
46
|
+
import { applyPreparedCatalogInstall, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
|
|
47
|
+
import { formatInstalledSkillInventory, scanInstalledSkills, } from "./core/skill-inventory.js";
|
|
48
|
+
import { enrichInventoryWithProvenance, formatProvenanceSummary, resolveCatalogSkillIndex, } from "./core/provenance.js";
|
|
49
|
+
import { compareSkill, formatSkillComparison } from "./core/skill-compare.js";
|
|
50
|
+
import { applyActivationChange, buildLibraryStateReport, formatActivationPlan, formatLibraryStateReport, planActivationChange, } from "./core/active-set.js";
|
|
51
|
+
import { applyProjectActivation, formatProjectActivation, planProjectActivation, } from "./core/active-policy.js";
|
|
52
|
+
import { applySkillAdoption, formatAdoptionPlan, planSkillAdoption, } from "./core/adopt.js";
|
|
53
|
+
import { formatReviewQueue, mergeReviewQueue, readReviewQueue, setReviewDecision, } from "./core/review-queue.js";
|
|
54
|
+
import { applyProviderModelSelection, defaultModelConfigurationPath, formatProviderModelConfiguration, planProviderModelSelection, readProviderModelConfiguration, requestOpenRouter, } from "./core/model-config.js";
|
|
55
|
+
import { applyNativeScheduler, applyNativeSchedulerBundle, formatNativeScheduler, planNativeScheduler, } from "./core/scheduler.js";
|
|
56
|
+
import { REVIEWED_RUNTIME_TOOLS, applyRuntimeToolPlan, formatRuntimeToolPlan, planRuntimeTool, } from "./core/runtime-tools.js";
|
|
57
|
+
import { buildPrivacySafeReport, formatPrivacySafeReport, parsePrivacySafeLoadoutReport, writePrivacySafeReport, } from "./core/share-report.js";
|
|
58
|
+
import { readLocalOutcomes, recordLocalOutcome, } from "./core/outcomes.js";
|
|
59
|
+
import { buildFreshnessAlerts, formatFreshnessAlerts, ignoreFreshnessAlert, pinReplacement, readReplacementPins, unpinReplacement, } from "./core/freshness-alerts.js";
|
|
60
|
+
import { runHeadToHeadHarness, replacementEvidenceFromSignedSnapshot, writeSignedHeadToHeadEvidence, } from "./core/head-to-head.js";
|
|
61
|
+
import { parseCompletionShell, renderShellCompletion, } from "./core/completion.js";
|
|
62
|
+
import { buildCatalogCoverage, formatCatalogCoverage, } from "./core/catalog-coverage.js";
|
|
63
|
+
import { createCredentialResolver, createOsCredentialStore, } from "./core/credentials.js";
|
|
64
|
+
import { buildCandidateDossier, buildCatalogProposal, formatCandidateDossier, formatCandidateSummaries, listDiscoveryCandidates, readCandidateDossier, verifyCandidateDossierSource, writeCandidateDossier, } from "./core/candidate-intelligence.js";
|
|
65
|
+
import { applyCatalogRelease, formatCatalogReleasePreview, previewCatalogRelease, } from "./core/catalog-release.js";
|
|
66
|
+
import { recoverPendingTransactions, withMutationLock, } from "./core/transaction.js";
|
|
67
|
+
import { applyUpgrade, formatUpgradePlan, planUpgrade, summarizeUpgradePlan, } from "./core/upgrade.js";
|
|
68
|
+
import { formatAgentVersions, inspectAgentVersions, } from "./core/agent-versions.js";
|
|
69
|
+
import { formatAgentHealthScore } from "./core/agent-health-score.js";
|
|
70
|
+
import { buildLocalAgentHealthScores } from "./core/health-score-evidence.js";
|
|
71
|
+
import { discoverSkillsSh } from "./core/skills-sh-discovery.js";
|
|
72
|
+
import { discoverOfficialMcpRegistry } from "./core/mcp-registry-discovery.js";
|
|
73
|
+
import { createBenchmarkRun, formatBenchmarkCampaignSummary, parseBenchmarkCampaign, summarizeBenchmarkCampaign, } from "./core/benchmark-campaign.js";
|
|
74
|
+
import { buildLoadoutCard, compareLoadoutReports, formatLoadoutCard, formatLoadoutComparison, } from "./core/loadout-card.js";
|
|
75
|
+
import { buildLoadoutBadge, formatLoadoutBadgeUsage, parseLoadoutBadgeMetric, } from "./core/loadout-badge.js";
|
|
76
|
+
import { auditReleaseClaims, formatReleaseEvidenceIndex, releaseEvidenceRoot, } from "./core/release-claims.js";
|
|
77
|
+
import { applyIntelligenceFeed, previewIntelligenceFeed, readCachedIntelligenceFeed, } from "./core/intelligence-feed.js";
|
|
78
|
+
import { scanSkillSecurity } from "./core/skill-security.js";
|
|
79
|
+
import { planApmImportFiles, planOpenPackageImportFiles, } from "./core/ecosystem-import.js";
|
|
80
|
+
import { buildCompatibilityIntelligence, parseCompatibilityNoticeSet, } from "./core/compatibility-intelligence.js";
|
|
81
|
+
const collectOption = (value, previous = []) => [
|
|
82
|
+
...previous,
|
|
83
|
+
value,
|
|
84
|
+
];
|
|
85
|
+
async function readCredentialFromStdin() {
|
|
86
|
+
if (process.stdin.isTTY)
|
|
87
|
+
throw new Error("Credential input must be piped on stdin; interactive echo is intentionally unsupported");
|
|
88
|
+
const chunks = [];
|
|
89
|
+
let size = 0;
|
|
90
|
+
for await (const chunk of process.stdin) {
|
|
91
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
92
|
+
size += value.length;
|
|
93
|
+
if (size > 64 * 1024)
|
|
94
|
+
throw new Error("Credential input exceeds the 64 KiB safety limit");
|
|
95
|
+
chunks.push(value);
|
|
96
|
+
}
|
|
97
|
+
const secret = Buffer.concat(chunks)
|
|
98
|
+
.toString("utf8")
|
|
99
|
+
.replace(/\r?\n$/, "");
|
|
100
|
+
if (!secret)
|
|
101
|
+
throw new Error("Credential input is empty");
|
|
102
|
+
return secret;
|
|
103
|
+
}
|
|
104
|
+
function setupSelection(mode, packageIds) {
|
|
105
|
+
if (!["stable", "power", "maximum", "custom"].includes(mode))
|
|
106
|
+
throw new Error("--mode must be stable, power, maximum, or custom");
|
|
107
|
+
if (mode === "custom" && packageIds.length === 0)
|
|
108
|
+
throw new Error("Custom setup requires at least one --package <id>");
|
|
109
|
+
if (mode !== "custom" && packageIds.length)
|
|
110
|
+
throw new Error("--package can only be used with --mode custom");
|
|
111
|
+
return {
|
|
112
|
+
mode: mode,
|
|
113
|
+
...(packageIds.length ? { packageIds } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function printSetupProgress(progress) {
|
|
117
|
+
const marker = progress.status === "ready"
|
|
118
|
+
? "✓"
|
|
119
|
+
: progress.status === "skipped"
|
|
120
|
+
? "○"
|
|
121
|
+
: "↓";
|
|
122
|
+
console.error(`${marker} [${progress.completed}/${progress.total}] ${progress.message}`);
|
|
123
|
+
}
|
|
124
|
+
function printProvenanceProgress(progress) {
|
|
125
|
+
const marker = progress.status === "ready"
|
|
126
|
+
? "✓"
|
|
127
|
+
: progress.status === "failed"
|
|
128
|
+
? "○"
|
|
129
|
+
: "↓";
|
|
130
|
+
console.error(`${marker} [${progress.completed}/${progress.total}] ${progress.message}`);
|
|
131
|
+
}
|
|
132
|
+
function riskyPackageSummary(prepared) {
|
|
133
|
+
return prepared.entries
|
|
134
|
+
.filter((entry) => entry.safety.approvalRequired)
|
|
135
|
+
.map((entry) => {
|
|
136
|
+
const categories = [
|
|
137
|
+
...new Set(entry.safety.findings.map((finding) => finding.category)),
|
|
138
|
+
];
|
|
139
|
+
return `${entry.package.id} (${categories.join(", ")})`;
|
|
140
|
+
})
|
|
141
|
+
.join(", ");
|
|
142
|
+
}
|
|
143
|
+
async function runSetup(options) {
|
|
144
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
145
|
+
let mode = options.mode;
|
|
146
|
+
let packageIds = options.package ?? [];
|
|
147
|
+
let reader;
|
|
148
|
+
try {
|
|
149
|
+
if (!mode) {
|
|
150
|
+
if (!interactive) {
|
|
151
|
+
console.log("Loadout is CLI-first. Run `loadout setup --mode stable` for the recommended daily driver, `--mode power` for broader opt-in skills, or `--mode maximum` to download the screened library without activating it.");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
reader = createInterface({
|
|
155
|
+
input: process.stdin,
|
|
156
|
+
output: process.stdout,
|
|
157
|
+
});
|
|
158
|
+
const answer = (await reader.question("Choose a loadout: [1] Stable Daily Driver (recommended), [2] Power Boost, [3] Maximum Library, [4] Custom: ")).trim();
|
|
159
|
+
mode =
|
|
160
|
+
answer === "2"
|
|
161
|
+
? "power"
|
|
162
|
+
: answer === "3"
|
|
163
|
+
? "maximum"
|
|
164
|
+
: answer === "4"
|
|
165
|
+
? "custom"
|
|
166
|
+
: "stable";
|
|
167
|
+
if (mode === "custom") {
|
|
168
|
+
const custom = await reader.question("Enter comma-separated catalog package ids: ");
|
|
169
|
+
packageIds = custom
|
|
170
|
+
.split(",")
|
|
171
|
+
.map((value) => value.trim())
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const selection = setupSelection(mode, packageIds);
|
|
176
|
+
console.log("\nPreparing a read-only install plan from screened immutable commits…");
|
|
177
|
+
const prepared = await prepareCatalogInstall(selection, {
|
|
178
|
+
requestedAgents: parseAgentSelection(options.agents),
|
|
179
|
+
onProgress: printSetupProgress,
|
|
180
|
+
});
|
|
181
|
+
console.log(`\n${formatPreparedCatalogInstall(prepared)}\n`);
|
|
182
|
+
const risky = riskyPackageSummary(prepared);
|
|
183
|
+
let approved = Boolean(options.yes);
|
|
184
|
+
let riskApproved = Boolean(options.approveRisk);
|
|
185
|
+
if (!approved) {
|
|
186
|
+
if (!interactive) {
|
|
187
|
+
console.log("Preview complete; nothing was changed. Re-run with --yes to install this exact screened plan.");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
reader ??= createInterface({
|
|
191
|
+
input: process.stdin,
|
|
192
|
+
output: process.stdout,
|
|
193
|
+
});
|
|
194
|
+
approved = /^(?:y|yes)$/i.test((await reader.question("Install this loadout as one rollback-safe transaction? [y/N] ")).trim());
|
|
195
|
+
if (!approved) {
|
|
196
|
+
console.log("Cancelled; no agent files were changed.");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (risky && !riskApproved) {
|
|
201
|
+
if (!interactive)
|
|
202
|
+
throw new Error(`The screened skills contain additional safety findings: ${risky}. Inspect the preview and add --approve-risk to proceed.`);
|
|
203
|
+
reader ??= createInterface({
|
|
204
|
+
input: process.stdin,
|
|
205
|
+
output: process.stdout,
|
|
206
|
+
});
|
|
207
|
+
console.log(`Additional safety findings: ${risky}`);
|
|
208
|
+
riskApproved = /^(?:y|yes)$/i.test((await reader.question("Approve these reviewed script/domain/instruction findings? [y/N] ")).trim());
|
|
209
|
+
if (!riskApproved) {
|
|
210
|
+
console.log("Cancelled; no agent files were changed.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const snapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
215
|
+
approveRisk: riskApproved,
|
|
216
|
+
});
|
|
217
|
+
console.log(`\nLoadout installed ${prepared.entries.length} repositories for ${prepared.agents.length} agent(s). Snapshot: ${snapshotId}`);
|
|
218
|
+
console.log("Next: `loadout status`, `loadout optimize --project .`, or `loadout autopilot --yes` for opt-in daily read-only discovery and update checks.");
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
reader?.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const LOADOUT_VERSION = "0.1.0";
|
|
225
|
+
function durableSchedulerLauncher() {
|
|
226
|
+
return [
|
|
227
|
+
join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
|
|
228
|
+
"--yes",
|
|
229
|
+
`loadout-ai@${LOADOUT_VERSION}`,
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
const program = new Command();
|
|
233
|
+
program
|
|
234
|
+
.name("loadout")
|
|
235
|
+
.description("The trusted upgrade layer for AI coding agents")
|
|
236
|
+
.version(LOADOUT_VERSION)
|
|
237
|
+
.option("--json-errors", "emit a machine-readable error object on stderr; normal output is unchanged")
|
|
238
|
+
// Commander normally calls process.exit() immediately after rendering help
|
|
239
|
+
// or version output. Large top-level help can then be truncated to one pipe
|
|
240
|
+
// buffer when Loadout is invoked by another process. Keep control in this
|
|
241
|
+
// module so Node has time to flush stdout before exiting naturally.
|
|
242
|
+
.exitOverride()
|
|
243
|
+
// The catch block below is the single error renderer. Commander otherwise
|
|
244
|
+
// writes its own parse error first, producing two stderr documents and
|
|
245
|
+
// breaking --json-errors consumers.
|
|
246
|
+
.configureOutput({ writeErr: () => undefined });
|
|
247
|
+
program
|
|
248
|
+
.command("setup")
|
|
249
|
+
.description("Preview and install a screened skill loadout for detected agents")
|
|
250
|
+
.option("--mode <mode>", "stable, power, maximum, or custom")
|
|
251
|
+
.option("--agents <ids>", "comma-separated target agent ids")
|
|
252
|
+
.option("--package <id>", "package id for custom mode", collectOption, [])
|
|
253
|
+
.option("-y, --yes", "install after preparing the screened plan")
|
|
254
|
+
.option("--approve-risk", "approve reviewed safety findings in non-interactive mode")
|
|
255
|
+
.action((options) => runSetup(options));
|
|
256
|
+
program
|
|
257
|
+
.command("upgrade")
|
|
258
|
+
.description("Diagnose, recommend, preview, and transactionally apply one screened upgrade")
|
|
259
|
+
.option("--mode <mode>", "stable, power, maximum, or custom", "stable")
|
|
260
|
+
.option("--project <path>", "project directory", process.cwd())
|
|
261
|
+
.option("--agents <ids>", "comma-separated target agent ids")
|
|
262
|
+
.option("--package <id>", "package id for custom mode", collectOption, [])
|
|
263
|
+
.option("--yes", "apply the exact displayed upgrade")
|
|
264
|
+
.option("--approve-risk", "approve the displayed reviewed safety findings")
|
|
265
|
+
.option("--json", "emit a machine-readable preview or result")
|
|
266
|
+
.action(async (options) => {
|
|
267
|
+
const plan = await planUpgrade(setupSelection(options.mode, options.package), {
|
|
268
|
+
projectPath: options.project,
|
|
269
|
+
requestedAgents: parseAgentSelection(options.agents),
|
|
270
|
+
onProgress: options.json ? undefined : printSetupProgress,
|
|
271
|
+
});
|
|
272
|
+
if (!options.yes) {
|
|
273
|
+
console.log(options.json
|
|
274
|
+
? JSON.stringify(summarizeUpgradePlan(plan), null, 2)
|
|
275
|
+
: `${formatUpgradePlan(plan)}\n\nPreview complete; nothing was changed. Re-run with --yes${plan.riskApprovalRequired ? " --approve-risk" : ""} to apply this exact upgrade.`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const result = await applyUpgrade(plan, {
|
|
279
|
+
approveRisk: options.approveRisk,
|
|
280
|
+
});
|
|
281
|
+
console.log(options.json
|
|
282
|
+
? JSON.stringify({
|
|
283
|
+
plan: summarizeUpgradePlan(plan),
|
|
284
|
+
result,
|
|
285
|
+
}, null, 2)
|
|
286
|
+
: [
|
|
287
|
+
`Upgrade applied as one transaction. Snapshot: ${result.snapshotId}`,
|
|
288
|
+
formatHealthReport(result.healthAfter),
|
|
289
|
+
...result.healthScoresAfter.map((score) => `Agent Health Score (${score.agent}): ${score.score}/100 (${score.rating}; evidence coverage ${score.evidenceCoverage}%)`),
|
|
290
|
+
"Scores summarize stored evidence only; they do not claim task improvement until benchmark or local outcome evidence exists.",
|
|
291
|
+
"Next: run `loadout rollback` to restore or `loadout outcome` after real use.",
|
|
292
|
+
].join("\n"));
|
|
293
|
+
});
|
|
294
|
+
program
|
|
295
|
+
.command("init")
|
|
296
|
+
.description("Create a shareable loadout.json manifest")
|
|
297
|
+
.option("--path <path>", "manifest path", "loadout.json")
|
|
298
|
+
.option("--name <name>", "Loadout name")
|
|
299
|
+
.option("--agents <ids>", "comma-separated agent ids", "codex,claude-code")
|
|
300
|
+
.option("--scope <scope>", "project or global", "project")
|
|
301
|
+
.action(async (options) => {
|
|
302
|
+
const manifest = await initManifest(options.path, {
|
|
303
|
+
name: options.name,
|
|
304
|
+
agents: parseAgentSelection(options.agents),
|
|
305
|
+
scope: options.scope,
|
|
306
|
+
});
|
|
307
|
+
console.log(`Created ${options.path} for ${manifest.agents.join(", ")}.`);
|
|
308
|
+
});
|
|
309
|
+
program
|
|
310
|
+
.command("lock")
|
|
311
|
+
.description("Write exact installed state to loadout.lock")
|
|
312
|
+
.option("--manifest <path>", "manifest path", "loadout.json")
|
|
313
|
+
.option("--output <path>", "lockfile path", "loadout.lock")
|
|
314
|
+
.action(async (options) => {
|
|
315
|
+
const lockfile = await writeLockfile(await readManifest(options.manifest), options.output);
|
|
316
|
+
console.log(`Wrote ${options.output} with ${lockfile.packages.length} resolved package(s).`);
|
|
317
|
+
});
|
|
318
|
+
program
|
|
319
|
+
.command("export")
|
|
320
|
+
.description("Export a portable Loadout manifest and optional lockfile")
|
|
321
|
+
.argument("<output>", "new portable JSON file")
|
|
322
|
+
.option("--manifest <path>", "manifest path", "loadout.json")
|
|
323
|
+
.option("--lock <path>", "include this exact lockfile")
|
|
324
|
+
.action(async (output, options) => {
|
|
325
|
+
const bundle = await exportPortableLoadout(options.manifest, output, options.lock);
|
|
326
|
+
console.log(`Exported ${bundle.manifest.packages.length} package(s) to ${output}.${bundle.lockfile ? " Exact lockfile included." : ""}`);
|
|
327
|
+
});
|
|
328
|
+
program
|
|
329
|
+
.command("import")
|
|
330
|
+
.description("Preview or apply a portable Loadout manifest and lockfile")
|
|
331
|
+
.argument("<source>", "portable JSON file")
|
|
332
|
+
.option("--manifest <path>", "manifest destination", "loadout.json")
|
|
333
|
+
.option("--lock <path>", "lockfile destination", "loadout.lock")
|
|
334
|
+
.option("--yes", "apply the import; otherwise remain read-only")
|
|
335
|
+
.option("--overwrite", "replace existing destination files after snapshotting them")
|
|
336
|
+
.action(async (source, options) => {
|
|
337
|
+
const preview = await planPortableImport(source, options.manifest, options.lock);
|
|
338
|
+
console.log(JSON.stringify(preview.plan, null, 2));
|
|
339
|
+
if (!options.yes)
|
|
340
|
+
return console.log("Dry run only. Re-run with --yes to import this Loadout.");
|
|
341
|
+
const result = await applyPortableImport(source, options.manifest, options.lock, { overwrite: options.overwrite });
|
|
342
|
+
console.log(`Imported successfully. Recovery snapshot: ${result.snapshotId}.`);
|
|
343
|
+
});
|
|
344
|
+
program
|
|
345
|
+
.command("audit")
|
|
346
|
+
.description("Verify manifest, lockfile, installed state, and managed file hashes for CI")
|
|
347
|
+
.option("--manifest <path>", "manifest path", "loadout.json")
|
|
348
|
+
.option("--lock <path>", "lockfile path", "loadout.lock")
|
|
349
|
+
.option("--json", "emit machine-readable JSON")
|
|
350
|
+
.action(async (options) => {
|
|
351
|
+
const report = await auditLoadout(options.manifest, options.lock);
|
|
352
|
+
console.log(options.json
|
|
353
|
+
? JSON.stringify(report, null, 2)
|
|
354
|
+
: formatAuditReport(report));
|
|
355
|
+
if (!report.valid)
|
|
356
|
+
process.exitCode = 1;
|
|
357
|
+
});
|
|
358
|
+
program
|
|
359
|
+
.command("create")
|
|
360
|
+
.description("Create a new Loadout package directory")
|
|
361
|
+
.argument("<directory>", "new package directory")
|
|
362
|
+
.requiredOption("--name <name>", "lowercase package name")
|
|
363
|
+
.option("--description <text>", "package description")
|
|
364
|
+
.option("--version <version>", "semantic version", "0.1.0")
|
|
365
|
+
.action(async (directory, options) => {
|
|
366
|
+
const descriptor = await createPackage(directory, options);
|
|
367
|
+
console.log(`Created ${descriptor.name}@${descriptor.version} in ${directory}.`);
|
|
368
|
+
});
|
|
369
|
+
program
|
|
370
|
+
.command("pack")
|
|
371
|
+
.description("Validate a package and print its deterministic inventory digest")
|
|
372
|
+
.argument("[directory]", "package directory", ".")
|
|
373
|
+
.option("--json", "emit machine-readable JSON")
|
|
374
|
+
.action(async (directory, options) => {
|
|
375
|
+
const packed = await packPackage(directory);
|
|
376
|
+
console.log(options.json
|
|
377
|
+
? JSON.stringify(packed, null, 2)
|
|
378
|
+
: `${packed.descriptor.name}@${packed.descriptor.version} — ${packed.files.length} file(s) — sha256:${packed.digest}`);
|
|
379
|
+
});
|
|
380
|
+
program
|
|
381
|
+
.command("publish")
|
|
382
|
+
.description("Publish an immutable package version to a local or remote Loadout registry")
|
|
383
|
+
.argument("[directory]", "package directory", ".")
|
|
384
|
+
.option("--local", "publish to the local registry")
|
|
385
|
+
.option("--registry-url <url>", "remote registry base URL")
|
|
386
|
+
.option("--credential-keychain <service>", "resolve the remote registry token from the OS credential store")
|
|
387
|
+
.option("--credential-account <account>", "OS credential account")
|
|
388
|
+
.option("--approve-risk", "explicitly approve publishing scripts, hooks, or binaries")
|
|
389
|
+
.action(async (directory, options) => {
|
|
390
|
+
if (Number(Boolean(options.local)) +
|
|
391
|
+
Number(Boolean(options.registryUrl)) !==
|
|
392
|
+
1)
|
|
393
|
+
throw new Error("Choose exactly one destination: --local or --registry-url");
|
|
394
|
+
if (options.local) {
|
|
395
|
+
if (options.credentialKeychain)
|
|
396
|
+
throw new Error("Local publishing does not require a credential");
|
|
397
|
+
const packed = await publishLocalPackage(directory, {
|
|
398
|
+
approveRisk: options.approveRisk,
|
|
399
|
+
});
|
|
400
|
+
console.log(`Published ${packed.descriptor.name}@${packed.descriptor.version} with digest ${packed.digest}.`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const token = await createCredentialResolver()(options.credentialKeychain
|
|
404
|
+
? {
|
|
405
|
+
kind: "os-keychain",
|
|
406
|
+
service: options.credentialKeychain,
|
|
407
|
+
...(options.credentialAccount
|
|
408
|
+
? { account: options.credentialAccount }
|
|
409
|
+
: {}),
|
|
410
|
+
}
|
|
411
|
+
: { kind: "environment", name: "LOADOUT_REGISTRY_TOKEN" });
|
|
412
|
+
if (!token)
|
|
413
|
+
throw new Error("Remote publishing credential did not resolve; set LOADOUT_REGISTRY_TOKEN or use --credential-keychain");
|
|
414
|
+
const published = await publishRemotePackage(directory, options.registryUrl, token, { approveRisk: options.approveRisk });
|
|
415
|
+
console.log(`Published ${published.name}@${published.version} with digest ${published.digest}.`);
|
|
416
|
+
});
|
|
417
|
+
program
|
|
418
|
+
.command("registry-serve")
|
|
419
|
+
.description("Run the Loadout registry protocol server")
|
|
420
|
+
.option("--host <host>", "listen host", "127.0.0.1")
|
|
421
|
+
.option("--port <port>", "listen port", "7331")
|
|
422
|
+
.option("--credential-keychain <service>", "resolve the server token from the OS credential store")
|
|
423
|
+
.option("--credential-account <account>", "OS credential account")
|
|
424
|
+
.action(async (options) => {
|
|
425
|
+
const token = await createCredentialResolver()(options.credentialKeychain
|
|
426
|
+
? {
|
|
427
|
+
kind: "os-keychain",
|
|
428
|
+
service: options.credentialKeychain,
|
|
429
|
+
...(options.credentialAccount
|
|
430
|
+
? { account: options.credentialAccount }
|
|
431
|
+
: {}),
|
|
432
|
+
}
|
|
433
|
+
: { kind: "environment", name: "LOADOUT_REGISTRY_TOKEN" });
|
|
434
|
+
if (!token)
|
|
435
|
+
throw new Error("Set LOADOUT_REGISTRY_TOKEN before starting a registry server");
|
|
436
|
+
const handle = await startRegistryServer({
|
|
437
|
+
host: options.host,
|
|
438
|
+
port: Number(options.port),
|
|
439
|
+
token,
|
|
440
|
+
});
|
|
441
|
+
console.log(`Loadout registry listening at http://${handle.host}:${handle.port}.`);
|
|
442
|
+
await new Promise((resolve) => {
|
|
443
|
+
const stop = () => {
|
|
444
|
+
process.off("SIGINT", stop);
|
|
445
|
+
process.off("SIGTERM", stop);
|
|
446
|
+
void handle.close().then(resolve);
|
|
447
|
+
};
|
|
448
|
+
process.on("SIGINT", stop);
|
|
449
|
+
process.on("SIGTERM", stop);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
program
|
|
453
|
+
.command("search")
|
|
454
|
+
.description("Search the bundled catalog and local registry")
|
|
455
|
+
.argument("[query]", "search text", "")
|
|
456
|
+
.option("--json", "emit machine-readable JSON")
|
|
457
|
+
.action(async (query, options) => {
|
|
458
|
+
const catalog = (await loadEffectiveCatalog())
|
|
459
|
+
.filter((pkg) => !query ||
|
|
460
|
+
`${pkg.id} ${pkg.displayName} ${pkg.description}`
|
|
461
|
+
.toLowerCase()
|
|
462
|
+
.includes(query.toLowerCase()))
|
|
463
|
+
.map((pkg) => ({
|
|
464
|
+
source: "catalog",
|
|
465
|
+
name: pkg.id,
|
|
466
|
+
description: pkg.description,
|
|
467
|
+
repository: pkg.repository,
|
|
468
|
+
}));
|
|
469
|
+
const local = (await searchLocalRegistry(query)).map((pkg) => ({
|
|
470
|
+
source: "registry",
|
|
471
|
+
...pkg,
|
|
472
|
+
}));
|
|
473
|
+
if (options.json)
|
|
474
|
+
return console.log(JSON.stringify([...catalog, ...local], null, 2));
|
|
475
|
+
for (const item of [...catalog, ...local])
|
|
476
|
+
console.log(`${item.name}${"version" in item ? `@${item.version}` : ""} [${item.source}] — ${item.description}`);
|
|
477
|
+
if (!catalog.length && !local.length)
|
|
478
|
+
console.log("No matching packages found.");
|
|
479
|
+
});
|
|
480
|
+
program
|
|
481
|
+
.command("add")
|
|
482
|
+
.description("Add a catalog, GitHub, or local package to loadout.json")
|
|
483
|
+
.argument("<id>", "package id")
|
|
484
|
+
.option("--manifest <path>", "manifest path", "loadout.json")
|
|
485
|
+
.option("--catalog <id>", "catalog package id")
|
|
486
|
+
.option("--repository <owner/repo>", "public GitHub repository")
|
|
487
|
+
.option("--git <url>", "generic HTTPS or SSH Git repository")
|
|
488
|
+
.option("--registry <name@version>", "exact local registry package version")
|
|
489
|
+
.option("--remote-registry <url>", "fetch --registry name@version from this remote registry")
|
|
490
|
+
.option("--ref <ref>", "Git branch, tag, or ref")
|
|
491
|
+
.option("--path <path>", "GitHub repository subpath or local path")
|
|
492
|
+
.option("--local", "treat --path as a local source")
|
|
493
|
+
.option("--agents <ids>", "comma-separated target agents")
|
|
494
|
+
.option("--depends-on <ids>", "comma-separated package dependencies")
|
|
495
|
+
.action(async (id, options) => {
|
|
496
|
+
const selected = Number(Boolean(options.catalog)) +
|
|
497
|
+
Number(Boolean(options.repository)) +
|
|
498
|
+
Number(Boolean(options.git)) +
|
|
499
|
+
Number(Boolean(options.registry)) +
|
|
500
|
+
Number(Boolean(options.local));
|
|
501
|
+
if (selected !== 1)
|
|
502
|
+
throw new Error("Choose exactly one source: --catalog, --repository, --git, --registry, or --local with --path");
|
|
503
|
+
if (options.local && !options.path)
|
|
504
|
+
throw new Error("--local requires --path <directory>");
|
|
505
|
+
const registry = options.registry?.match(/^([a-z0-9][a-z0-9._-]*)@(.+)$/);
|
|
506
|
+
if (options.registry && !registry)
|
|
507
|
+
throw new Error("--registry expects name@version");
|
|
508
|
+
if (options.remoteRegistry && !registry)
|
|
509
|
+
throw new Error("--remote-registry requires --registry name@version");
|
|
510
|
+
const source = options.catalog
|
|
511
|
+
? { type: "catalog", id: options.catalog }
|
|
512
|
+
: options.repository
|
|
513
|
+
? {
|
|
514
|
+
type: "github",
|
|
515
|
+
repository: options.repository,
|
|
516
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
517
|
+
...(options.path ? { path: options.path } : {}),
|
|
518
|
+
}
|
|
519
|
+
: options.git
|
|
520
|
+
? {
|
|
521
|
+
type: "git",
|
|
522
|
+
url: options.git,
|
|
523
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
524
|
+
...(options.path ? { path: options.path } : {}),
|
|
525
|
+
}
|
|
526
|
+
: registry && options.remoteRegistry
|
|
527
|
+
? {
|
|
528
|
+
type: "remote-registry",
|
|
529
|
+
registry: options.remoteRegistry,
|
|
530
|
+
name: registry[1],
|
|
531
|
+
version: registry[2],
|
|
532
|
+
}
|
|
533
|
+
: registry
|
|
534
|
+
? {
|
|
535
|
+
type: "registry",
|
|
536
|
+
name: registry[1],
|
|
537
|
+
version: registry[2],
|
|
538
|
+
}
|
|
539
|
+
: { type: "local", path: options.path };
|
|
540
|
+
const manifest = await addManifestPackage(options.manifest, {
|
|
541
|
+
id,
|
|
542
|
+
source,
|
|
543
|
+
...(options.agents
|
|
544
|
+
? { agents: parseAgentSelection(options.agents) }
|
|
545
|
+
: {}),
|
|
546
|
+
...(options.dependsOn
|
|
547
|
+
? { dependsOn: options.dependsOn.split(",") }
|
|
548
|
+
: {}),
|
|
549
|
+
});
|
|
550
|
+
console.log(`Added ${id} to ${options.manifest}. ${manifest.packages.length} package(s) configured.`);
|
|
551
|
+
});
|
|
552
|
+
program
|
|
553
|
+
.command("unadd")
|
|
554
|
+
.description("Remove a desired package from loadout.json without touching installed files")
|
|
555
|
+
.argument("<id>", "package id")
|
|
556
|
+
.option("--manifest <path>", "manifest path", "loadout.json")
|
|
557
|
+
.action(async (id, options) => {
|
|
558
|
+
const manifest = await removeManifestPackage(options.manifest, id);
|
|
559
|
+
console.log(`Removed ${id} from ${options.manifest}. ${manifest.packages.length} package(s) configured.`);
|
|
560
|
+
});
|
|
561
|
+
program
|
|
562
|
+
.command("list")
|
|
563
|
+
.alias("ls")
|
|
564
|
+
.description("List packages managed by Loadout")
|
|
565
|
+
.option("--json", "emit machine-readable JSON")
|
|
566
|
+
.action(async (options) => {
|
|
567
|
+
const state = await readInstallState();
|
|
568
|
+
if (options.json)
|
|
569
|
+
return console.log(JSON.stringify(state.installs, null, 2));
|
|
570
|
+
if (!state.installs.length)
|
|
571
|
+
return console.log("No Loadout-managed packages are installed.");
|
|
572
|
+
for (const item of state.installs)
|
|
573
|
+
console.log(`${item.packageId} — ${item.targetAgents.join(", ")} — ${item.resolvedCommit?.slice(0, 12) ?? "local"} — ${item.files.length} file(s)`);
|
|
574
|
+
});
|
|
575
|
+
program
|
|
576
|
+
.command("library")
|
|
577
|
+
.description("Show separate cache, review, installation, and per-agent activation state")
|
|
578
|
+
.option("--json", "emit machine-readable JSON")
|
|
579
|
+
.action(async (options) => {
|
|
580
|
+
const report = await buildLibraryStateReport();
|
|
581
|
+
console.log(options.json
|
|
582
|
+
? JSON.stringify(report, null, 2)
|
|
583
|
+
: formatLibraryStateReport(report));
|
|
584
|
+
});
|
|
585
|
+
program
|
|
586
|
+
.command("report")
|
|
587
|
+
.description("Print a privacy-safe shareable summary without paths, code, prompts, or secrets")
|
|
588
|
+
.option("--json", "emit the machine-readable artifact")
|
|
589
|
+
.action(async (options) => {
|
|
590
|
+
const report = await buildPrivacySafeReport();
|
|
591
|
+
console.log(options.json
|
|
592
|
+
? JSON.stringify(report, null, 2)
|
|
593
|
+
: formatPrivacySafeReport(report));
|
|
594
|
+
});
|
|
595
|
+
program
|
|
596
|
+
.command("outcomes")
|
|
597
|
+
.description("Show privacy-safe local outcome signals; never uploads project or prompt data")
|
|
598
|
+
.option("--json", "emit machine-readable JSON")
|
|
599
|
+
.action(async (options) => {
|
|
600
|
+
const store = await readLocalOutcomes();
|
|
601
|
+
console.log(options.json
|
|
602
|
+
? JSON.stringify(store, null, 2)
|
|
603
|
+
: [
|
|
604
|
+
`Local outcomes: ${store.events.length}`,
|
|
605
|
+
...store.events.map((event) => `${event.recordedAt} — ${event.selector} — ${event.agent}/${event.taskFamily} — ${event.result}`),
|
|
606
|
+
"Privacy: local only; no project names, paths, prompts, code, filenames, or secrets.",
|
|
607
|
+
].join("\n"));
|
|
608
|
+
});
|
|
609
|
+
program
|
|
610
|
+
.command("outcome")
|
|
611
|
+
.description("Record an explicit local, agent/task-scoped skill outcome")
|
|
612
|
+
.argument("<selector>", "exact package/skill selector")
|
|
613
|
+
.requiredOption("--agent <id>", "agent id")
|
|
614
|
+
.requiredOption("--task <family>", "general, frontend, testing, javascript, python, backend, security, or documentation")
|
|
615
|
+
.requiredOption("--result <value>", "accept, reject, success, failure, activation, disable, or rollback")
|
|
616
|
+
.action(async (selector, options) => {
|
|
617
|
+
const knownAgents = new Set((await detectAgents()).map((agent) => agent.id));
|
|
618
|
+
if (!knownAgents.has(options.agent))
|
|
619
|
+
throw new Error(`Unknown agent id: ${options.agent}`);
|
|
620
|
+
const event = await recordLocalOutcome({
|
|
621
|
+
selector,
|
|
622
|
+
agent: options.agent,
|
|
623
|
+
taskFamily: options.task,
|
|
624
|
+
result: options.result,
|
|
625
|
+
});
|
|
626
|
+
console.log(`Recorded local outcome ${event.id}. No project, prompt, or source data was stored.`);
|
|
627
|
+
});
|
|
628
|
+
program
|
|
629
|
+
.command("share")
|
|
630
|
+
.description("Write the privacy-safe Loadout report to a JSON artifact")
|
|
631
|
+
.argument("<output>", "new or replacement report path")
|
|
632
|
+
.action(async (output) => {
|
|
633
|
+
const report = await buildPrivacySafeReport();
|
|
634
|
+
await writePrivacySafeReport(output, report);
|
|
635
|
+
console.log(`Wrote privacy-safe Loadout report to ${output}. Review it before sharing.`);
|
|
636
|
+
});
|
|
637
|
+
program
|
|
638
|
+
.command("card")
|
|
639
|
+
.description("Render a privacy-safe Markdown evidence card without project or repository details")
|
|
640
|
+
.option("--output <path>", "write the Markdown card to a file")
|
|
641
|
+
.option("--json", "emit the underlying privacy-safe card as JSON")
|
|
642
|
+
.action(async (options) => {
|
|
643
|
+
if (options.output && options.json)
|
|
644
|
+
throw new Error("--output and --json cannot be used together");
|
|
645
|
+
const card = await buildLoadoutCard();
|
|
646
|
+
const markdown = formatLoadoutCard(card);
|
|
647
|
+
if (options.output) {
|
|
648
|
+
await writeFileAtomically(resolve(options.output), `${markdown}\n`);
|
|
649
|
+
console.log(`Wrote privacy-safe Loadout card to ${resolve(options.output)}. Review it before sharing.`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
console.log(options.json ? JSON.stringify(card, null, 2) : markdown);
|
|
653
|
+
});
|
|
654
|
+
program
|
|
655
|
+
.command("compare-loadouts")
|
|
656
|
+
.description("Compare aggregate counts from two explicit privacy-safe Loadout reports")
|
|
657
|
+
.argument("<left>", "earlier privacy-safe report JSON")
|
|
658
|
+
.argument("<right>", "later privacy-safe report JSON")
|
|
659
|
+
.option("--json", "emit machine-readable count deltas")
|
|
660
|
+
.action(async (left, right, options) => {
|
|
661
|
+
const [before, after] = await Promise.all([left, right].map(async (path) => parsePrivacySafeLoadoutReport(JSON.parse(await readFile(resolve(path), "utf8")))));
|
|
662
|
+
const comparison = compareLoadoutReports(before, after);
|
|
663
|
+
console.log(options.json
|
|
664
|
+
? JSON.stringify(comparison, null, 2)
|
|
665
|
+
: formatLoadoutComparison(comparison));
|
|
666
|
+
});
|
|
667
|
+
for (const workflow of ["activate", "optimize"]) {
|
|
668
|
+
program
|
|
669
|
+
.command(workflow)
|
|
670
|
+
.description(workflow === "activate"
|
|
671
|
+
? "Select and activate reviewed library skills for a project"
|
|
672
|
+
: "Scan a project and propose the best reviewed active-set additions")
|
|
673
|
+
.option("--project <path>", "project directory to scan", ".")
|
|
674
|
+
.option("--agents <ids>", "comma-separated agent ids")
|
|
675
|
+
.option("--limit <count>", "maximum active skills per agent", "40")
|
|
676
|
+
.option("--pin <selector>", "always prioritize package/skill or skill", collectOption, [])
|
|
677
|
+
.option("--yes", "apply the proposed activation transaction")
|
|
678
|
+
.option("--json", "emit machine-readable JSON")
|
|
679
|
+
.action(async (options) => {
|
|
680
|
+
const agents = options.agents
|
|
681
|
+
?.split(",")
|
|
682
|
+
.map((value) => value.trim())
|
|
683
|
+
.filter(Boolean);
|
|
684
|
+
const plan = await planProjectActivation(options.project, {
|
|
685
|
+
...(agents?.length ? { agents } : {}),
|
|
686
|
+
limit: Number(options.limit),
|
|
687
|
+
pins: options.pin,
|
|
688
|
+
});
|
|
689
|
+
if (!options.yes) {
|
|
690
|
+
console.log(options.json
|
|
691
|
+
? JSON.stringify(plan, null, 2)
|
|
692
|
+
: `${formatProjectActivation(plan)}\nDry run only. Re-run with --yes to activate this reviewed set.`);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const snapshotId = await applyProjectActivation(plan);
|
|
696
|
+
console.log(options.json
|
|
697
|
+
? JSON.stringify({ plan, snapshotId }, null, 2)
|
|
698
|
+
: `${formatProjectActivation(plan)}\nApplied and verified. Snapshot: ${snapshotId}\nRollback: loadout rollback --snapshot ${snapshotId}`);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
for (const action of [
|
|
702
|
+
"enable",
|
|
703
|
+
"disable",
|
|
704
|
+
]) {
|
|
705
|
+
program
|
|
706
|
+
.command(action)
|
|
707
|
+
.description(`${action === "enable" ? "Activate" : "Deactivate"} Loadout-managed skills without deleting the reviewed-library copy`)
|
|
708
|
+
.argument("<packages...>", "one or more managed package ids")
|
|
709
|
+
.option("--agents <ids>", "comma-separated agent ids")
|
|
710
|
+
.option("--yes", "apply the transaction; otherwise show a plan")
|
|
711
|
+
.option("--json", "emit machine-readable JSON")
|
|
712
|
+
.action(async (packageIds, options) => {
|
|
713
|
+
const agents = options.agents
|
|
714
|
+
?.split(",")
|
|
715
|
+
.map((value) => value.trim())
|
|
716
|
+
.filter(Boolean);
|
|
717
|
+
if (agents?.length) {
|
|
718
|
+
const known = new Set((await detectAgents()).map((agent) => agent.id));
|
|
719
|
+
const unknown = agents.filter((agent) => !known.has(agent));
|
|
720
|
+
if (unknown.length)
|
|
721
|
+
throw new Error(`Unknown agent id(s): ${unknown.join(", ")}`);
|
|
722
|
+
}
|
|
723
|
+
const plan = await planActivationChange(action, packageIds, {
|
|
724
|
+
...(agents?.length ? { agents } : {}),
|
|
725
|
+
});
|
|
726
|
+
if (!options.yes) {
|
|
727
|
+
console.log(options.json
|
|
728
|
+
? JSON.stringify(plan, null, 2)
|
|
729
|
+
: `${formatActivationPlan(plan)}\nDry run only. Re-run with --yes to apply this exact transaction.`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const snapshotId = await applyActivationChange(plan);
|
|
733
|
+
console.log(options.json
|
|
734
|
+
? JSON.stringify({ plan, snapshotId }, null, 2)
|
|
735
|
+
: `${formatActivationPlan(plan)}\nApplied. Snapshot: ${snapshotId}`);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
program
|
|
739
|
+
.command("autopilot")
|
|
740
|
+
.description("Preview or enable both daily read-only discovery and update radar jobs")
|
|
741
|
+
.option("--time <HH:MM>", "local daily check time", "09:00")
|
|
742
|
+
.option("--remove", "remove both daily radar jobs")
|
|
743
|
+
.option("--yes", "apply both native scheduler changes atomically")
|
|
744
|
+
.option("--json", "emit machine-readable JSON")
|
|
745
|
+
.action(async (options) => {
|
|
746
|
+
const action = options.remove
|
|
747
|
+
? "unschedule"
|
|
748
|
+
: "schedule";
|
|
749
|
+
const plans = ["updates", "discovery"].map((job) => planNativeScheduler(action, {
|
|
750
|
+
time: options.time,
|
|
751
|
+
launcher: durableSchedulerLauncher(),
|
|
752
|
+
job,
|
|
753
|
+
}));
|
|
754
|
+
if (!options.yes) {
|
|
755
|
+
console.log(options.json
|
|
756
|
+
? JSON.stringify({ action, plans }, null, 2)
|
|
757
|
+
: `${plans.map(formatNativeScheduler).join("\n\n")}\n\nDry run only. Re-run with --yes to ${options.remove ? "remove" : "enable"} both read-only jobs.`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const snapshotId = await applyNativeSchedulerBundle(plans);
|
|
761
|
+
console.log(options.json
|
|
762
|
+
? JSON.stringify({ action, plans, snapshotId }, null, 2)
|
|
763
|
+
: `Loadout Autopilot ${options.remove ? "removed" : "enabled"}: daily update radar + multi-source candidate discovery.\nNo scheduled command can install, promote, or execute a candidate. Snapshot: ${snapshotId}`);
|
|
764
|
+
});
|
|
765
|
+
program
|
|
766
|
+
.command("badge")
|
|
767
|
+
.description("Generate telemetry-free Shields endpoint JSON from the local privacy-safe card")
|
|
768
|
+
.option("--metric <metric>", "evidence, active-skills, managed-packages, or mcp", "evidence")
|
|
769
|
+
.option("--output <path>", "write endpoint JSON to this path")
|
|
770
|
+
.action(async (options) => {
|
|
771
|
+
const badge = buildLoadoutBadge(await buildLoadoutCard(), parseLoadoutBadgeMetric(options.metric));
|
|
772
|
+
if (!options.output)
|
|
773
|
+
return console.log(JSON.stringify(badge, null, 2));
|
|
774
|
+
const output = resolve(options.output);
|
|
775
|
+
await writeFileAtomically(output, `${JSON.stringify(badge, null, 2)}\n`);
|
|
776
|
+
console.log(formatLoadoutBadgeUsage(output));
|
|
777
|
+
});
|
|
778
|
+
program
|
|
779
|
+
.command("claims")
|
|
780
|
+
.description("Audit public product claims against current repository evidence and boundaries")
|
|
781
|
+
.option("--json", "emit the machine-readable release evidence index")
|
|
782
|
+
.action(async (options) => {
|
|
783
|
+
const root = releaseEvidenceRoot();
|
|
784
|
+
const catalog = await loadEffectiveCatalog();
|
|
785
|
+
const readme = await readFile(resolve(root, "README.md"), "utf8");
|
|
786
|
+
const index = await auditReleaseClaims({
|
|
787
|
+
root,
|
|
788
|
+
readme,
|
|
789
|
+
catalogCount: catalog.length,
|
|
790
|
+
verifyEvidenceFiles: existsSync(join(root, "tests")),
|
|
791
|
+
});
|
|
792
|
+
console.log(options.json
|
|
793
|
+
? JSON.stringify(index, null, 2)
|
|
794
|
+
: formatReleaseEvidenceIndex(index));
|
|
795
|
+
if (index.releaseBlocked)
|
|
796
|
+
process.exitCode = 1;
|
|
797
|
+
});
|
|
798
|
+
program
|
|
799
|
+
.command("tool")
|
|
800
|
+
.description("Preview, install, or remove an explicitly reviewed runtime-tool recipe")
|
|
801
|
+
.argument("[id]", "runtime tool id; omit to list reviewed recipes")
|
|
802
|
+
.option("--agents <ids>", "comma-separated target agent ids")
|
|
803
|
+
.option("--remove", "restore pre-install agent state and remove the runtime")
|
|
804
|
+
.option("--yes", "apply the exact displayed plan")
|
|
805
|
+
.option("--approve-risk", "approve installing and running the exact reviewed external artifact")
|
|
806
|
+
.option("--json", "emit machine-readable JSON")
|
|
807
|
+
.action(async (id, options) => {
|
|
808
|
+
if (!id) {
|
|
809
|
+
if (options.remove || options.yes || options.approveRisk)
|
|
810
|
+
throw new Error("Select a runtime tool id for this operation");
|
|
811
|
+
const listed = REVIEWED_RUNTIME_TOOLS.map((recipe) => ({
|
|
812
|
+
id: recipe.id,
|
|
813
|
+
displayName: recipe.displayName,
|
|
814
|
+
version: recipe.version,
|
|
815
|
+
source: recipe.source,
|
|
816
|
+
artifactSha256: recipe.artifactSha256,
|
|
817
|
+
}));
|
|
818
|
+
console.log(options.json
|
|
819
|
+
? JSON.stringify(listed, null, 2)
|
|
820
|
+
: listed
|
|
821
|
+
.map((recipe) => `${recipe.id} — ${recipe.displayName} ${recipe.version} — ${recipe.source}`)
|
|
822
|
+
.join("\n"));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const plan = await planRuntimeTool(id, {
|
|
826
|
+
action: options.remove ? "remove" : "install",
|
|
827
|
+
requestedAgents: parseAgentSelection(options.agents),
|
|
828
|
+
});
|
|
829
|
+
if (!options.yes) {
|
|
830
|
+
console.log(options.json
|
|
831
|
+
? JSON.stringify(plan, null, 2)
|
|
832
|
+
: `${formatRuntimeToolPlan(plan)}\n\nDry run only. Re-run with --yes --approve-risk to apply this exact runtime recipe.`);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (!options.approveRisk)
|
|
836
|
+
throw new Error("Runtime tool changes require --approve-risk after reviewing the preview");
|
|
837
|
+
const result = await applyRuntimeToolPlan(plan, { approveRisk: true });
|
|
838
|
+
console.log(options.json
|
|
839
|
+
? JSON.stringify({ plan, result }, null, 2)
|
|
840
|
+
: `${plan.recipe.displayName} ${result.action === "install" ? "installed and registered" : "removed and prior agent state restored"}. Snapshot: ${result.snapshotId}`);
|
|
841
|
+
});
|
|
842
|
+
program
|
|
843
|
+
.command("health")
|
|
844
|
+
.description("Quickly check agents, installed packages, and local file drift")
|
|
845
|
+
.option("--json", "emit machine-readable JSON")
|
|
846
|
+
.option("--updates", "also perform live network update checks")
|
|
847
|
+
.option("--explain", "add deterministic score dimensions, evidence, uncertainty, and remediation")
|
|
848
|
+
.option("--agents <ids>", "limit explained scores to selected agent ids")
|
|
849
|
+
.action(async (options) => {
|
|
850
|
+
const report = await buildHealthReport({ checkUpdates: options.updates });
|
|
851
|
+
if (options.agents && !options.explain)
|
|
852
|
+
throw new Error("--agents requires --explain");
|
|
853
|
+
const selectedAgents = parseAgentSelection(options.agents);
|
|
854
|
+
const scores = options.explain
|
|
855
|
+
? (await buildLocalAgentHealthScores()).filter((score) => !selectedAgents || selectedAgents.includes(score.agent))
|
|
856
|
+
: [];
|
|
857
|
+
console.log(options.json
|
|
858
|
+
? JSON.stringify(options.explain ? { report, scores } : report, null, 2)
|
|
859
|
+
: [
|
|
860
|
+
formatHealthReport(report),
|
|
861
|
+
...scores.map((score) => `\n${formatAgentHealthScore(score)}`),
|
|
862
|
+
].join("\n"));
|
|
863
|
+
});
|
|
864
|
+
program
|
|
865
|
+
.command("alerts")
|
|
866
|
+
.description("Explain evidence-backed archive, staleness, reviewed-commit, and permission alerts")
|
|
867
|
+
.option("--updates", "perform live update safety checks")
|
|
868
|
+
.option("--all", "include ignored alerts")
|
|
869
|
+
.option("--evidence <path>", "verified signed head-to-head evidence path")
|
|
870
|
+
.option("--public-key <path>", "trusted public key for --evidence")
|
|
871
|
+
.option("--json", "emit machine-readable JSON")
|
|
872
|
+
.action(async (options) => {
|
|
873
|
+
if (Boolean(options.evidence) !== Boolean(options.publicKey))
|
|
874
|
+
throw new Error("--evidence and --public-key must be provided together");
|
|
875
|
+
const replacementEvidence = options.evidence
|
|
876
|
+
? replacementEvidenceFromSignedSnapshot(JSON.parse(await readFile(resolve(options.evidence), "utf8")), await readFile(resolve(options.publicKey), "utf8"))
|
|
877
|
+
: undefined;
|
|
878
|
+
const alerts = await buildFreshnessAlerts({
|
|
879
|
+
checkUpdates: options.updates,
|
|
880
|
+
replacementEvidence,
|
|
881
|
+
});
|
|
882
|
+
const selected = options.all
|
|
883
|
+
? alerts
|
|
884
|
+
: alerts.filter((alert) => !alert.ignored);
|
|
885
|
+
console.log(options.json
|
|
886
|
+
? JSON.stringify(selected, null, 2)
|
|
887
|
+
: formatFreshnessAlerts(selected));
|
|
888
|
+
});
|
|
889
|
+
program
|
|
890
|
+
.command("alert-ignore")
|
|
891
|
+
.description("Ignore one exact freshness alert id on this machine")
|
|
892
|
+
.argument("<id>", "alert id shown by loadout alerts")
|
|
893
|
+
.action(async (id) => {
|
|
894
|
+
await ignoreFreshnessAlert(id);
|
|
895
|
+
console.log(`Ignored ${id} locally. Re-run loadout alerts --all to inspect it.`);
|
|
896
|
+
});
|
|
897
|
+
program
|
|
898
|
+
.command("alert-pin")
|
|
899
|
+
.description("Pin a reviewed replacement preference after comparing evidence; does not change active skills")
|
|
900
|
+
.argument("<package>", "currently installed package id")
|
|
901
|
+
.argument("<replacement>", "reviewed replacement package id")
|
|
902
|
+
.action(async (packageId, replacementId) => {
|
|
903
|
+
await pinReplacement(packageId, replacementId);
|
|
904
|
+
console.log(`Pinned ${replacementId} as a local replacement preference for ${packageId}. Review and activate it explicitly with loadout compare/enable.`);
|
|
905
|
+
});
|
|
906
|
+
program
|
|
907
|
+
.command("alert-unpin")
|
|
908
|
+
.description("Remove a local replacement preference")
|
|
909
|
+
.argument("<package>", "currently installed package id")
|
|
910
|
+
.action(async (packageId) => {
|
|
911
|
+
const removed = await unpinReplacement(packageId);
|
|
912
|
+
console.log(removed
|
|
913
|
+
? `Removed the replacement preference for ${packageId}.`
|
|
914
|
+
: `No replacement preference exists for ${packageId}.`);
|
|
915
|
+
});
|
|
916
|
+
program
|
|
917
|
+
.command("alert-pins")
|
|
918
|
+
.description("Show local replacement preferences")
|
|
919
|
+
.option("--json", "emit machine-readable JSON")
|
|
920
|
+
.action(async (options) => {
|
|
921
|
+
const pins = await readReplacementPins();
|
|
922
|
+
console.log(options.json
|
|
923
|
+
? JSON.stringify(pins, null, 2)
|
|
924
|
+
: pins.length
|
|
925
|
+
? pins
|
|
926
|
+
.map((pin) => `${pin.packageId} -> ${pin.replacementPackageId}`)
|
|
927
|
+
.join("\n")
|
|
928
|
+
: "No local replacement preferences.");
|
|
929
|
+
});
|
|
930
|
+
program
|
|
931
|
+
.command("remove")
|
|
932
|
+
.description("Safely remove only files managed for one package")
|
|
933
|
+
.argument("<package>", "managed package id")
|
|
934
|
+
.option("--yes", "apply removal; otherwise show a plan")
|
|
935
|
+
.option("--force", "also remove managed files changed outside Loadout")
|
|
936
|
+
.action(async (packageId, options) => {
|
|
937
|
+
const plan = await planRemove(packageId);
|
|
938
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
939
|
+
if (!options.yes)
|
|
940
|
+
return console.log("Dry run only. Re-run with --yes to remove these files.");
|
|
941
|
+
const snapshot = await applyRemove(plan, { force: options.force });
|
|
942
|
+
console.log(`Removed ${packageId}. Snapshot: ${snapshot}`);
|
|
943
|
+
});
|
|
944
|
+
program
|
|
945
|
+
.command("recommend")
|
|
946
|
+
.description("Recommend catalog packages from local project signals")
|
|
947
|
+
.option("--project <path>", "project directory", process.cwd())
|
|
948
|
+
.option("--agent <id>", "personalize with local-only outcomes recorded for one supported agent")
|
|
949
|
+
.option("--json", "emit machine-readable JSON")
|
|
950
|
+
.action(async (options) => {
|
|
951
|
+
const signals = await scanProject(options.project);
|
|
952
|
+
let recommendations = recommendPackages(signals, await loadEffectiveCatalog());
|
|
953
|
+
if (options.agent) {
|
|
954
|
+
const agents = parseAgentSelection(options.agent);
|
|
955
|
+
if (agents.length !== 1)
|
|
956
|
+
throw new Error("--agent accepts exactly one id");
|
|
957
|
+
recommendations = personalizeRecommendations(recommendations, signals, await readLocalOutcomes(), agents[0]);
|
|
958
|
+
}
|
|
959
|
+
console.log(options.json
|
|
960
|
+
? JSON.stringify({
|
|
961
|
+
signals,
|
|
962
|
+
recommendations,
|
|
963
|
+
personalization: options.agent
|
|
964
|
+
? {
|
|
965
|
+
agent: options.agent,
|
|
966
|
+
privacy: "local-only-no-project-or-content",
|
|
967
|
+
}
|
|
968
|
+
: undefined,
|
|
969
|
+
}, null, 2)
|
|
970
|
+
: formatRecommendations(signals, recommendations));
|
|
971
|
+
});
|
|
972
|
+
program
|
|
973
|
+
.command("profiles")
|
|
974
|
+
.description("List tested Loadout profiles or inspect one profile")
|
|
975
|
+
.argument("[name]", "profile name")
|
|
976
|
+
.option("--json", "emit machine-readable JSON")
|
|
977
|
+
.option("--apply-to <path>", "add the selected profile packages to a manifest")
|
|
978
|
+
.action(async (name, options) => {
|
|
979
|
+
if (!name)
|
|
980
|
+
return console.log(options.json
|
|
981
|
+
? JSON.stringify(TESTED_PROFILES, null, 2)
|
|
982
|
+
: Object.entries(TESTED_PROFILES)
|
|
983
|
+
.map(([id, profile]) => `${id} — ${profile.description}`)
|
|
984
|
+
.join("\n"));
|
|
985
|
+
const packages = profileManifestPackages(name, await loadEffectiveCatalog());
|
|
986
|
+
if (options.applyTo) {
|
|
987
|
+
const manifest = await applyProfileToManifest(options.applyTo, name, packages);
|
|
988
|
+
console.log(`Applied profile ${name} to ${options.applyTo}. ${manifest.packages.length} package(s) configured.`);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
console.log(options.json
|
|
992
|
+
? JSON.stringify({ name, ...TESTED_PROFILES[name], packages }, null, 2)
|
|
993
|
+
: `${name}: ${TESTED_PROFILES[name].description}\n${packages.map((pkg) => ` ${pkg.id} — ${pkg.repository}`).join("\n")}`);
|
|
994
|
+
});
|
|
995
|
+
program
|
|
996
|
+
.command("improve")
|
|
997
|
+
.description("Propose the next evidence-backed improvement without changing anything")
|
|
998
|
+
.option("--json", "emit machine-readable JSON")
|
|
999
|
+
.option("--write", "persist the cycle record and reusable prompt locally")
|
|
1000
|
+
.option("--output <directory>", "output directory; defaults to private Loadout state")
|
|
1001
|
+
.action(async (options) => {
|
|
1002
|
+
const cycle = await buildImprovementCycle();
|
|
1003
|
+
console.log(options.json
|
|
1004
|
+
? JSON.stringify(cycle, null, 2)
|
|
1005
|
+
: formatImprovementCycle(cycle));
|
|
1006
|
+
if (options.write) {
|
|
1007
|
+
const paths = await writeImprovementCycle(cycle, options.output);
|
|
1008
|
+
console.log(`Cycle record: ${paths.json}\nLoop prompt: ${paths.prompt}`);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
program
|
|
1012
|
+
.command("improve-feedback")
|
|
1013
|
+
.description("Record a human-reviewed outcome for a persisted improvement cycle")
|
|
1014
|
+
.requiredOption("--id <id>", "cycle id")
|
|
1015
|
+
.requiredOption("--outcome <outcome>", "success, failure, or partial")
|
|
1016
|
+
.option("--note <text>", "short non-secret lesson")
|
|
1017
|
+
.option("--directory <path>", "improvement history directory")
|
|
1018
|
+
.action(async (options) => {
|
|
1019
|
+
if (!["success", "failure", "partial"].includes(options.outcome))
|
|
1020
|
+
throw new Error("--outcome must be success, failure, or partial");
|
|
1021
|
+
await recordImprovementOutcome(options.id, options.outcome, options.note, options.directory);
|
|
1022
|
+
console.log(`Recorded ${options.outcome} outcome for ${options.id}.`);
|
|
1023
|
+
});
|
|
1024
|
+
program
|
|
1025
|
+
.command("sync")
|
|
1026
|
+
.description("Reproduce a loadout.json manifest as one safe transaction")
|
|
1027
|
+
.option("--manifest <path>", "manifest path", "loadout.json")
|
|
1028
|
+
.option("--lock <path>", "lockfile output", "loadout.lock")
|
|
1029
|
+
.option("--yes", "apply the plan; otherwise remain read-only")
|
|
1030
|
+
.option("--approve-risk", "explicitly approve plans containing scripts, hooks, or binaries")
|
|
1031
|
+
.action(async (options) => {
|
|
1032
|
+
const plan = await buildSyncPlan(options.manifest);
|
|
1033
|
+
console.log(JSON.stringify({
|
|
1034
|
+
manifest: plan.manifest,
|
|
1035
|
+
packages: plan.packages.map((entry) => ({
|
|
1036
|
+
...entry.plan,
|
|
1037
|
+
safety: entry.safety,
|
|
1038
|
+
})),
|
|
1039
|
+
mcpChanges: plan.mcpPlans.map((entry) => ({
|
|
1040
|
+
packageId: entry.packageId,
|
|
1041
|
+
path: entry.plan.path,
|
|
1042
|
+
changes: entry.plan.changes,
|
|
1043
|
+
warnings: entry.plan.warnings,
|
|
1044
|
+
})),
|
|
1045
|
+
skipped: plan.skipped,
|
|
1046
|
+
policyViolations: plan.policyViolations,
|
|
1047
|
+
}, null, 2));
|
|
1048
|
+
if (!options.yes)
|
|
1049
|
+
return console.log("Dry run only. Re-run with --yes to synchronize this Loadout.");
|
|
1050
|
+
const result = await applySyncPlan(plan, options.lock, {
|
|
1051
|
+
approveRisk: options.approveRisk,
|
|
1052
|
+
});
|
|
1053
|
+
console.log(`Synchronized successfully.${result.snapshotId ? ` Snapshot: ${result.snapshotId}.` : ""} Lockfile: ${result.lockfile}`);
|
|
1054
|
+
});
|
|
1055
|
+
program
|
|
1056
|
+
.command("scan")
|
|
1057
|
+
.description("Inventory existing agent skills, ownership, fingerprints, and duplicates without changing anything")
|
|
1058
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to detected agents")
|
|
1059
|
+
.option("--refresh-provenance", "fetch exact reviewed commits and rebuild the local catalog skill index")
|
|
1060
|
+
.option("--json", "emit the complete machine-readable inventory")
|
|
1061
|
+
.action(async (options) => {
|
|
1062
|
+
const detected = await detectAgents();
|
|
1063
|
+
const requested = parseAgentSelection(options.agents);
|
|
1064
|
+
const selected = requested?.length
|
|
1065
|
+
? detected.filter((agent) => requested.includes(agent.id))
|
|
1066
|
+
: detected.filter((agent) => agent.installed);
|
|
1067
|
+
if (!selected.length)
|
|
1068
|
+
throw new Error("No detected agent profile is available to scan");
|
|
1069
|
+
const report = await scanInstalledSkills(selected);
|
|
1070
|
+
const catalog = await loadEffectiveCatalog();
|
|
1071
|
+
const resolved = await resolveCatalogSkillIndex({
|
|
1072
|
+
refresh: options.refreshProvenance,
|
|
1073
|
+
offline: !options.refreshProvenance,
|
|
1074
|
+
build: {
|
|
1075
|
+
catalog,
|
|
1076
|
+
onProgress: options.refreshProvenance
|
|
1077
|
+
? printProvenanceProgress
|
|
1078
|
+
: undefined,
|
|
1079
|
+
},
|
|
1080
|
+
});
|
|
1081
|
+
const enriched = enrichInventoryWithProvenance(report, resolved.index, resolved.source);
|
|
1082
|
+
console.log(options.json
|
|
1083
|
+
? JSON.stringify(enriched, null, 2)
|
|
1084
|
+
: `${formatInstalledSkillInventory(enriched)}\n${formatProvenanceSummary(enriched)}`);
|
|
1085
|
+
});
|
|
1086
|
+
program
|
|
1087
|
+
.command("adopt")
|
|
1088
|
+
.description("Take ownership of one explicitly selected installed skill without changing its bytes")
|
|
1089
|
+
.argument("<skill>", "installed skill name, directory name, or exact path")
|
|
1090
|
+
.requiredOption("--agent <id>", "agent that owns the installed skill")
|
|
1091
|
+
.option("--refresh-provenance", "rebuild the reviewed catalog skill index")
|
|
1092
|
+
.option("--yes", "record ownership; otherwise show a dry-run plan")
|
|
1093
|
+
.option("--json", "emit machine-readable JSON")
|
|
1094
|
+
.action(async (skill, options) => {
|
|
1095
|
+
const detected = await detectAgents();
|
|
1096
|
+
const agent = detected.find((item) => item.id === options.agent && item.installed);
|
|
1097
|
+
if (!agent)
|
|
1098
|
+
throw new Error(`Agent '${options.agent}' is unknown or is not installed`);
|
|
1099
|
+
const resolved = await resolveCatalogSkillIndex({
|
|
1100
|
+
refresh: options.refreshProvenance,
|
|
1101
|
+
offline: !options.refreshProvenance,
|
|
1102
|
+
build: {
|
|
1103
|
+
catalog: await loadEffectiveCatalog(),
|
|
1104
|
+
onProgress: options.refreshProvenance
|
|
1105
|
+
? printProvenanceProgress
|
|
1106
|
+
: undefined,
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
const plan = await planSkillAdoption(skill, agent, resolved.index);
|
|
1110
|
+
if (!options.yes) {
|
|
1111
|
+
console.log(options.json
|
|
1112
|
+
? JSON.stringify(plan, null, 2)
|
|
1113
|
+
: `${formatAdoptionPlan(plan)}\nDry run only. Re-run with --yes to adopt this one skill.`);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const snapshotId = await applySkillAdoption(plan);
|
|
1117
|
+
console.log(options.json
|
|
1118
|
+
? JSON.stringify({ plan, snapshotId }, null, 2)
|
|
1119
|
+
: `${formatAdoptionPlan(plan)}\nAdopted without changing skill bytes. Snapshot: ${snapshotId}`);
|
|
1120
|
+
});
|
|
1121
|
+
program
|
|
1122
|
+
.command("compare")
|
|
1123
|
+
.description("Compare an installed or reviewed skill with evidence-related catalog alternatives without changing anything")
|
|
1124
|
+
.argument("<skill>", "installed skill name, directory name, or catalog package id")
|
|
1125
|
+
.option("--agent <id>", "select one installed agent when names are ambiguous")
|
|
1126
|
+
.option("--refresh", "rebuild the reviewed catalog skill index")
|
|
1127
|
+
.option("--offline", "never fetch; use the existing local index only")
|
|
1128
|
+
.option("--limit <count>", "maximum alternatives to show", "10")
|
|
1129
|
+
.option("--json", "emit the complete machine-readable comparison")
|
|
1130
|
+
.action(async (skill, options) => {
|
|
1131
|
+
if (options.refresh && options.offline)
|
|
1132
|
+
throw new Error("--refresh and --offline cannot be used together");
|
|
1133
|
+
const limit = Number(options.limit);
|
|
1134
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50)
|
|
1135
|
+
throw new Error("--limit must be an integer from 1 to 50");
|
|
1136
|
+
const detected = await detectAgents();
|
|
1137
|
+
const agent = options.agent;
|
|
1138
|
+
if (agent && !detected.some((item) => item.id === agent))
|
|
1139
|
+
throw new Error(`Unknown agent id: ${options.agent}`);
|
|
1140
|
+
const selected = detected.filter((item) => item.installed);
|
|
1141
|
+
if (!selected.length)
|
|
1142
|
+
throw new Error("No detected agent profile is available to compare");
|
|
1143
|
+
const catalog = await loadEffectiveCatalog();
|
|
1144
|
+
const resolved = await resolveCatalogSkillIndex({
|
|
1145
|
+
refresh: options.refresh,
|
|
1146
|
+
offline: options.offline,
|
|
1147
|
+
build: {
|
|
1148
|
+
catalog,
|
|
1149
|
+
onProgress: options.offline ? undefined : printProvenanceProgress,
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
if (!resolved.index)
|
|
1153
|
+
throw new Error("No local catalog skill index exists. Re-run without --offline or use --refresh.");
|
|
1154
|
+
const inventory = enrichInventoryWithProvenance(await scanInstalledSkills(selected), resolved.index, resolved.source);
|
|
1155
|
+
const comparison = compareSkill(skill, inventory, resolved.index.records, catalog, {
|
|
1156
|
+
...(agent ? { agent } : {}),
|
|
1157
|
+
limit,
|
|
1158
|
+
indexGeneratedAt: resolved.index.generatedAt,
|
|
1159
|
+
failures: resolved.index.failures,
|
|
1160
|
+
});
|
|
1161
|
+
console.log(options.json
|
|
1162
|
+
? JSON.stringify(comparison, null, 2)
|
|
1163
|
+
: formatSkillComparison(comparison));
|
|
1164
|
+
});
|
|
1165
|
+
program
|
|
1166
|
+
.command("status")
|
|
1167
|
+
.description("Show detected coding agents and their managed component inventory")
|
|
1168
|
+
.option("--json", "emit machine-readable inventory")
|
|
1169
|
+
.action(async (options) => {
|
|
1170
|
+
const agents = await detectAgents();
|
|
1171
|
+
const inventory = await inspectAgents(agents);
|
|
1172
|
+
if (options.json)
|
|
1173
|
+
return console.log(JSON.stringify(inventory, null, 2));
|
|
1174
|
+
for (const item of inventory)
|
|
1175
|
+
console.log(formatAgentInventory(item));
|
|
1176
|
+
});
|
|
1177
|
+
program
|
|
1178
|
+
.command("versions")
|
|
1179
|
+
.description("Detect installed agent versions with bounded read-only commands")
|
|
1180
|
+
.option("--json", "emit machine-readable version evidence")
|
|
1181
|
+
.action(async (options) => {
|
|
1182
|
+
const evidence = await inspectAgentVersions();
|
|
1183
|
+
console.log(options.json
|
|
1184
|
+
? JSON.stringify(evidence, null, 2)
|
|
1185
|
+
: formatAgentVersions(evidence));
|
|
1186
|
+
});
|
|
1187
|
+
program
|
|
1188
|
+
.command("intelligence")
|
|
1189
|
+
.description("Verify and optionally cache a signed read-only discovery and compatibility feed")
|
|
1190
|
+
.requiredOption("--source <path-or-url>", "signed feed file or HTTPS URL")
|
|
1191
|
+
.requiredOption("--public-key <path>", "trusted Ed25519 public key")
|
|
1192
|
+
.option("--state <path>", "alternate replay-protection state path")
|
|
1193
|
+
.option("--cache <path>", "alternate verified feed cache path")
|
|
1194
|
+
.option("--yes", "cache this exact verified feed and advance replay protection")
|
|
1195
|
+
.option("--json", "emit the machine-readable preview or result")
|
|
1196
|
+
.action(async (options) => {
|
|
1197
|
+
const preview = await previewIntelligenceFeed({
|
|
1198
|
+
source: options.source,
|
|
1199
|
+
publicKeyPath: options.publicKey,
|
|
1200
|
+
...(options.state ? { statePath: options.state } : {}),
|
|
1201
|
+
...(options.cache ? { cachePath: options.cache } : {}),
|
|
1202
|
+
});
|
|
1203
|
+
const applied = options.yes
|
|
1204
|
+
? await applyIntelligenceFeed(preview)
|
|
1205
|
+
: undefined;
|
|
1206
|
+
if (options.json) {
|
|
1207
|
+
console.log(JSON.stringify({
|
|
1208
|
+
source: preview.source,
|
|
1209
|
+
fingerprint: preview.fingerprint,
|
|
1210
|
+
sequence: preview.payload.sequence,
|
|
1211
|
+
createdAt: preview.payload.createdAt,
|
|
1212
|
+
expiresAt: preview.payload.expiresAt,
|
|
1213
|
+
summary: preview.summary,
|
|
1214
|
+
firstPin: preview.firstPin,
|
|
1215
|
+
keyRotation: preview.keyRotation,
|
|
1216
|
+
boundary: preview.boundary,
|
|
1217
|
+
applied: Boolean(applied),
|
|
1218
|
+
...(applied ? { cachePath: applied.cachePath } : {}),
|
|
1219
|
+
}, null, 2));
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
console.log([
|
|
1223
|
+
`Signed intelligence sequence ${preview.payload.sequence} (${preview.payload.createdAt})`,
|
|
1224
|
+
`Signer: ${preview.fingerprint}${preview.firstPin ? " (first pin on apply)" : preview.keyRotation ? " (authorized rotation)" : " (pinned)"}`,
|
|
1225
|
+
`Public observations: ${preview.summary.discoveryObservations}; compatibility notices: ${preview.summary.compatibilityNotices}; candidate summaries: ${preview.summary.candidateSummaries}; benchmark changes: ${preview.summary.benchmarkChanges}`,
|
|
1226
|
+
"Boundary: read-only intelligence; this cannot install, promote, update, or execute a candidate.",
|
|
1227
|
+
applied
|
|
1228
|
+
? `Verified feed cached at ${applied.cachePath}.`
|
|
1229
|
+
: "Preview only; nothing changed. Re-run with --yes to cache this exact verified feed.",
|
|
1230
|
+
].join("\n"));
|
|
1231
|
+
});
|
|
1232
|
+
program
|
|
1233
|
+
.command("compatibility")
|
|
1234
|
+
.description("Match signed compatibility notices to local agent versions and managed content")
|
|
1235
|
+
.requiredOption("--public-key <path>", "trusted intelligence-feed public key")
|
|
1236
|
+
.option("--source <path-or-url>", "preview a fresh feed instead of the applied cache")
|
|
1237
|
+
.option("--state <path>", "alternate replay-protection state path")
|
|
1238
|
+
.option("--cache <path>", "alternate verified feed cache path")
|
|
1239
|
+
.option("--json", "emit the full evidence and migration preview")
|
|
1240
|
+
.action(async (options) => {
|
|
1241
|
+
const paths = {
|
|
1242
|
+
publicKeyPath: options.publicKey,
|
|
1243
|
+
...(options.state ? { statePath: options.state } : {}),
|
|
1244
|
+
...(options.cache ? { cachePath: options.cache } : {}),
|
|
1245
|
+
};
|
|
1246
|
+
const loaded = options.source
|
|
1247
|
+
? await previewIntelligenceFeed({ source: options.source, ...paths })
|
|
1248
|
+
: await readCachedIntelligenceFeed(paths);
|
|
1249
|
+
const payload = loaded.payload;
|
|
1250
|
+
const report = buildCompatibilityIntelligence({
|
|
1251
|
+
versions: await inspectAgentVersions(),
|
|
1252
|
+
state: await readInstallState(),
|
|
1253
|
+
feed: parseCompatibilityNoticeSet({
|
|
1254
|
+
schemaVersion: 1,
|
|
1255
|
+
generatedAt: payload.createdAt,
|
|
1256
|
+
expiresAt: payload.expiresAt,
|
|
1257
|
+
notices: payload.compatibilityNotices,
|
|
1258
|
+
}),
|
|
1259
|
+
sourceStatus: options.source ? "verified" : "offline-cache",
|
|
1260
|
+
});
|
|
1261
|
+
console.log(options.json
|
|
1262
|
+
? JSON.stringify(report, null, 2)
|
|
1263
|
+
: [
|
|
1264
|
+
`Compatibility intelligence: ${report.freshness.status} (${report.freshness.sourceStatus})`,
|
|
1265
|
+
`Notices: ${report.assessments.length}; applicable/potential: ${report.assessments.filter((item) => item.applicability !== "not-applicable").length}`,
|
|
1266
|
+
`Affected managed components: ${report.affectedManagedContent.length}; migration steps: ${report.migrationPreview.length}`,
|
|
1267
|
+
...report.assessments.map((item) => `${item.applicability === "applies" ? "!" : item.applicability === "potential" ? "?" : "○"} ${item.notice.id}: ${item.applicability} — ${item.reason}`),
|
|
1268
|
+
"Migration output is preview-only and always requires explicit approval; no files were changed.",
|
|
1269
|
+
].join("\n"));
|
|
1270
|
+
});
|
|
1271
|
+
program
|
|
1272
|
+
.command("skill-audit")
|
|
1273
|
+
.description("Statically inspect one Agent Skill and emit its security/capability inventory")
|
|
1274
|
+
.argument("<directory>", "skill directory containing SKILL.md")
|
|
1275
|
+
.option("--json", "emit the complete machine-readable report")
|
|
1276
|
+
.action(async (directory, options) => {
|
|
1277
|
+
const report = await scanSkillSecurity(resolve(directory));
|
|
1278
|
+
console.log(options.json
|
|
1279
|
+
? JSON.stringify(report, null, 2)
|
|
1280
|
+
: [
|
|
1281
|
+
`Skill audit: ${report.specification.name ?? report.rootName}`,
|
|
1282
|
+
`Verdict: ${report.verdict}`,
|
|
1283
|
+
`Inventory: ${report.inventory.totalFiles} files, ${report.inventory.totalBytes} bytes, sha256:${report.inventory.treeHash}`,
|
|
1284
|
+
`Capabilities: ${report.capabilities.executableFiles.length} executable/script files, ${report.capabilities.dependencyNames.length} named dependencies, ${report.capabilities.domains.length} domains, ${report.capabilities.environmentNames.length} environment names`,
|
|
1285
|
+
...report.deterministicFindings.map((item) => `${item.severity === "critical" ? "✗" : "!"} ${item.severity}/${item.category}: ${item.message} (${item.paths.join(", ")})`),
|
|
1286
|
+
...(!report.deterministicFindings.length
|
|
1287
|
+
? ["No deterministic findings within the bounded static policy."]
|
|
1288
|
+
: []),
|
|
1289
|
+
"Static inspection cannot prove runtime safety or task quality.",
|
|
1290
|
+
].join("\n"));
|
|
1291
|
+
if (report.verdict === "blocked")
|
|
1292
|
+
process.exitCode = 2;
|
|
1293
|
+
});
|
|
1294
|
+
function formatEcosystemImport(result) {
|
|
1295
|
+
const plans = [result.manifest, result.lockEvidence].filter((item) => Boolean(item));
|
|
1296
|
+
return [
|
|
1297
|
+
`Interoperability preview: ${result.manifest.format}`,
|
|
1298
|
+
...plans.flatMap((plan) => [
|
|
1299
|
+
`${plan.artifact}: ${plan.source.filename} (sha256:${plan.source.sha256})`,
|
|
1300
|
+
` ${plan.candidates.length} candidate declarations; ${plan.unsupported.length} loss-reported fields; trust ${plan.trust.level}`,
|
|
1301
|
+
...plan.candidates.map((candidate) => ` - ${candidate.id}: ${candidate.dependencyKind}, ${candidate.disposition}`),
|
|
1302
|
+
]),
|
|
1303
|
+
"Read-only boundary: no external CLI, network request, file write, install, or registry claim occurred.",
|
|
1304
|
+
].join("\n");
|
|
1305
|
+
}
|
|
1306
|
+
const interop = program
|
|
1307
|
+
.command("interop")
|
|
1308
|
+
.description("Loss-reportingly inspect Microsoft APM or OpenPackage manifests without invoking them");
|
|
1309
|
+
interop
|
|
1310
|
+
.command("apm")
|
|
1311
|
+
.description("Inspect an OpenAPM manifest and optional lock evidence")
|
|
1312
|
+
.argument("<manifest>", "apm.yml path")
|
|
1313
|
+
.option("--lock <path>", "optional apm.lock.yaml path")
|
|
1314
|
+
.option("--json", "emit the complete read-only plan")
|
|
1315
|
+
.action(async (manifest, options) => {
|
|
1316
|
+
const result = await planApmImportFiles(manifest, options.lock);
|
|
1317
|
+
console.log(options.json
|
|
1318
|
+
? JSON.stringify(result, null, 2)
|
|
1319
|
+
: formatEcosystemImport(result));
|
|
1320
|
+
});
|
|
1321
|
+
interop
|
|
1322
|
+
.command("openpackage")
|
|
1323
|
+
.description("Inspect an OpenPackage manifest and optional workspace index")
|
|
1324
|
+
.argument("<manifest>", "openpackage.yml path")
|
|
1325
|
+
.option("--index <path>", "optional workspace index path")
|
|
1326
|
+
.option("--json", "emit the complete read-only plan")
|
|
1327
|
+
.action(async (manifest, options) => {
|
|
1328
|
+
const result = await planOpenPackageImportFiles(manifest, options.index);
|
|
1329
|
+
console.log(options.json
|
|
1330
|
+
? JSON.stringify(result, null, 2)
|
|
1331
|
+
: formatEcosystemImport(result));
|
|
1332
|
+
});
|
|
1333
|
+
const benchmark = program
|
|
1334
|
+
.command("benchmark")
|
|
1335
|
+
.description("Validate deterministic evaluation campaigns without making model calls");
|
|
1336
|
+
benchmark
|
|
1337
|
+
.command("plan")
|
|
1338
|
+
.description("Validate a campaign, preview its worst-case budget, and optionally write resumable run metadata")
|
|
1339
|
+
.argument("<campaign>", "benchmark campaign JSON file")
|
|
1340
|
+
.option("--run-id <id>", "create resumable run metadata with this id")
|
|
1341
|
+
.option("--output <path>", "write run metadata to this path")
|
|
1342
|
+
.option("--json", "emit a machine-readable campaign summary")
|
|
1343
|
+
.action(async (campaignPath, options) => {
|
|
1344
|
+
if (Boolean(options.runId) !== Boolean(options.output))
|
|
1345
|
+
throw new Error("--run-id and --output must be provided together");
|
|
1346
|
+
const campaign = parseBenchmarkCampaign(JSON.parse(await readFile(resolve(campaignPath), "utf8")));
|
|
1347
|
+
const summary = summarizeBenchmarkCampaign(campaign);
|
|
1348
|
+
if (options.output) {
|
|
1349
|
+
if (!summary.withinBudget)
|
|
1350
|
+
throw new Error(`Refusing to write runnable benchmark metadata because declared ceilings are exceeded: ${summary.blockers.join("; ")}`);
|
|
1351
|
+
const run = createBenchmarkRun(campaign, options.runId);
|
|
1352
|
+
await writeFileAtomically(resolve(options.output), `${JSON.stringify(run, null, 2)}\n`, 0o600);
|
|
1353
|
+
}
|
|
1354
|
+
console.log(options.json
|
|
1355
|
+
? JSON.stringify({
|
|
1356
|
+
summary,
|
|
1357
|
+
executed: false,
|
|
1358
|
+
...(options.output
|
|
1359
|
+
? { runMetadataPath: resolve(options.output) }
|
|
1360
|
+
: {}),
|
|
1361
|
+
safetyBoundary: "Planning only: no model call, prompt, output, credential, or candidate execution occurred.",
|
|
1362
|
+
}, null, 2)
|
|
1363
|
+
: [
|
|
1364
|
+
formatBenchmarkCampaignSummary(campaign),
|
|
1365
|
+
options.output
|
|
1366
|
+
? `Run metadata: ${resolve(options.output)}`
|
|
1367
|
+
: "No run metadata written; add --run-id <id> --output <path> to create it.",
|
|
1368
|
+
"Planning only; no model call or candidate execution occurred.",
|
|
1369
|
+
].join("\n"));
|
|
1370
|
+
if (!summary.withinBudget)
|
|
1371
|
+
process.exitCode = 2;
|
|
1372
|
+
});
|
|
1373
|
+
program
|
|
1374
|
+
.command("demo")
|
|
1375
|
+
.alias("test-drive")
|
|
1376
|
+
.description("Test-drive a reviewed install + rollback in a disposable profile; never touches local agent config")
|
|
1377
|
+
.option("--repository <owner/repo>", "public GitHub skill repository", "obra/superpowers")
|
|
1378
|
+
.option("--package <id>", "package identifier", "obra-superpowers")
|
|
1379
|
+
.option("--agents <ids>", "comma-separated virtual demo targets", "codex")
|
|
1380
|
+
.option("--keep", "retain the isolated profile after install for inspection")
|
|
1381
|
+
.option("--json", "emit the demo result as JSON")
|
|
1382
|
+
.action(async (options) => {
|
|
1383
|
+
const result = await runIsolatedDemo({
|
|
1384
|
+
repository: options.repository,
|
|
1385
|
+
packageId: options.package,
|
|
1386
|
+
agents: parseAgentSelection(options.agents),
|
|
1387
|
+
keep: options.keep,
|
|
1388
|
+
});
|
|
1389
|
+
console.log(options.json
|
|
1390
|
+
? JSON.stringify(result, null, 2)
|
|
1391
|
+
: formatDemoResult(result));
|
|
1392
|
+
});
|
|
1393
|
+
program
|
|
1394
|
+
.command("capabilities")
|
|
1395
|
+
.description("Show honest native, adapted, and unsupported adapter capabilities")
|
|
1396
|
+
.option("--json", "emit machine-readable JSON")
|
|
1397
|
+
.option("--inspect", "also inspect managed component directories on this machine")
|
|
1398
|
+
.option("--gaps", "show the evidence-gated backlog for unsupported adapter combinations")
|
|
1399
|
+
.action(async (options) => {
|
|
1400
|
+
if (options.gaps) {
|
|
1401
|
+
if (options.inspect)
|
|
1402
|
+
throw new Error("Choose either --inspect or --gaps");
|
|
1403
|
+
const gaps = buildAdapterCapabilityGaps();
|
|
1404
|
+
return console.log(options.json
|
|
1405
|
+
? JSON.stringify(gaps, null, 2)
|
|
1406
|
+
: formatAdapterCapabilityGaps());
|
|
1407
|
+
}
|
|
1408
|
+
if (!options.inspect)
|
|
1409
|
+
return console.log(options.json
|
|
1410
|
+
? JSON.stringify(ADAPTER_CAPABILITIES, null, 2)
|
|
1411
|
+
: formatCapabilityMatrix());
|
|
1412
|
+
const inventory = await inspectAgents(await detectAgents());
|
|
1413
|
+
if (options.json)
|
|
1414
|
+
return console.log(JSON.stringify({ capabilities: ADAPTER_CAPABILITIES, inventory }, null, 2));
|
|
1415
|
+
console.log(`${formatCapabilityMatrix()}\n\nLocal managed-component inventory:`);
|
|
1416
|
+
for (const item of inventory)
|
|
1417
|
+
console.log(formatAgentInventory(item));
|
|
1418
|
+
});
|
|
1419
|
+
program
|
|
1420
|
+
.command("doctor")
|
|
1421
|
+
.description("Check agents, skill directories, permissions, and Loadout setup")
|
|
1422
|
+
.option("--json", "print a machine-readable report")
|
|
1423
|
+
.action(async (options) => {
|
|
1424
|
+
const report = await runDoctor();
|
|
1425
|
+
console.log(options.json
|
|
1426
|
+
? JSON.stringify(report, null, 2)
|
|
1427
|
+
: formatDoctorReport(report));
|
|
1428
|
+
});
|
|
1429
|
+
program
|
|
1430
|
+
.command("catalog")
|
|
1431
|
+
.description("List the real package catalog")
|
|
1432
|
+
.option("--refresh", "fetch current GitHub stars and repository metadata")
|
|
1433
|
+
.option("--explain <id>", "print the evidence and guardrails behind one package's ranking")
|
|
1434
|
+
.option("--history <id>", "show locally recorded stars and release history")
|
|
1435
|
+
.option("--coverage", "show capability, evidence, license, and overlap metrics")
|
|
1436
|
+
.option("--json", "emit machine-readable output")
|
|
1437
|
+
.action(async (options) => {
|
|
1438
|
+
if ([options.explain, options.history, options.coverage].filter(Boolean)
|
|
1439
|
+
.length > 1)
|
|
1440
|
+
throw new Error("Choose one of --explain, --history, or --coverage");
|
|
1441
|
+
const base = await loadCatalog();
|
|
1442
|
+
const result = options.refresh
|
|
1443
|
+
? await refreshCatalog(base, { forceRefresh: true })
|
|
1444
|
+
: {
|
|
1445
|
+
catalog: await loadEffectiveCatalog(),
|
|
1446
|
+
failures: [],
|
|
1447
|
+
observationFailures: [],
|
|
1448
|
+
};
|
|
1449
|
+
if (options.history) {
|
|
1450
|
+
const pkg = result.catalog.find((item) => item.id === options.history);
|
|
1451
|
+
if (!pkg)
|
|
1452
|
+
throw new Error(`Unknown catalog package '${options.history}'`);
|
|
1453
|
+
console.log(formatStarHistory(await readCatalogObservations(pkg.repository)));
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (options.explain) {
|
|
1457
|
+
const pkg = result.catalog.find((item) => item.id === options.explain);
|
|
1458
|
+
if (!pkg)
|
|
1459
|
+
throw new Error(`Unknown catalog package '${options.explain}'`);
|
|
1460
|
+
console.log(JSON.stringify({
|
|
1461
|
+
package: {
|
|
1462
|
+
id: pkg.id,
|
|
1463
|
+
displayName: pkg.displayName,
|
|
1464
|
+
category: pkg.category,
|
|
1465
|
+
tier: pkg.tier,
|
|
1466
|
+
trustStage: catalogTrustStage(pkg),
|
|
1467
|
+
},
|
|
1468
|
+
ranking: explainCatalogScore(pkg),
|
|
1469
|
+
}, null, 2));
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (options.coverage) {
|
|
1473
|
+
const coverage = buildCatalogCoverage(result.catalog);
|
|
1474
|
+
console.log(options.json
|
|
1475
|
+
? JSON.stringify(coverage, null, 2)
|
|
1476
|
+
: formatCatalogCoverage(coverage));
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
for (const pkg of rankCatalog(result.catalog)) {
|
|
1480
|
+
const topics = pkg.topics?.length ? ` — ${pkg.topics.join(", ")}` : "";
|
|
1481
|
+
const updated = pkg.lastUpdatedAt
|
|
1482
|
+
? ` — updated ${pkg.lastUpdatedAt.slice(0, 10)}`
|
|
1483
|
+
: "";
|
|
1484
|
+
console.log(`${pkg.displayName} [${pkg.tier}; ${catalogTrustStage(pkg)}] ★${pkg.stars ?? "?"} — ${pkg.repository}${topics}${updated}`);
|
|
1485
|
+
}
|
|
1486
|
+
for (const failure of result.failures)
|
|
1487
|
+
console.error(`Warning: could not refresh ${failure.repository}: ${failure.error}`);
|
|
1488
|
+
for (const failure of result.observationFailures)
|
|
1489
|
+
console.error(`Warning: could not record release observation for ${failure.repository}: ${failure.error}`);
|
|
1490
|
+
});
|
|
1491
|
+
const candidate = program
|
|
1492
|
+
.command("candidate")
|
|
1493
|
+
.description("Triage and statically inspect daily discovery candidates; never auto-promotes");
|
|
1494
|
+
candidate
|
|
1495
|
+
.command("list")
|
|
1496
|
+
.allowExcessArguments(false)
|
|
1497
|
+
.description("Rank discovery leads for human triage, not as universal quality")
|
|
1498
|
+
.option("--limit <count>", "maximum candidates", "20")
|
|
1499
|
+
.option("--query <words>", "require all search words")
|
|
1500
|
+
.option("--include-reviewed", "include repositories already in the catalog")
|
|
1501
|
+
.option("--feed <path>", "alternate discovered.json evidence feed")
|
|
1502
|
+
.option("--json", "emit machine-readable JSON")
|
|
1503
|
+
.action(async (options) => {
|
|
1504
|
+
const limit = Number(options.limit);
|
|
1505
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500)
|
|
1506
|
+
throw new Error("--limit must be an integer from 1 to 500");
|
|
1507
|
+
const result = await listDiscoveryCandidates({
|
|
1508
|
+
limit,
|
|
1509
|
+
query: options.query,
|
|
1510
|
+
includeReviewed: options.includeReviewed,
|
|
1511
|
+
path: options.feed,
|
|
1512
|
+
});
|
|
1513
|
+
console.log(options.json
|
|
1514
|
+
? JSON.stringify(result, null, 2)
|
|
1515
|
+
: formatCandidateSummaries(result));
|
|
1516
|
+
});
|
|
1517
|
+
candidate
|
|
1518
|
+
.command("inspect")
|
|
1519
|
+
.allowExcessArguments(false)
|
|
1520
|
+
.description("Clone one lead at an immutable commit and build a static evidence dossier")
|
|
1521
|
+
.argument("<repository>", "owner/repository present in the discovery feed")
|
|
1522
|
+
.option("--feed <path>", "alternate discovered.json evidence feed")
|
|
1523
|
+
.option("--write", "persist the dossier in private Loadout state")
|
|
1524
|
+
.option("--output <path>", "persist at an explicit path (implies --write)")
|
|
1525
|
+
.option("--json", "emit the complete dossier as JSON")
|
|
1526
|
+
.action(async (repository, options) => {
|
|
1527
|
+
const dossier = await buildCandidateDossier(repository, {
|
|
1528
|
+
discoveryPath: options.feed,
|
|
1529
|
+
});
|
|
1530
|
+
const path = options.write || options.output
|
|
1531
|
+
? await writeCandidateDossier(dossier, options.output)
|
|
1532
|
+
: undefined;
|
|
1533
|
+
if (options.json)
|
|
1534
|
+
return console.log(JSON.stringify({
|
|
1535
|
+
dossier,
|
|
1536
|
+
persisted: Boolean(path),
|
|
1537
|
+
...(path ? { path } : {}),
|
|
1538
|
+
}, null, 2));
|
|
1539
|
+
console.log(formatCandidateDossier(dossier));
|
|
1540
|
+
if (path)
|
|
1541
|
+
console.log(`Dossier: ${path}`);
|
|
1542
|
+
else {
|
|
1543
|
+
console.log("Preview only. Re-run with --write to persist this dossier.");
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
candidate
|
|
1547
|
+
.command("propose")
|
|
1548
|
+
.allowExcessArguments(false)
|
|
1549
|
+
.description("Convert a reviewed dossier into a catalog-record proposal; never edits the catalog")
|
|
1550
|
+
.argument("<dossier>", "persisted candidate dossier JSON")
|
|
1551
|
+
.requiredOption("--id <id>", "lowercase kebab-case catalog id")
|
|
1552
|
+
.requiredOption("--category <category>", "reviewed catalog category")
|
|
1553
|
+
.requiredOption("--platforms <ids>", "explicitly reviewed comma-separated platforms: windows,macos,linux")
|
|
1554
|
+
.option("--display-name <name>", "reviewed display name")
|
|
1555
|
+
.option("--description <text>", "reviewed description")
|
|
1556
|
+
.option("--license <spdx>", "human-reviewed license override")
|
|
1557
|
+
.option("--tier <tier>", "official, stable, trending, or community", "community")
|
|
1558
|
+
.option("--approve", "confirm human review and write the proposal")
|
|
1559
|
+
.option("--output <path>", "proposal JSON output path; required with --approve")
|
|
1560
|
+
.option("--json", "emit machine-readable JSON")
|
|
1561
|
+
.action(async (dossierPath, options) => {
|
|
1562
|
+
const platforms = options.platforms
|
|
1563
|
+
.split(",")
|
|
1564
|
+
.map((item) => item.trim())
|
|
1565
|
+
.filter(Boolean);
|
|
1566
|
+
const knownPlatforms = new Set(["windows", "macos", "linux"]);
|
|
1567
|
+
if (platforms.some((item) => !knownPlatforms.has(item)))
|
|
1568
|
+
throw new Error("--platforms supports only windows, macos, and linux");
|
|
1569
|
+
const knownTiers = new Set([
|
|
1570
|
+
"official",
|
|
1571
|
+
"stable",
|
|
1572
|
+
"trending",
|
|
1573
|
+
"community",
|
|
1574
|
+
]);
|
|
1575
|
+
if (!knownTiers.has(options.tier))
|
|
1576
|
+
throw new Error("--tier is invalid");
|
|
1577
|
+
if (options.approve && !options.output)
|
|
1578
|
+
throw new Error("--approve requires --output so catalog mutation stays separate");
|
|
1579
|
+
const proposal = buildCatalogProposal(await verifyCandidateDossierSource(await readCandidateDossier(dossierPath)), {
|
|
1580
|
+
id: options.id,
|
|
1581
|
+
category: options.category,
|
|
1582
|
+
operatingSystems: platforms,
|
|
1583
|
+
tier: options.tier,
|
|
1584
|
+
displayName: options.displayName,
|
|
1585
|
+
description: options.description,
|
|
1586
|
+
license: options.license,
|
|
1587
|
+
}, await loadEffectiveCatalog());
|
|
1588
|
+
const output = options.approve ? resolve(options.output) : undefined;
|
|
1589
|
+
if (output)
|
|
1590
|
+
await writeFileAtomically(output, `${JSON.stringify(proposal, null, 2)}\n`);
|
|
1591
|
+
if (options.json)
|
|
1592
|
+
return console.log(JSON.stringify({
|
|
1593
|
+
proposal,
|
|
1594
|
+
approved: Boolean(output),
|
|
1595
|
+
catalogMutated: false,
|
|
1596
|
+
...(output ? { output } : {}),
|
|
1597
|
+
}, null, 2));
|
|
1598
|
+
console.log(JSON.stringify(proposal, null, 2));
|
|
1599
|
+
if (!output)
|
|
1600
|
+
console.log("Proposal preview only. Human review is still required; use --approve --output <path> to persist it.");
|
|
1601
|
+
else
|
|
1602
|
+
console.log(`Approved proposal written to ${output}.`);
|
|
1603
|
+
});
|
|
1604
|
+
program
|
|
1605
|
+
.command("catalog-update")
|
|
1606
|
+
.allowExcessArguments(false)
|
|
1607
|
+
.description("Verify, diff, and explicitly apply a signed catalog release from a file or HTTPS")
|
|
1608
|
+
.requiredOption("--source <path-or-url>", "signed catalog envelope")
|
|
1609
|
+
.requiredOption("--public-key <path>", "trusted Ed25519 public key")
|
|
1610
|
+
.option("--yes", "atomically apply after signature and evidence validation")
|
|
1611
|
+
.option("--allow-removals", "explicitly allow reviewed packages to be removed by this release")
|
|
1612
|
+
.option("--json", "emit machine-readable preview")
|
|
1613
|
+
.action(async (options) => {
|
|
1614
|
+
const preview = await previewCatalogRelease({
|
|
1615
|
+
source: options.source,
|
|
1616
|
+
publicKeyPath: options.publicKey,
|
|
1617
|
+
currentCatalog: await loadEffectiveCatalog(),
|
|
1618
|
+
});
|
|
1619
|
+
const result = options.yes
|
|
1620
|
+
? await applyCatalogRelease(preview, {
|
|
1621
|
+
allowRemovals: options.allowRemovals,
|
|
1622
|
+
})
|
|
1623
|
+
: undefined;
|
|
1624
|
+
if (options.json)
|
|
1625
|
+
return console.log(JSON.stringify({
|
|
1626
|
+
source: preview.source,
|
|
1627
|
+
createdAt: preview.createdAt,
|
|
1628
|
+
fingerprint: preview.fingerprint,
|
|
1629
|
+
packageCount: preview.packageCount,
|
|
1630
|
+
diff: result?.diff ?? preview.diff,
|
|
1631
|
+
replay: preview.replay,
|
|
1632
|
+
applied: Boolean(result),
|
|
1633
|
+
...(result
|
|
1634
|
+
? { path: result.path, snapshotId: result.snapshotId }
|
|
1635
|
+
: {}),
|
|
1636
|
+
}, null, 2));
|
|
1637
|
+
console.log(formatCatalogReleasePreview(preview));
|
|
1638
|
+
if (!result)
|
|
1639
|
+
console.log("Preview only. Re-run with --yes to trust this release.");
|
|
1640
|
+
else
|
|
1641
|
+
console.log(`Applied signed catalog atomically. Snapshot: ${result.snapshotId}\nState: ${result.path}`);
|
|
1642
|
+
});
|
|
1643
|
+
program
|
|
1644
|
+
.command("discover")
|
|
1645
|
+
.description("Find public community leads; discovery never installs anything")
|
|
1646
|
+
.option("--source <source>", "source: github, hacker-news, skills-sh, mcp-registry, or all", "hacker-news")
|
|
1647
|
+
.option("--limit <count>", "maximum source records or stories", "50")
|
|
1648
|
+
.option("--min-score <count>", "minimum Hacker News score", "20")
|
|
1649
|
+
.option("--query <words>", "comma-separated words that must appear in a story (for example: codex,mcp,agent)")
|
|
1650
|
+
.option("--private", "opt into private GitHub metadata discovery using GITHUB_TOKEN")
|
|
1651
|
+
.option("--credential-keychain <service>", "resolve the private GitHub token from the OS credential store")
|
|
1652
|
+
.option("--credential-account <account>", "OS credential account")
|
|
1653
|
+
.option("--queue", "persist deduplicated public leads for human review; never promotes them")
|
|
1654
|
+
.option("--json", "emit source evidence as JSON")
|
|
1655
|
+
.action(async (options) => {
|
|
1656
|
+
const limit = Number(options.limit);
|
|
1657
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
1658
|
+
throw new Error("--limit must be a positive integer");
|
|
1659
|
+
if (options.credentialKeychain && !options.private)
|
|
1660
|
+
throw new Error("--credential-keychain requires --private");
|
|
1661
|
+
if (options.private) {
|
|
1662
|
+
const token = options.credentialKeychain
|
|
1663
|
+
? await createCredentialResolver()({
|
|
1664
|
+
kind: "os-keychain",
|
|
1665
|
+
service: options.credentialKeychain,
|
|
1666
|
+
...(options.credentialAccount
|
|
1667
|
+
? { account: options.credentialAccount }
|
|
1668
|
+
: {}),
|
|
1669
|
+
})
|
|
1670
|
+
: undefined;
|
|
1671
|
+
if (options.credentialKeychain && !token)
|
|
1672
|
+
throw new Error("Private GitHub keychain credential did not resolve");
|
|
1673
|
+
const repositories = await discoverPrivateRepositories({ token });
|
|
1674
|
+
if (options.json)
|
|
1675
|
+
return console.log(JSON.stringify(repositories, null, 2));
|
|
1676
|
+
console.log(`Private GitHub repositories: ${repositories.length}`);
|
|
1677
|
+
for (const repository of repositories)
|
|
1678
|
+
console.log(`${repository.repository} — ${repository.description}`);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (options.source === "all") {
|
|
1682
|
+
const minScore = Number(options.minScore);
|
|
1683
|
+
if (!Number.isFinite(minScore) || minScore < 0)
|
|
1684
|
+
throw new Error("--min-score must be a non-negative number");
|
|
1685
|
+
const [github, hackerNews, skillsSh, mcpRegistry] = await Promise.allSettled([
|
|
1686
|
+
discoverGitHubRepositories({
|
|
1687
|
+
...(options.query
|
|
1688
|
+
? { query: options.query }
|
|
1689
|
+
: { queries: defaultGitHubDiscoveryQueries() }),
|
|
1690
|
+
limit,
|
|
1691
|
+
}),
|
|
1692
|
+
discoverHackerNewsRepositories({
|
|
1693
|
+
limit,
|
|
1694
|
+
minScore,
|
|
1695
|
+
keywords: options.query?.split(",") ?? [],
|
|
1696
|
+
}),
|
|
1697
|
+
discoverSkillsSh({ maxRecords: limit }),
|
|
1698
|
+
discoverOfficialMcpRegistry({
|
|
1699
|
+
maxRecords: limit,
|
|
1700
|
+
...(options.query ? { search: options.query } : {}),
|
|
1701
|
+
}),
|
|
1702
|
+
]);
|
|
1703
|
+
const leads = [
|
|
1704
|
+
...(github.status === "fulfilled" ? github.value : []),
|
|
1705
|
+
...(hackerNews.status === "fulfilled"
|
|
1706
|
+
? hackerNews.value.candidates
|
|
1707
|
+
: []),
|
|
1708
|
+
...(skillsSh.status === "fulfilled" ? skillsSh.value.records : []),
|
|
1709
|
+
...(mcpRegistry.status === "fulfilled"
|
|
1710
|
+
? mcpRegistry.value.records
|
|
1711
|
+
: []),
|
|
1712
|
+
];
|
|
1713
|
+
if (!leads.length) {
|
|
1714
|
+
const failures = [github, hackerNews, skillsSh, mcpRegistry]
|
|
1715
|
+
.filter((result) => result.status === "rejected")
|
|
1716
|
+
.map((result) => result.reason instanceof Error
|
|
1717
|
+
? result.reason.message
|
|
1718
|
+
: String(result.reason));
|
|
1719
|
+
const connectorIssues = [skillsSh, mcpRegistry].flatMap((result) => result.status === "fulfilled"
|
|
1720
|
+
? result.value.issues.map((issue) => issue.message)
|
|
1721
|
+
: []);
|
|
1722
|
+
throw new Error(`All discovery sources returned no usable leads: ${[...failures, ...connectorIssues].join("; ")}`);
|
|
1723
|
+
}
|
|
1724
|
+
const sourceWarnings = [github, hackerNews, skillsSh, mcpRegistry]
|
|
1725
|
+
.filter((result) => result.status === "rejected")
|
|
1726
|
+
.map((result) => result.reason instanceof Error
|
|
1727
|
+
? result.reason.message
|
|
1728
|
+
: String(result.reason))
|
|
1729
|
+
.concat([skillsSh, mcpRegistry].flatMap((result) => result.status === "fulfilled"
|
|
1730
|
+
? result.value.issues.map((issue) => `${result.value.source}: ${issue.message}`)
|
|
1731
|
+
: []));
|
|
1732
|
+
const queue = options.queue
|
|
1733
|
+
? await mergeReviewQueue(leads, await loadEffectiveCatalog())
|
|
1734
|
+
: undefined;
|
|
1735
|
+
const output = {
|
|
1736
|
+
leads,
|
|
1737
|
+
queue,
|
|
1738
|
+
sourceWarnings,
|
|
1739
|
+
connectorStatus: {
|
|
1740
|
+
skillsSh: skillsSh.status === "fulfilled"
|
|
1741
|
+
? skillsSh.value.status
|
|
1742
|
+
: "failed",
|
|
1743
|
+
mcpRegistry: mcpRegistry.status === "fulfilled"
|
|
1744
|
+
? mcpRegistry.value.status
|
|
1745
|
+
: "failed",
|
|
1746
|
+
},
|
|
1747
|
+
};
|
|
1748
|
+
if (options.json)
|
|
1749
|
+
return console.log(JSON.stringify(output, null, 2));
|
|
1750
|
+
if (queue)
|
|
1751
|
+
console.log(formatReviewQueue(queue));
|
|
1752
|
+
else
|
|
1753
|
+
console.log(`Multi-source discovery: ${leads.length} public lead(s).`);
|
|
1754
|
+
for (const warning of sourceWarnings)
|
|
1755
|
+
console.error(`Warning: ${warning}`);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (options.source === "github") {
|
|
1759
|
+
const repositories = await discoverGitHubRepositories({
|
|
1760
|
+
...(options.query
|
|
1761
|
+
? { query: options.query }
|
|
1762
|
+
: { queries: defaultGitHubDiscoveryQueries() }),
|
|
1763
|
+
limit,
|
|
1764
|
+
});
|
|
1765
|
+
if (options.queue) {
|
|
1766
|
+
const queue = await mergeReviewQueue(repositories, await loadEffectiveCatalog());
|
|
1767
|
+
if (options.json)
|
|
1768
|
+
return console.log(JSON.stringify(queue, null, 2));
|
|
1769
|
+
console.log(formatReviewQueue(queue));
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (options.json)
|
|
1773
|
+
return console.log(JSON.stringify(repositories, null, 2));
|
|
1774
|
+
console.log(`GitHub: ${repositories.length} repository lead(s)`);
|
|
1775
|
+
for (const repository of repositories)
|
|
1776
|
+
console.log(`★${repository.stars} · ${repository.repository} — ${repository.description}`);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (options.source === "skills-sh") {
|
|
1780
|
+
const result = await discoverSkillsSh({ maxRecords: limit });
|
|
1781
|
+
const queue = options.queue
|
|
1782
|
+
? await mergeReviewQueue(result.records, await loadEffectiveCatalog())
|
|
1783
|
+
: undefined;
|
|
1784
|
+
if (options.json)
|
|
1785
|
+
return console.log(JSON.stringify({ result, queue }, null, 2));
|
|
1786
|
+
if (queue)
|
|
1787
|
+
console.log(formatReviewQueue(queue));
|
|
1788
|
+
else
|
|
1789
|
+
console.log(`skills.sh (${result.status}): ${result.records.length} metadata lead(s); leaderboard popularity is not a safety or quality verdict.`);
|
|
1790
|
+
for (const issue of result.issues)
|
|
1791
|
+
console.error(`Warning: ${issue.message}`);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (options.source === "mcp-registry") {
|
|
1795
|
+
const result = await discoverOfficialMcpRegistry({
|
|
1796
|
+
maxRecords: limit,
|
|
1797
|
+
...(options.query ? { search: options.query } : {}),
|
|
1798
|
+
});
|
|
1799
|
+
const queue = options.queue
|
|
1800
|
+
? await mergeReviewQueue(result.records, await loadEffectiveCatalog())
|
|
1801
|
+
: undefined;
|
|
1802
|
+
if (options.json)
|
|
1803
|
+
return console.log(JSON.stringify({ result, queue }, null, 2));
|
|
1804
|
+
if (queue)
|
|
1805
|
+
console.log(formatReviewQueue(queue));
|
|
1806
|
+
else
|
|
1807
|
+
console.log(`Official MCP Registry (${result.status}): ${result.records.length} identity/distribution lead(s); registry presence is not a Loadout safety approval.`);
|
|
1808
|
+
for (const issue of result.issues)
|
|
1809
|
+
console.error(`Warning: ${issue.message}`);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
if (options.source !== "hacker-news")
|
|
1813
|
+
throw new Error(`Unsupported discovery source '${options.source}'. Supported: github, hacker-news, skills-sh, mcp-registry, all`);
|
|
1814
|
+
const minScore = Number(options.minScore);
|
|
1815
|
+
if (!Number.isFinite(minScore) || minScore < 0)
|
|
1816
|
+
throw new Error("--min-score must be a non-negative number");
|
|
1817
|
+
const result = await discoverHackerNewsRepositories({
|
|
1818
|
+
limit,
|
|
1819
|
+
minScore,
|
|
1820
|
+
keywords: options.query?.split(",") ?? [],
|
|
1821
|
+
});
|
|
1822
|
+
if (options.queue) {
|
|
1823
|
+
const queue = await mergeReviewQueue(result.candidates, await loadEffectiveCatalog());
|
|
1824
|
+
if (options.json)
|
|
1825
|
+
return console.log(JSON.stringify(queue, null, 2));
|
|
1826
|
+
console.log(formatReviewQueue(queue));
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (options.json)
|
|
1830
|
+
return console.log(JSON.stringify(result, null, 2));
|
|
1831
|
+
console.log(`Hacker News: ${result.candidates.length} GitHub repository lead(s) from ${result.storiesScanned} stories.`);
|
|
1832
|
+
for (const candidate of result.candidates) {
|
|
1833
|
+
console.log(`★${candidate.score} · ${candidate.repository} — ${candidate.title}\n ${candidate.discussionUrl}`);
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
program
|
|
1837
|
+
.command("review-queue")
|
|
1838
|
+
.description("Show deduplicated discovery leads awaiting human review; never installs")
|
|
1839
|
+
.option("--decision <value>", "filter: pending, shortlisted, or ignored")
|
|
1840
|
+
.option("--json", "emit machine-readable JSON")
|
|
1841
|
+
.action(async (options) => {
|
|
1842
|
+
const queue = await readReviewQueue();
|
|
1843
|
+
if (options.decision &&
|
|
1844
|
+
!["pending", "shortlisted", "ignored"].includes(options.decision))
|
|
1845
|
+
throw new Error("--decision must be pending, shortlisted, or ignored");
|
|
1846
|
+
const filtered = options.decision
|
|
1847
|
+
? {
|
|
1848
|
+
...queue,
|
|
1849
|
+
items: queue.items.filter((item) => item.decision === options.decision),
|
|
1850
|
+
}
|
|
1851
|
+
: queue;
|
|
1852
|
+
console.log(options.json
|
|
1853
|
+
? JSON.stringify(filtered, null, 2)
|
|
1854
|
+
: formatReviewQueue(filtered));
|
|
1855
|
+
});
|
|
1856
|
+
program
|
|
1857
|
+
.command("review")
|
|
1858
|
+
.description("Record a human queue decision; shortlisting still does not promote or install")
|
|
1859
|
+
.argument("<repository>", "owner/repository")
|
|
1860
|
+
.requiredOption("--decision <value>", "pending, shortlisted, or ignored")
|
|
1861
|
+
.action(async (repository, options) => {
|
|
1862
|
+
if (!["pending", "shortlisted", "ignored"].includes(options.decision))
|
|
1863
|
+
throw new Error("--decision must be pending, shortlisted, or ignored");
|
|
1864
|
+
const item = await setReviewDecision(repository, options.decision);
|
|
1865
|
+
console.log(`${item.repository}: ${item.decision}. No catalog or agent files changed.`);
|
|
1866
|
+
});
|
|
1867
|
+
const credentials = program
|
|
1868
|
+
.command("credentials")
|
|
1869
|
+
.description("Store, inspect, or remove secrets in the native OS credential store");
|
|
1870
|
+
credentials
|
|
1871
|
+
.command("status")
|
|
1872
|
+
.description("Check whether the native OS credential backend is available")
|
|
1873
|
+
.option("--json", "emit machine-readable status")
|
|
1874
|
+
.action(async (options) => {
|
|
1875
|
+
const status = await createOsCredentialStore().status();
|
|
1876
|
+
console.log(options.json
|
|
1877
|
+
? JSON.stringify(status, null, 2)
|
|
1878
|
+
: `${status.backend}: ${status.available ? "available" : "unavailable"}`);
|
|
1879
|
+
if (!status.available)
|
|
1880
|
+
process.exitCode = 1;
|
|
1881
|
+
});
|
|
1882
|
+
credentials
|
|
1883
|
+
.command("set")
|
|
1884
|
+
.description("Store a credential read from stdin; its value is never placed in arguments or output")
|
|
1885
|
+
.argument("<service>", "credential service identifier")
|
|
1886
|
+
.option("--account <account>", "credential account")
|
|
1887
|
+
.requiredOption("--stdin", "require secret input from stdin")
|
|
1888
|
+
.action(async (service, options) => {
|
|
1889
|
+
await createOsCredentialStore().set({
|
|
1890
|
+
kind: "os-keychain",
|
|
1891
|
+
service,
|
|
1892
|
+
...(options.account ? { account: options.account } : {}),
|
|
1893
|
+
}, await readCredentialFromStdin());
|
|
1894
|
+
console.log(`Stored '${service}' in the native OS credential store.`);
|
|
1895
|
+
});
|
|
1896
|
+
credentials
|
|
1897
|
+
.command("check")
|
|
1898
|
+
.description("Check whether one credential resolves without printing it")
|
|
1899
|
+
.argument("<service>", "credential service identifier")
|
|
1900
|
+
.option("--account <account>", "credential account")
|
|
1901
|
+
.option("--json", "emit machine-readable status")
|
|
1902
|
+
.action(async (service, options) => {
|
|
1903
|
+
const found = Boolean(await createOsCredentialStore().get({
|
|
1904
|
+
kind: "os-keychain",
|
|
1905
|
+
service,
|
|
1906
|
+
...(options.account ? { account: options.account } : {}),
|
|
1907
|
+
}));
|
|
1908
|
+
console.log(options.json
|
|
1909
|
+
? JSON.stringify({ service, found })
|
|
1910
|
+
: `${service}: ${found ? "stored" : "not found"}`);
|
|
1911
|
+
if (!found)
|
|
1912
|
+
process.exitCode = 1;
|
|
1913
|
+
});
|
|
1914
|
+
credentials
|
|
1915
|
+
.command("delete")
|
|
1916
|
+
.description("Remove one credential from the native OS store")
|
|
1917
|
+
.argument("<service>", "credential service identifier")
|
|
1918
|
+
.option("--account <account>", "credential account")
|
|
1919
|
+
.action(async (service, options) => {
|
|
1920
|
+
const deleted = await createOsCredentialStore().delete({
|
|
1921
|
+
kind: "os-keychain",
|
|
1922
|
+
service,
|
|
1923
|
+
...(options.account ? { account: options.account } : {}),
|
|
1924
|
+
});
|
|
1925
|
+
console.log(`${service}: ${deleted ? "deleted" : "not found"}`);
|
|
1926
|
+
});
|
|
1927
|
+
const models = program
|
|
1928
|
+
.command("models")
|
|
1929
|
+
.description("Plan, apply, inspect, or verify secret-free provider model selections");
|
|
1930
|
+
models
|
|
1931
|
+
.command("status")
|
|
1932
|
+
.description("Show configured model metadata and credential references")
|
|
1933
|
+
.option("--config <path>", "model configuration path")
|
|
1934
|
+
.option("--json", "emit machine-readable JSON")
|
|
1935
|
+
.action(async (options) => {
|
|
1936
|
+
const configuration = await readProviderModelConfiguration(options.config ?? defaultModelConfigurationPath());
|
|
1937
|
+
console.log(options.json
|
|
1938
|
+
? JSON.stringify(configuration ?? null, null, 2)
|
|
1939
|
+
: formatProviderModelConfiguration(configuration));
|
|
1940
|
+
});
|
|
1941
|
+
models
|
|
1942
|
+
.command("set")
|
|
1943
|
+
.description("Plan or store one provider selection; never stores a raw key")
|
|
1944
|
+
.requiredOption("--id <id>", "selection id")
|
|
1945
|
+
.requiredOption("--model <model>", "provider model identifier")
|
|
1946
|
+
.option("--provider <provider>", "provider id", "openrouter")
|
|
1947
|
+
.option("--endpoint <url>", "provider HTTPS endpoint", "https://openrouter.ai/api/v1")
|
|
1948
|
+
.option("--credential-env <name>", "environment variable reference (default: OPENROUTER_API_KEY)")
|
|
1949
|
+
.option("--credential-keychain <service>", "native OS credential service reference")
|
|
1950
|
+
.option("--credential-account <account>", "native credential account")
|
|
1951
|
+
.option("--agents <ids>", "comma-separated target agent ids")
|
|
1952
|
+
.option("--config <path>", "model configuration path")
|
|
1953
|
+
.option("--yes", "apply after preview")
|
|
1954
|
+
.option("--json", "emit machine-readable JSON")
|
|
1955
|
+
.action(async (options) => {
|
|
1956
|
+
if (options.credentialEnv && options.credentialKeychain)
|
|
1957
|
+
throw new Error("Choose either --credential-env or --credential-keychain");
|
|
1958
|
+
if (options.credentialAccount && !options.credentialKeychain)
|
|
1959
|
+
throw new Error("--credential-account requires --credential-keychain");
|
|
1960
|
+
const credential = options.credentialKeychain
|
|
1961
|
+
? {
|
|
1962
|
+
kind: "os-keychain",
|
|
1963
|
+
service: options.credentialKeychain,
|
|
1964
|
+
...(options.credentialAccount
|
|
1965
|
+
? { account: options.credentialAccount }
|
|
1966
|
+
: {}),
|
|
1967
|
+
}
|
|
1968
|
+
: {
|
|
1969
|
+
kind: "environment",
|
|
1970
|
+
name: options.credentialEnv ?? "OPENROUTER_API_KEY",
|
|
1971
|
+
};
|
|
1972
|
+
const plan = await planProviderModelSelection({
|
|
1973
|
+
id: options.id,
|
|
1974
|
+
provider: options.provider,
|
|
1975
|
+
model: options.model,
|
|
1976
|
+
endpoint: options.endpoint,
|
|
1977
|
+
credential,
|
|
1978
|
+
...(options.agents
|
|
1979
|
+
? { targetAgents: parseAgentSelection(options.agents) }
|
|
1980
|
+
: {}),
|
|
1981
|
+
}, options.config ?? defaultModelConfigurationPath());
|
|
1982
|
+
if (!options.yes) {
|
|
1983
|
+
console.log(options.json
|
|
1984
|
+
? JSON.stringify(plan, null, 2)
|
|
1985
|
+
: `${formatProviderModelConfiguration(plan.configuration)}\nPath: ${plan.path}\nDry run only. Re-run with --yes to save metadata and the credential reference.`);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
const snapshotId = await applyProviderModelSelection(plan);
|
|
1989
|
+
console.log(options.json
|
|
1990
|
+
? JSON.stringify({ configuration: plan.configuration, snapshotId }, null, 2)
|
|
1991
|
+
: `${formatProviderModelConfiguration(plan.configuration)}\nSaved. Snapshot: ${snapshotId}`);
|
|
1992
|
+
});
|
|
1993
|
+
models
|
|
1994
|
+
.command("verify")
|
|
1995
|
+
.description("Make one explicit minimal provider request using the referenced credential")
|
|
1996
|
+
.argument("<id>", "selection id")
|
|
1997
|
+
.option("--config <path>", "model configuration path")
|
|
1998
|
+
.action(async (id, options) => {
|
|
1999
|
+
const configuration = await readProviderModelConfiguration(options.config ?? defaultModelConfigurationPath());
|
|
2000
|
+
if (!configuration)
|
|
2001
|
+
throw new Error("No provider model configuration exists");
|
|
2002
|
+
await requestOpenRouter(configuration, id, [
|
|
2003
|
+
{
|
|
2004
|
+
role: "user",
|
|
2005
|
+
content: "Reply with the single word OK.",
|
|
2006
|
+
},
|
|
2007
|
+
], {
|
|
2008
|
+
resolveCredential: createCredentialResolver(),
|
|
2009
|
+
});
|
|
2010
|
+
console.log(`Verified model selection '${id}'. No credential value was stored or printed.`);
|
|
2011
|
+
});
|
|
2012
|
+
program
|
|
2013
|
+
.command("keygen")
|
|
2014
|
+
.description("Generate an Ed25519 signing keypair outside the repository")
|
|
2015
|
+
.requiredOption("--private-key <path>", "new private key path (owner-only)")
|
|
2016
|
+
.requiredOption("--public-key <path>", "new public key path")
|
|
2017
|
+
.action(async (options) => {
|
|
2018
|
+
const result = await generateSigningKeys(options.privateKey, options.publicKey);
|
|
2019
|
+
console.log(`Generated signing keys. Public fingerprint: ${result.fingerprint}\nPrivate key: ${result.privateKey}\nPublic key: ${result.publicKey}`);
|
|
2020
|
+
});
|
|
2021
|
+
program
|
|
2022
|
+
.command("catalog-sign")
|
|
2023
|
+
.description("Create a signed immutable catalog envelope")
|
|
2024
|
+
.requiredOption("--catalog <path>", "catalog JSON path")
|
|
2025
|
+
.requiredOption("--private-key <path>", "Ed25519 private key path")
|
|
2026
|
+
.requiredOption("--output <path>", "new signed snapshot path")
|
|
2027
|
+
.action(async (options) => {
|
|
2028
|
+
const envelope = await signJsonFile(options.catalog, options.privateKey, options.output);
|
|
2029
|
+
console.log(`Signed catalog snapshot with ${envelope.publicKeyFingerprint}.`);
|
|
2030
|
+
});
|
|
2031
|
+
program
|
|
2032
|
+
.command("catalog-verify")
|
|
2033
|
+
.description("Verify a signed catalog snapshot before trusting it")
|
|
2034
|
+
.requiredOption("--snapshot <path>", "signed snapshot path")
|
|
2035
|
+
.requiredOption("--public-key <path>", "trusted Ed25519 public key path")
|
|
2036
|
+
.action(async (options) => {
|
|
2037
|
+
const result = await verifyJsonFile(options.snapshot, options.publicKey);
|
|
2038
|
+
console.log(`${result.valid ? "VALID" : "INVALID"} catalog signature (${result.fingerprint})`);
|
|
2039
|
+
if (!result.valid)
|
|
2040
|
+
process.exitCode = 1;
|
|
2041
|
+
});
|
|
2042
|
+
program
|
|
2043
|
+
.command("completion")
|
|
2044
|
+
.description("Print a shell-completion script; redirect it to your shell profile")
|
|
2045
|
+
.argument("<shell>", "bash, zsh, fish, or powershell")
|
|
2046
|
+
.action((shell) => {
|
|
2047
|
+
process.stdout.write(renderShellCompletion(parseCompletionShell(shell)));
|
|
2048
|
+
});
|
|
2049
|
+
program
|
|
2050
|
+
.command("mcp-recipe")
|
|
2051
|
+
.description("Preview/configure a reviewed MCP recipe, or explicitly verify its real connection")
|
|
2052
|
+
.argument("[id]", "recipe id; omit to list reviewed recipes")
|
|
2053
|
+
.option("--config <path>", "target JSON MCP config path")
|
|
2054
|
+
.option("--yes", "write the reviewed server entry after preview")
|
|
2055
|
+
.option("--verify", "verify the configured entry without starting a server")
|
|
2056
|
+
.option("--connect", "launch the exact pinned artifact and perform an MCP initialize handshake")
|
|
2057
|
+
.option("--credential <mapping>", "credential mapping NAME=env:VARIABLE or NAME=keychain:SERVICE (repeatable)", collectOption, [])
|
|
2058
|
+
.option("--credential-account <account>", "account for keychain mappings")
|
|
2059
|
+
.option("--timeout <milliseconds>", "real connection timeout", "8000")
|
|
2060
|
+
.option("--approve-risk", "approve launching the reviewed pinned MCP artifact for --connect")
|
|
2061
|
+
.option("--json", "emit machine-readable JSON")
|
|
2062
|
+
.action(async (id, options) => {
|
|
2063
|
+
if (!id) {
|
|
2064
|
+
if (options.connect || options.verify || options.yes)
|
|
2065
|
+
throw new Error("Select an MCP recipe id for this operation");
|
|
2066
|
+
const listed = REVIEWED_MCP_RECIPES.map((recipe) => ({
|
|
2067
|
+
id: recipe.id,
|
|
2068
|
+
displayName: recipe.displayName,
|
|
2069
|
+
source: recipe.source,
|
|
2070
|
+
permissions: recipe.permissions,
|
|
2071
|
+
environment: recipe.environment,
|
|
2072
|
+
}));
|
|
2073
|
+
console.log(options.json
|
|
2074
|
+
? JSON.stringify(listed, null, 2)
|
|
2075
|
+
: listed
|
|
2076
|
+
.map((recipe) => `${recipe.id} — ${recipe.displayName} — env: ${recipe.environment.length ? recipe.environment.join(", ") : "none"}`)
|
|
2077
|
+
.join("\n"));
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
if (options.connect) {
|
|
2081
|
+
if (options.verify || options.yes)
|
|
2082
|
+
throw new Error("--connect cannot be combined with --verify or --yes");
|
|
2083
|
+
const recipe = findMcpRecipe(id);
|
|
2084
|
+
const credentialReferences = {};
|
|
2085
|
+
for (const mapping of options.credential) {
|
|
2086
|
+
const separator = mapping.indexOf("=");
|
|
2087
|
+
if (separator <= 0)
|
|
2088
|
+
throw new Error(`Invalid --credential '${mapping}'; expected NAME=env:VARIABLE or NAME=keychain:SERVICE`);
|
|
2089
|
+
const name = mapping.slice(0, separator);
|
|
2090
|
+
const value = mapping.slice(separator + 1);
|
|
2091
|
+
if (!recipe.environment.includes(name))
|
|
2092
|
+
throw new Error(`Credential '${name}' is not required by recipe '${id}'`);
|
|
2093
|
+
if (value.startsWith("env:"))
|
|
2094
|
+
credentialReferences[name] = {
|
|
2095
|
+
kind: "environment",
|
|
2096
|
+
name: value.slice(4),
|
|
2097
|
+
};
|
|
2098
|
+
else if (value.startsWith("keychain:"))
|
|
2099
|
+
credentialReferences[name] = {
|
|
2100
|
+
kind: "os-keychain",
|
|
2101
|
+
service: value.slice(9),
|
|
2102
|
+
...(options.credentialAccount
|
|
2103
|
+
? { account: options.credentialAccount }
|
|
2104
|
+
: {}),
|
|
2105
|
+
};
|
|
2106
|
+
else
|
|
2107
|
+
throw new Error(`Invalid --credential '${mapping}'; use env: or keychain:`);
|
|
2108
|
+
}
|
|
2109
|
+
const timeoutMs = Number(options.timeout);
|
|
2110
|
+
const controller = new AbortController();
|
|
2111
|
+
const abort = () => controller.abort();
|
|
2112
|
+
process.once("SIGINT", abort);
|
|
2113
|
+
process.once("SIGTERM", abort);
|
|
2114
|
+
try {
|
|
2115
|
+
const result = await verifyMcpRecipeConnection(id, {
|
|
2116
|
+
approveRisk: Boolean(options.approveRisk),
|
|
2117
|
+
credentialReferences,
|
|
2118
|
+
timeoutMs,
|
|
2119
|
+
signal: controller.signal,
|
|
2120
|
+
});
|
|
2121
|
+
console.log(options.json
|
|
2122
|
+
? JSON.stringify(result, null, 2)
|
|
2123
|
+
: `Connected: ${result.recipeId} · MCP ${result.protocolVersion}${result.serverInfo ? ` · ${result.serverInfo.name}${result.serverInfo.version ? ` ${result.serverInfo.version}` : ""}` : ""}\n${result.checks.join("\n")}`);
|
|
2124
|
+
}
|
|
2125
|
+
finally {
|
|
2126
|
+
process.off("SIGINT", abort);
|
|
2127
|
+
process.off("SIGTERM", abort);
|
|
2128
|
+
}
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
if (!options.config)
|
|
2132
|
+
throw new Error("--config is required for recipe planning or --verify");
|
|
2133
|
+
if (options.verify) {
|
|
2134
|
+
if (options.yes)
|
|
2135
|
+
throw new Error("--verify cannot be combined with --yes");
|
|
2136
|
+
const verification = await verifyMcpRecipe(id, options.config);
|
|
2137
|
+
console.log(options.json
|
|
2138
|
+
? JSON.stringify(verification, null, 2)
|
|
2139
|
+
: `${verification.configured ? "Configured" : "Not configured"}: ${verification.recipeId}\n${[...verification.checks, ...verification.warnings].join("\n")}`);
|
|
2140
|
+
if (!verification.configured)
|
|
2141
|
+
process.exitCode = 1;
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
const plan = await planMcpRecipe(id, options.config);
|
|
2145
|
+
if (!options.yes) {
|
|
2146
|
+
console.log(options.json
|
|
2147
|
+
? JSON.stringify(plan, null, 2)
|
|
2148
|
+
: `${formatMcpRecipePlan(plan)}\nDry run only. Re-run with --yes to write this server entry.`);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const snapshot = await applyMcpConfigPlan(plan.config);
|
|
2152
|
+
console.log(options.json
|
|
2153
|
+
? JSON.stringify({ plan, snapshot }, null, 2)
|
|
2154
|
+
: `${formatMcpRecipePlan(plan)}\nConfigured. Snapshot: ${snapshot.id}\nAuthorize the service separately, then run: loadout mcp-recipe ${id} --config ${plan.config.path} --verify`);
|
|
2155
|
+
});
|
|
2156
|
+
program
|
|
2157
|
+
.command("mcp")
|
|
2158
|
+
.description("Inspect MCP manifests without executing servers or scripts")
|
|
2159
|
+
.option("--source <directory>", "local repository directory")
|
|
2160
|
+
.option("--repository <owner/repo>", "public GitHub repository")
|
|
2161
|
+
.option("--json", "emit normalized JSON")
|
|
2162
|
+
.action(async (options) => {
|
|
2163
|
+
if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1) {
|
|
2164
|
+
throw new Error("Provide exactly one of --source or --repository");
|
|
2165
|
+
}
|
|
2166
|
+
const source = options.repository
|
|
2167
|
+
? (await fetchRepositorySnapshot(options.repository)).path
|
|
2168
|
+
: options.source;
|
|
2169
|
+
const manifests = await discoverMcpManifests(source);
|
|
2170
|
+
if (options.json)
|
|
2171
|
+
console.log(JSON.stringify(manifests, null, 2));
|
|
2172
|
+
else if (manifests.length === 0)
|
|
2173
|
+
console.log("No supported MCP manifests found.");
|
|
2174
|
+
else
|
|
2175
|
+
for (const manifest of manifests)
|
|
2176
|
+
console.log(summarizeMcpManifest(manifest));
|
|
2177
|
+
});
|
|
2178
|
+
program
|
|
2179
|
+
.command("inspect")
|
|
2180
|
+
.description("Inspect skills and MCP components in a local directory or public GitHub repository")
|
|
2181
|
+
.option("--source <directory>", "local package directory")
|
|
2182
|
+
.option("--repository <owner/repo>", "public GitHub repository")
|
|
2183
|
+
.option("--json", "emit normalized JSON")
|
|
2184
|
+
.action(async (options) => {
|
|
2185
|
+
if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1)
|
|
2186
|
+
throw new Error("Provide exactly one of --source or --repository");
|
|
2187
|
+
const source = options.repository
|
|
2188
|
+
? (await fetchRepositorySnapshot(options.repository)).path
|
|
2189
|
+
: options.source;
|
|
2190
|
+
const result = await inspectPackage(source);
|
|
2191
|
+
console.log(options.json
|
|
2192
|
+
? JSON.stringify(result, null, 2)
|
|
2193
|
+
: formatPackageInspection(result));
|
|
2194
|
+
});
|
|
2195
|
+
program
|
|
2196
|
+
.command("evaluate")
|
|
2197
|
+
.description("Evaluate static skill and MCP evidence without executing package code")
|
|
2198
|
+
.option("--source <directory>", "local package directory")
|
|
2199
|
+
.option("--repository <owner/repo>", "public GitHub repository")
|
|
2200
|
+
.option("--json", "emit evaluation JSON")
|
|
2201
|
+
.action(async (options) => {
|
|
2202
|
+
if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1)
|
|
2203
|
+
throw new Error("Provide exactly one of --source or --repository");
|
|
2204
|
+
const source = options.repository
|
|
2205
|
+
? (await fetchRepositorySnapshot(options.repository)).path
|
|
2206
|
+
: options.source;
|
|
2207
|
+
const result = await evaluatePackage(source);
|
|
2208
|
+
console.log(options.json
|
|
2209
|
+
? JSON.stringify(result, null, 2)
|
|
2210
|
+
: formatPackageEvaluation(result));
|
|
2211
|
+
});
|
|
2212
|
+
program
|
|
2213
|
+
.command("head-to-head")
|
|
2214
|
+
.description("Score synthetic workflow or code-review trials and write signed evidence; never executes candidate content")
|
|
2215
|
+
.requiredOption("--fixture <path>", "synthetic fixture JSON path")
|
|
2216
|
+
.requiredOption("--trials <path>", "declared trial observations JSON path")
|
|
2217
|
+
.requiredOption("--private-key <path>", "Ed25519 private key PEM path")
|
|
2218
|
+
.requiredOption("--output <path>", "new signed evidence JSON path")
|
|
2219
|
+
.option("--json", "emit the signed envelope as JSON")
|
|
2220
|
+
.action(async (options) => {
|
|
2221
|
+
const [fixture, trials, privateKey] = await Promise.all([
|
|
2222
|
+
readFile(resolve(options.fixture), "utf8").then((value) => JSON.parse(value)),
|
|
2223
|
+
readFile(resolve(options.trials), "utf8").then((value) => JSON.parse(value)),
|
|
2224
|
+
readFile(resolve(options.privateKey), "utf8"),
|
|
2225
|
+
]);
|
|
2226
|
+
const evidence = runHeadToHeadHarness(fixture, trials);
|
|
2227
|
+
const envelope = await writeSignedHeadToHeadEvidence(evidence, privateKey, options.output);
|
|
2228
|
+
console.log(options.json
|
|
2229
|
+
? JSON.stringify(envelope, null, 2)
|
|
2230
|
+
: `Signed ${evidence.category} evidence for ${evidence.results.length} trial(s).\nOutput: ${resolve(options.output)}\nFingerprint: ${envelope.publicKeyFingerprint}`);
|
|
2231
|
+
});
|
|
2232
|
+
program
|
|
2233
|
+
.command("watch")
|
|
2234
|
+
.description("Watch for read-only updates; never applies changes automatically")
|
|
2235
|
+
.option("--interval <minutes>", "check interval", "1440")
|
|
2236
|
+
.option("--once", "check once and exit")
|
|
2237
|
+
.option("--json", "emit each notification as JSON")
|
|
2238
|
+
.action(async (options) => {
|
|
2239
|
+
const minutes = Number(options.interval);
|
|
2240
|
+
if (!Number.isFinite(minutes) || minutes <= 0)
|
|
2241
|
+
throw new Error("--interval must be a positive number of minutes");
|
|
2242
|
+
const notify = (notification) => console.log(options.json
|
|
2243
|
+
? JSON.stringify(notification)
|
|
2244
|
+
: `${notification.checkedAt}: ${notification.message}`);
|
|
2245
|
+
if (options.once) {
|
|
2246
|
+
notify(await checkForUpdates());
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
const stop = startUpdateWatcher({
|
|
2250
|
+
intervalMs: minutes * 60_000,
|
|
2251
|
+
notify,
|
|
2252
|
+
});
|
|
2253
|
+
const shutdown = () => {
|
|
2254
|
+
stop();
|
|
2255
|
+
process.exit(0);
|
|
2256
|
+
};
|
|
2257
|
+
process.once("SIGINT", shutdown);
|
|
2258
|
+
process.once("SIGTERM", shutdown);
|
|
2259
|
+
await new Promise(() => undefined);
|
|
2260
|
+
});
|
|
2261
|
+
for (const action of [
|
|
2262
|
+
"schedule",
|
|
2263
|
+
"unschedule",
|
|
2264
|
+
]) {
|
|
2265
|
+
program
|
|
2266
|
+
.command(action)
|
|
2267
|
+
.description(`${action === "schedule" ? "Install" : "Remove"} a native daily read-only update or candidate-discovery check`)
|
|
2268
|
+
.option("--time <HH:MM>", "local daily check time", "09:00")
|
|
2269
|
+
.option("--job <updates|discovery>", "daily read-only job", "updates")
|
|
2270
|
+
.option("--yes", "apply the native scheduler change")
|
|
2271
|
+
.option("--json", "emit machine-readable JSON")
|
|
2272
|
+
.action(async (options) => {
|
|
2273
|
+
if (options.job !== "updates" && options.job !== "discovery")
|
|
2274
|
+
throw new Error("--job must be updates or discovery");
|
|
2275
|
+
const plan = planNativeScheduler(action, {
|
|
2276
|
+
time: options.time,
|
|
2277
|
+
launcher: durableSchedulerLauncher(),
|
|
2278
|
+
job: options.job,
|
|
2279
|
+
});
|
|
2280
|
+
if (!options.yes) {
|
|
2281
|
+
console.log(options.json
|
|
2282
|
+
? JSON.stringify(plan, null, 2)
|
|
2283
|
+
: `${formatNativeScheduler(plan)}\nDry run only. Re-run with --yes to change the native scheduler.`);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
const snapshotId = await applyNativeScheduler(plan);
|
|
2287
|
+
console.log(options.json
|
|
2288
|
+
? JSON.stringify({ plan, snapshotId }, null, 2)
|
|
2289
|
+
: `${formatNativeScheduler(plan)}\nApplied. Snapshot: ${snapshotId}`);
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
program
|
|
2293
|
+
.command("sandbox-run")
|
|
2294
|
+
.description("Run an explicitly approved command in a disposable networkless Docker sandbox")
|
|
2295
|
+
.requiredOption("--source <directory>", "read-only source directory")
|
|
2296
|
+
.requiredOption("--image <image>", "reviewed/pinned Docker image reference")
|
|
2297
|
+
.requiredOption("--command <argument>", "command argument (repeatable; first is executable)", (value, previous = []) => [...previous, value], [])
|
|
2298
|
+
.requiredOption("--approve-risk", "explicitly approve sandbox execution")
|
|
2299
|
+
.option("--timeout <milliseconds>", "execution timeout", "120000")
|
|
2300
|
+
.option("--json", "emit result JSON")
|
|
2301
|
+
.action(async (options) => {
|
|
2302
|
+
const result = await runDisposableSandbox({
|
|
2303
|
+
sourceDirectory: options.source,
|
|
2304
|
+
image: options.image,
|
|
2305
|
+
command: options.command,
|
|
2306
|
+
approveRisk: options.approveRisk,
|
|
2307
|
+
timeoutMs: Number(options.timeout),
|
|
2308
|
+
});
|
|
2309
|
+
console.log(options.json
|
|
2310
|
+
? JSON.stringify(result, null, 2)
|
|
2311
|
+
: `Sandbox exited ${result.exitCode}\n${result.stdout}${result.stderr ? `\n${result.stderr}` : ""}`);
|
|
2312
|
+
if (result.exitCode !== 0)
|
|
2313
|
+
process.exitCode = result.exitCode;
|
|
2314
|
+
});
|
|
2315
|
+
program
|
|
2316
|
+
.command("mcp-config")
|
|
2317
|
+
.description("Plan or apply a safe MCP server configuration change (dry-run by default)")
|
|
2318
|
+
.requiredOption("--config <path>", "MCP JSON configuration path")
|
|
2319
|
+
.requiredOption("--name <name>", "server name")
|
|
2320
|
+
.option("--command <command>", "local server command")
|
|
2321
|
+
.option("--url <url>", "remote MCP server URL")
|
|
2322
|
+
.option("--arg <value>", "server argument (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
2323
|
+
.option("--env <NAME=VALUE>", "environment variable (repeatable; values are never printed)", (value, previous = []) => [...previous, value], [])
|
|
2324
|
+
.option("--yes", "apply the change; without this flag only a plan is shown")
|
|
2325
|
+
.action(async (options) => {
|
|
2326
|
+
if ((options.command ? 1 : 0) + (options.url ? 1 : 0) !== 1)
|
|
2327
|
+
throw new Error("Provide exactly one of --command or --url");
|
|
2328
|
+
const env = {};
|
|
2329
|
+
for (const item of options.env) {
|
|
2330
|
+
const separator = item.indexOf("=");
|
|
2331
|
+
if (separator <= 0)
|
|
2332
|
+
throw new Error(`Invalid --env '${item}'; expected NAME=VALUE`);
|
|
2333
|
+
const key = item.slice(0, separator);
|
|
2334
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
2335
|
+
throw new Error(`Invalid environment variable name '${key}'`);
|
|
2336
|
+
env[key] = item.slice(separator + 1);
|
|
2337
|
+
}
|
|
2338
|
+
const server = {
|
|
2339
|
+
name: options.name,
|
|
2340
|
+
command: options.command,
|
|
2341
|
+
url: options.url,
|
|
2342
|
+
args: options.arg,
|
|
2343
|
+
env,
|
|
2344
|
+
sourcePath: options.config,
|
|
2345
|
+
warnings: [],
|
|
2346
|
+
};
|
|
2347
|
+
const plan = await planMcpConfig(options.config, server);
|
|
2348
|
+
console.log(summarizeMcpConfigPlan(plan));
|
|
2349
|
+
if (!options.yes) {
|
|
2350
|
+
console.log("Dry run only. Re-run with --yes to apply this change.");
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
const snapshot = await applyMcpConfigPlan(plan);
|
|
2354
|
+
console.log(`Applied successfully. Snapshot: ${snapshot.id}`);
|
|
2355
|
+
});
|
|
2356
|
+
program
|
|
2357
|
+
.command("codex-mcp-config")
|
|
2358
|
+
.description("Plan or add a Codex TOML MCP server (dry-run by default)")
|
|
2359
|
+
.option("--config <path>", "Codex config.toml path", defaultCodexMcpConfigPath())
|
|
2360
|
+
.requiredOption("--name <name>", "server name")
|
|
2361
|
+
.option("--command <command>", "local server command")
|
|
2362
|
+
.option("--url <url>", "remote MCP server URL")
|
|
2363
|
+
.option("--arg <value>", "server argument (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
2364
|
+
.option("--env <NAME=VALUE>", "environment variable (repeatable; values are never printed)", (value, previous = []) => [...previous, value], [])
|
|
2365
|
+
.option("--yes", "apply the change; without this flag only a plan is shown")
|
|
2366
|
+
.action(async (options) => {
|
|
2367
|
+
if ((options.command ? 1 : 0) + (options.url ? 1 : 0) !== 1)
|
|
2368
|
+
throw new Error("Provide exactly one of --command or --url");
|
|
2369
|
+
const env = {};
|
|
2370
|
+
for (const item of options.env) {
|
|
2371
|
+
const separator = item.indexOf("=");
|
|
2372
|
+
if (separator <= 0)
|
|
2373
|
+
throw new Error(`Invalid --env '${item}'; expected NAME=VALUE`);
|
|
2374
|
+
const key = item.slice(0, separator);
|
|
2375
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
2376
|
+
throw new Error(`Invalid environment variable name '${key}'`);
|
|
2377
|
+
env[key] = item.slice(separator + 1);
|
|
2378
|
+
}
|
|
2379
|
+
const server = {
|
|
2380
|
+
name: options.name,
|
|
2381
|
+
command: options.command,
|
|
2382
|
+
url: options.url,
|
|
2383
|
+
args: options.arg,
|
|
2384
|
+
env,
|
|
2385
|
+
sourcePath: options.config,
|
|
2386
|
+
warnings: [],
|
|
2387
|
+
};
|
|
2388
|
+
const plan = await planCodexMcpConfig(options.config, server);
|
|
2389
|
+
console.log(`Codex config: ${plan.path}\n - ${plan.summary}`);
|
|
2390
|
+
if (!options.yes)
|
|
2391
|
+
return console.log("Dry run only. Re-run with --yes to add this Codex MCP server.");
|
|
2392
|
+
const snapshot = await applyCodexMcpConfigPlan(plan);
|
|
2393
|
+
console.log(`Applied successfully. Snapshot: ${snapshot.id}`);
|
|
2394
|
+
});
|
|
2395
|
+
program
|
|
2396
|
+
.command("plan")
|
|
2397
|
+
.description("Plan installing packages from a local directory, catalog, or public GitHub repository")
|
|
2398
|
+
.option("--source <directory>", "local package directory containing SKILL.md")
|
|
2399
|
+
.option("--repository <owner/repo>", "public GitHub repository containing SKILL.md")
|
|
2400
|
+
.option("--package <id>", "package identifier (repeat for custom mode)", (value, previous = []) => [...previous, value], [])
|
|
2401
|
+
.option("--mode <mode>", "catalog selection mode: stable, power, maximum, or custom")
|
|
2402
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to all detected agents")
|
|
2403
|
+
.action(async (options) => {
|
|
2404
|
+
const packageIds = options.package ?? [];
|
|
2405
|
+
const hasSource = Boolean(options.source || options.repository);
|
|
2406
|
+
if (hasSource && options.mode)
|
|
2407
|
+
throw new Error("--mode cannot be combined with --source or --repository");
|
|
2408
|
+
if (hasSource && packageIds.length !== 1)
|
|
2409
|
+
throw new Error("A source or repository requires exactly one --package");
|
|
2410
|
+
if (!hasSource && !options.mode)
|
|
2411
|
+
throw new Error("Provide --mode or exactly one of --source/--repository");
|
|
2412
|
+
if (!hasSource) {
|
|
2413
|
+
const prepared = await prepareCatalogInstall(setupSelection(options.mode, packageIds), {
|
|
2414
|
+
requestedAgents: parseAgentSelection(options.agents),
|
|
2415
|
+
onProgress: printSetupProgress,
|
|
2416
|
+
});
|
|
2417
|
+
console.log(JSON.stringify({
|
|
2418
|
+
mode: prepared.selection.mode,
|
|
2419
|
+
agents: prepared.agents.map((agent) => agent.id),
|
|
2420
|
+
packages: prepared.entries.map((entry) => ({
|
|
2421
|
+
...entry.plan,
|
|
2422
|
+
repository: entry.metadata?.repository,
|
|
2423
|
+
resolvedCommit: entry.metadata?.resolvedCommit,
|
|
2424
|
+
safety: entry.safety,
|
|
2425
|
+
})),
|
|
2426
|
+
skipped: prepared.skipped,
|
|
2427
|
+
profile: {
|
|
2428
|
+
deferred: prepared.resolution.deferred.map((pkg) => pkg.id),
|
|
2429
|
+
conflicts: prepared.resolution.conflicts,
|
|
2430
|
+
warnings: prepared.resolution.warnings,
|
|
2431
|
+
},
|
|
2432
|
+
}, null, 2));
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
const resolution = hasSource
|
|
2436
|
+
? undefined
|
|
2437
|
+
: resolveCatalogProfile(await loadEffectiveCatalog(), {
|
|
2438
|
+
mode: options.mode,
|
|
2439
|
+
packageIds,
|
|
2440
|
+
});
|
|
2441
|
+
const selected = hasSource
|
|
2442
|
+
? [{ id: packageIds[0] }]
|
|
2443
|
+
: resolution.packages;
|
|
2444
|
+
const agents = installedAgents(await detectAgents(), parseAgentSelection(options.agents));
|
|
2445
|
+
const plans = [];
|
|
2446
|
+
const skipped = [];
|
|
2447
|
+
for (const pkg of selected) {
|
|
2448
|
+
const fetched = options.repository
|
|
2449
|
+
? await fetchRepositorySnapshot(options.repository)
|
|
2450
|
+
: !options.source
|
|
2451
|
+
? await fetchRepositorySnapshot(pkg.repository)
|
|
2452
|
+
: undefined;
|
|
2453
|
+
try {
|
|
2454
|
+
plans.push(await buildSkillPlan(fetched?.path ?? options.source, pkg.id, agents));
|
|
2455
|
+
}
|
|
2456
|
+
catch (error) {
|
|
2457
|
+
if (!options.mode ||
|
|
2458
|
+
!(error instanceof Error) ||
|
|
2459
|
+
!error.message.startsWith("No SKILL.md found"))
|
|
2460
|
+
throw error;
|
|
2461
|
+
skipped.push({
|
|
2462
|
+
packageId: pkg.id,
|
|
2463
|
+
reason: "No SKILL.md found; this package is not skill-installable yet (inspect its MCP manifest instead).",
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
console.log(JSON.stringify(options.mode
|
|
2468
|
+
? {
|
|
2469
|
+
mode: options.mode,
|
|
2470
|
+
packages: plans,
|
|
2471
|
+
skipped,
|
|
2472
|
+
profile: resolution && {
|
|
2473
|
+
deferred: resolution.deferred.map((pkg) => pkg.id),
|
|
2474
|
+
conflicts: resolution.conflicts,
|
|
2475
|
+
warnings: resolution.warnings,
|
|
2476
|
+
},
|
|
2477
|
+
}
|
|
2478
|
+
: plans[0], null, 2));
|
|
2479
|
+
});
|
|
2480
|
+
program
|
|
2481
|
+
.command("install")
|
|
2482
|
+
.description("Install packages from a local directory, catalog, or public GitHub repository")
|
|
2483
|
+
.option("--source <directory>", "local package directory containing SKILL.md")
|
|
2484
|
+
.option("--repository <owner/repo>", "public GitHub repository containing SKILL.md")
|
|
2485
|
+
.option("--package <id>", "package identifier (repeat for custom mode)", (value, previous = []) => [...previous, value], [])
|
|
2486
|
+
.option("--mode <mode>", "catalog selection mode: stable, power, maximum, or custom")
|
|
2487
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to all detected agents")
|
|
2488
|
+
.option("--yes", "apply without interactive confirmation")
|
|
2489
|
+
.option("--approve-risk", "approve reviewed safety findings for catalog mode")
|
|
2490
|
+
.action(async (options) => {
|
|
2491
|
+
const packageIds = options.package ?? [];
|
|
2492
|
+
const hasSource = Boolean(options.source || options.repository);
|
|
2493
|
+
if (hasSource && options.mode)
|
|
2494
|
+
throw new Error("--mode cannot be combined with --source or --repository");
|
|
2495
|
+
if (hasSource && packageIds.length !== 1)
|
|
2496
|
+
throw new Error("A source or repository requires exactly one --package");
|
|
2497
|
+
if (!hasSource && !options.mode)
|
|
2498
|
+
throw new Error("Provide --mode or exactly one of --source/--repository");
|
|
2499
|
+
if (!hasSource) {
|
|
2500
|
+
const prepared = await prepareCatalogInstall(setupSelection(options.mode, packageIds), {
|
|
2501
|
+
requestedAgents: parseAgentSelection(options.agents),
|
|
2502
|
+
onProgress: printSetupProgress,
|
|
2503
|
+
});
|
|
2504
|
+
console.log(formatPreparedCatalogInstall(prepared));
|
|
2505
|
+
if (!options.yes) {
|
|
2506
|
+
console.log("Preview complete; nothing was changed. Use --yes after reviewing to install this exact plan.");
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
const snapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
2510
|
+
approveRisk: options.approveRisk,
|
|
2511
|
+
});
|
|
2512
|
+
console.log(`Installed ${prepared.entries.length} repositories as one transaction. Snapshot: ${snapshotId}`);
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
const resolution = hasSource
|
|
2516
|
+
? undefined
|
|
2517
|
+
: resolveCatalogProfile(await loadEffectiveCatalog(), {
|
|
2518
|
+
mode: options.mode,
|
|
2519
|
+
packageIds,
|
|
2520
|
+
});
|
|
2521
|
+
const selected = hasSource
|
|
2522
|
+
? [{ id: packageIds[0] }]
|
|
2523
|
+
: resolution.packages;
|
|
2524
|
+
const agents = installedAgents(await detectAgents(), parseAgentSelection(options.agents));
|
|
2525
|
+
const plans = [];
|
|
2526
|
+
const skipped = [];
|
|
2527
|
+
for (const pkg of selected) {
|
|
2528
|
+
const fetched = options.repository
|
|
2529
|
+
? await fetchRepositorySnapshot(options.repository)
|
|
2530
|
+
: !options.source
|
|
2531
|
+
? await fetchRepositorySnapshot(pkg.repository)
|
|
2532
|
+
: undefined;
|
|
2533
|
+
try {
|
|
2534
|
+
plans.push({
|
|
2535
|
+
plan: await buildSkillPlan(fetched?.path ?? options.source, pkg.id, agents),
|
|
2536
|
+
repository: fetched?.repository,
|
|
2537
|
+
commit: fetched?.commit,
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
catch (error) {
|
|
2541
|
+
if (!options.mode ||
|
|
2542
|
+
!(error instanceof Error) ||
|
|
2543
|
+
!error.message.startsWith("No SKILL.md found"))
|
|
2544
|
+
throw error;
|
|
2545
|
+
skipped.push({
|
|
2546
|
+
packageId: pkg.id,
|
|
2547
|
+
reason: "No SKILL.md found; this package is not skill-installable yet (inspect its MCP manifest instead).",
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
console.log(`Installing ${plans.map(({ plan }) => plan.packageId).join(", ")} for ${agents.map((agent) => agent.id).join(", ")}...`);
|
|
2552
|
+
for (const warning of resolution?.warnings ?? [])
|
|
2553
|
+
console.log(`Profile warning: ${warning}`);
|
|
2554
|
+
for (const entry of skipped)
|
|
2555
|
+
console.log(`Skipping ${entry.packageId}: ${entry.reason}`);
|
|
2556
|
+
if (!options.yes)
|
|
2557
|
+
console.log("Review the plan with `loadout plan`; use --yes to apply it.");
|
|
2558
|
+
if (!options.yes)
|
|
2559
|
+
return;
|
|
2560
|
+
for (const entry of plans) {
|
|
2561
|
+
const snapshotId = await applySkillInstall(entry.plan, entry.repository
|
|
2562
|
+
? { repository: entry.repository, resolvedCommit: entry.commit }
|
|
2563
|
+
: undefined);
|
|
2564
|
+
console.log(`Installed ${entry.plan.packageId} successfully. Snapshot: ${snapshotId}`);
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
program
|
|
2568
|
+
.command("rollback")
|
|
2569
|
+
.description("Restore the most recent Loadout snapshot")
|
|
2570
|
+
.option("--snapshot <id>", "specific snapshot id")
|
|
2571
|
+
.option("--list", "list snapshot ids without restoring anything")
|
|
2572
|
+
.action(async (options) => {
|
|
2573
|
+
if (options.list) {
|
|
2574
|
+
const snapshotIds = await listSnapshotIds();
|
|
2575
|
+
if (!snapshotIds.length)
|
|
2576
|
+
return console.log("No Loadout snapshots found.");
|
|
2577
|
+
for (const id of snapshotIds)
|
|
2578
|
+
console.log(id);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
const selected = await withMutationLock(async () => {
|
|
2582
|
+
const snapshotIds = await listSnapshotIds();
|
|
2583
|
+
const chosen = options.snapshot ?? snapshotIds.at(-1);
|
|
2584
|
+
if (!chosen)
|
|
2585
|
+
throw new Error("No Loadout snapshots found");
|
|
2586
|
+
await restoreSnapshot(await readSnapshot(chosen));
|
|
2587
|
+
return chosen;
|
|
2588
|
+
});
|
|
2589
|
+
console.log(`Restored snapshot ${selected}`);
|
|
2590
|
+
});
|
|
2591
|
+
program
|
|
2592
|
+
.command("update")
|
|
2593
|
+
.description("Plan updates, or apply one explicitly selected package update")
|
|
2594
|
+
.option("--json", "emit machine-readable JSON")
|
|
2595
|
+
.option("--apply", "apply the selected update")
|
|
2596
|
+
.option("--package <id>", "managed package id to update")
|
|
2597
|
+
.option("--approve-risk", "explicitly approve an update containing blocked safety findings")
|
|
2598
|
+
.action(async (options) => {
|
|
2599
|
+
if (options.apply) {
|
|
2600
|
+
if (!options.package)
|
|
2601
|
+
throw new Error("--apply requires --package <id>");
|
|
2602
|
+
const result = await applyPackageUpdate(options.package, {
|
|
2603
|
+
approveRisk: options.approveRisk,
|
|
2604
|
+
});
|
|
2605
|
+
console.log(`Updated ${options.package} to ${result.commit}. Snapshot: ${result.snapshotId}`);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const plans = await buildUpdatePlan();
|
|
2609
|
+
console.log(options.json ? JSON.stringify(plans, null, 2) : formatUpdatePlan(plans));
|
|
2610
|
+
});
|
|
2611
|
+
program
|
|
2612
|
+
.command("convert")
|
|
2613
|
+
.description("Convert a subagent or hook into a static, loss-reported artifact")
|
|
2614
|
+
.requiredOption("--kind <kind>", "subagent or hook")
|
|
2615
|
+
.requiredOption("--target <target>", "codex-skill, claude-skill, or static-review")
|
|
2616
|
+
.requiredOption("--name <name>", "source component name")
|
|
2617
|
+
.requiredOption("--input <path>", "UTF-8 instruction or hook source file")
|
|
2618
|
+
.requiredOption("--output <directory>", "artifact output directory")
|
|
2619
|
+
.option("--yes", "write the artifact; otherwise preview only")
|
|
2620
|
+
.option("--json", "emit machine-readable JSON")
|
|
2621
|
+
.action(async (options) => {
|
|
2622
|
+
const kinds = ["subagent", "hook"];
|
|
2623
|
+
const targets = [
|
|
2624
|
+
"codex-skill",
|
|
2625
|
+
"claude-skill",
|
|
2626
|
+
"static-review",
|
|
2627
|
+
];
|
|
2628
|
+
if (!kinds.includes(options.kind))
|
|
2629
|
+
throw new Error("--kind must be subagent or hook");
|
|
2630
|
+
if (!targets.includes(options.target))
|
|
2631
|
+
throw new Error("--target must be codex-skill, claude-skill, or static-review");
|
|
2632
|
+
const result = compileConversion({
|
|
2633
|
+
kind: options.kind,
|
|
2634
|
+
name: options.name,
|
|
2635
|
+
body: await readFile(options.input, "utf8"),
|
|
2636
|
+
}, options.target);
|
|
2637
|
+
const destination = resolve(options.output, result.relativePath);
|
|
2638
|
+
if (options.yes)
|
|
2639
|
+
await writeFileAtomically(destination, result.content, 0o600);
|
|
2640
|
+
const report = {
|
|
2641
|
+
...result,
|
|
2642
|
+
...(options.yes
|
|
2643
|
+
? { destination }
|
|
2644
|
+
: { destination, write: "preview-only" }),
|
|
2645
|
+
};
|
|
2646
|
+
console.log(options.json
|
|
2647
|
+
? JSON.stringify(report, null, 2)
|
|
2648
|
+
: `${options.yes ? "Wrote" : "Previewed"} ${destination}.\n` +
|
|
2649
|
+
`Preserved: ${result.preserved.join(", ")}. Dropped: ${result.dropped.length} field(s).\n` +
|
|
2650
|
+
(result.requiresApproval
|
|
2651
|
+
? "Manual approval is required before using this artifact."
|
|
2652
|
+
: "No additional approval is required."));
|
|
2653
|
+
});
|
|
2654
|
+
program
|
|
2655
|
+
.command("canary")
|
|
2656
|
+
.description("Run a static canary policy gate for a candidate without installing it")
|
|
2657
|
+
.requiredOption("--source <directory>", "candidate package directory")
|
|
2658
|
+
.requiredOption("--package <id>", "candidate package id")
|
|
2659
|
+
.option("--repository <owner/repo>", "candidate repository")
|
|
2660
|
+
.option("--commit <sha>", "candidate immutable commit")
|
|
2661
|
+
.option("--approve", "approve promotion if a promotion callback is supplied")
|
|
2662
|
+
.option("--allow-unready", "allow review findings in the static gate")
|
|
2663
|
+
.option("--json", "emit machine-readable JSON")
|
|
2664
|
+
.action(async (options) => {
|
|
2665
|
+
const result = await runCanary({
|
|
2666
|
+
packageId: options.package,
|
|
2667
|
+
root: options.source,
|
|
2668
|
+
...(options.repository ? { repository: options.repository } : {}),
|
|
2669
|
+
...(options.commit ? { commit: options.commit } : {}),
|
|
2670
|
+
}, { enabled: true, requireStaticReady: !options.allowUnready }, { approve: options.approve });
|
|
2671
|
+
console.log(options.json
|
|
2672
|
+
? JSON.stringify(result, null, 2)
|
|
2673
|
+
: formatCanaryResult(result));
|
|
2674
|
+
if (result.status === "blocked")
|
|
2675
|
+
process.exitCode = 1;
|
|
2676
|
+
});
|
|
2677
|
+
program
|
|
2678
|
+
.command("dashboard")
|
|
2679
|
+
.description("Start the local Loadout dashboard on a loopback-only port")
|
|
2680
|
+
.option("--port <port>", "TCP port (0 selects an available port)", "0")
|
|
2681
|
+
.action(async (options) => {
|
|
2682
|
+
const port = Number(options.port);
|
|
2683
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535)
|
|
2684
|
+
throw new Error("--port must be an integer between 0 and 65535");
|
|
2685
|
+
const handle = await startDashboardServer({}, port);
|
|
2686
|
+
console.log(`Loadout dashboard: http://${handle.host}:${handle.port}`);
|
|
2687
|
+
await new Promise((resolve) => {
|
|
2688
|
+
const stop = () => {
|
|
2689
|
+
process.off("SIGINT", stop);
|
|
2690
|
+
process.off("SIGTERM", stop);
|
|
2691
|
+
void handle.close().then(resolve);
|
|
2692
|
+
};
|
|
2693
|
+
process.on("SIGINT", stop);
|
|
2694
|
+
process.on("SIGTERM", stop);
|
|
2695
|
+
});
|
|
2696
|
+
});
|
|
2697
|
+
program
|
|
2698
|
+
.command("serve")
|
|
2699
|
+
.description("Start a loopback-only read-only API for status, health, catalog, and updates")
|
|
2700
|
+
.option("--port <port>", "TCP port (0 selects an available port)", "0")
|
|
2701
|
+
.action(async (options) => {
|
|
2702
|
+
const handle = await startApiServer({ port: Number(options.port) });
|
|
2703
|
+
console.log(`Loadout API listening at http://${handle.host}:${handle.port}`);
|
|
2704
|
+
await new Promise((resolve) => {
|
|
2705
|
+
process.once("SIGINT", resolve);
|
|
2706
|
+
process.once("SIGTERM", resolve);
|
|
2707
|
+
});
|
|
2708
|
+
await handle.close();
|
|
2709
|
+
});
|
|
2710
|
+
program.action(() => runSetup({ package: [] }));
|
|
2711
|
+
try {
|
|
2712
|
+
await recoverPendingTransactions();
|
|
2713
|
+
await program.parseAsync();
|
|
2714
|
+
}
|
|
2715
|
+
catch (error) {
|
|
2716
|
+
if (error instanceof CommanderError &&
|
|
2717
|
+
(error.code === "commander.helpDisplayed" ||
|
|
2718
|
+
error.code === "commander.version")) {
|
|
2719
|
+
// Help/version were already rendered successfully by Commander.
|
|
2720
|
+
}
|
|
2721
|
+
else {
|
|
2722
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2723
|
+
const jsonErrors = process.argv.includes("--json-errors") ||
|
|
2724
|
+
Boolean(program.opts().jsonErrors);
|
|
2725
|
+
console.error(jsonErrors
|
|
2726
|
+
? JSON.stringify({
|
|
2727
|
+
error: {
|
|
2728
|
+
code: error instanceof CommanderError ? error.code : "loadout.error",
|
|
2729
|
+
message,
|
|
2730
|
+
},
|
|
2731
|
+
})
|
|
2732
|
+
: `Error: ${message}`);
|
|
2733
|
+
process.exitCode = 1;
|
|
2734
|
+
}
|
|
2735
|
+
}
|