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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { managedFileReadPath } from "./active-set.js";
|
|
4
|
+
import { detectAgents } from "./paths.js";
|
|
5
|
+
import { readInstallState } from "./state.js";
|
|
6
|
+
import { buildUpdatePlan } from "./update.js";
|
|
7
|
+
async function drift(record, activations) {
|
|
8
|
+
const changed = [];
|
|
9
|
+
for (const file of record.files) {
|
|
10
|
+
try {
|
|
11
|
+
const digest = createHash("sha256")
|
|
12
|
+
.update(await readFile(managedFileReadPath(record.packageId, file.path, activations)))
|
|
13
|
+
.digest("hex");
|
|
14
|
+
if (digest !== file.sha256)
|
|
15
|
+
changed.push(file.path);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
changed.push(file.path);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return changed;
|
|
22
|
+
}
|
|
23
|
+
async function mcpDrift(configPath, serverName, expected) {
|
|
24
|
+
try {
|
|
25
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
26
|
+
return (createHash("sha256")
|
|
27
|
+
.update(JSON.stringify(config.mcpServers?.[serverName] ?? null))
|
|
28
|
+
.digest("hex") !== expected);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function buildHealthReport(options = {}) {
|
|
35
|
+
const [agents, state, updates] = await Promise.all([
|
|
36
|
+
detectAgents(),
|
|
37
|
+
readInstallState(),
|
|
38
|
+
options.updates
|
|
39
|
+
? options.updates()
|
|
40
|
+
: options.checkUpdates
|
|
41
|
+
? buildUpdatePlan()
|
|
42
|
+
: Promise.resolve([]),
|
|
43
|
+
]);
|
|
44
|
+
const drifted = (await Promise.all(state.installs.map((record) => drift(record, state.activations ?? [])))).flat();
|
|
45
|
+
const driftedMcpServers = (await Promise.all((state.mcpInstalls ?? []).map((entry) => mcpDrift(entry.configPath, entry.serverName, entry.fingerprint)))).filter(Boolean).length;
|
|
46
|
+
const findings = [];
|
|
47
|
+
if (!agents.some((agent) => agent.installed))
|
|
48
|
+
findings.push({
|
|
49
|
+
level: "error",
|
|
50
|
+
code: "no-agents",
|
|
51
|
+
message: "No supported AI coding agent was detected.",
|
|
52
|
+
fix: "Install an agent or make its command available on PATH.",
|
|
53
|
+
});
|
|
54
|
+
for (const agent of agents.filter((item) => item.installed))
|
|
55
|
+
findings.push({
|
|
56
|
+
level: "ok",
|
|
57
|
+
code: `agent-${agent.id}`,
|
|
58
|
+
message: `${agent.displayName} is detected.`,
|
|
59
|
+
});
|
|
60
|
+
if (state.installs.length === 0)
|
|
61
|
+
findings.push({
|
|
62
|
+
level: "info",
|
|
63
|
+
code: "no-packages",
|
|
64
|
+
message: "No Loadout-managed packages are installed yet.",
|
|
65
|
+
fix: "Choose a profile or install a package.",
|
|
66
|
+
});
|
|
67
|
+
if (drifted.length)
|
|
68
|
+
findings.push({
|
|
69
|
+
level: "warning",
|
|
70
|
+
code: "managed-file-drift",
|
|
71
|
+
message: `${drifted.length} managed file(s) changed or disappeared outside Loadout.`,
|
|
72
|
+
fix: "Review the files, then reinstall or remove the owning package.",
|
|
73
|
+
});
|
|
74
|
+
if (driftedMcpServers)
|
|
75
|
+
findings.push({
|
|
76
|
+
level: "warning",
|
|
77
|
+
code: "managed-mcp-drift",
|
|
78
|
+
message: `${driftedMcpServers} managed MCP server entry or entries changed or disappeared outside Loadout.`,
|
|
79
|
+
fix: "Review the MCP config, then synchronize or remove the owning package.",
|
|
80
|
+
});
|
|
81
|
+
const available = updates.filter((update) => update.status === "update-available");
|
|
82
|
+
if (available.length)
|
|
83
|
+
findings.push({
|
|
84
|
+
level: available.some((update) => update.approvalRequired)
|
|
85
|
+
? "warning"
|
|
86
|
+
: "info",
|
|
87
|
+
code: "updates-available",
|
|
88
|
+
message: `${available.length} package update(s) are available.`,
|
|
89
|
+
fix: "Run loadout update and review the safety findings.",
|
|
90
|
+
});
|
|
91
|
+
const errors = updates.filter((update) => update.status === "error");
|
|
92
|
+
if (errors.length)
|
|
93
|
+
findings.push({
|
|
94
|
+
level: "warning",
|
|
95
|
+
code: "update-check-failed",
|
|
96
|
+
message: `${errors.length} update check(s) could not be completed.`,
|
|
97
|
+
fix: "Check connectivity and retry.",
|
|
98
|
+
});
|
|
99
|
+
const status = findings.some((finding) => finding.level === "error")
|
|
100
|
+
? "unhealthy"
|
|
101
|
+
: findings.some((finding) => finding.level === "warning")
|
|
102
|
+
? "attention"
|
|
103
|
+
: "healthy";
|
|
104
|
+
return {
|
|
105
|
+
status,
|
|
106
|
+
generatedAt: new Date().toISOString(),
|
|
107
|
+
agents,
|
|
108
|
+
installedPackages: state.installs.length,
|
|
109
|
+
updatesChecked: Boolean(options.updates || options.checkUpdates),
|
|
110
|
+
updatesAvailable: available.length,
|
|
111
|
+
driftedFiles: drifted.length,
|
|
112
|
+
driftedMcpServers,
|
|
113
|
+
findings,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function formatHealthReport(report) {
|
|
117
|
+
const icon = report.status === "healthy"
|
|
118
|
+
? "✓"
|
|
119
|
+
: report.status === "attention"
|
|
120
|
+
? "!"
|
|
121
|
+
: "✗";
|
|
122
|
+
const lines = [
|
|
123
|
+
`${icon} Loadout health: ${report.status}`,
|
|
124
|
+
`Packages: ${report.installedPackages} installed, ${report.updatesChecked ? `${report.updatesAvailable} update(s)` : "updates not checked (use --updates)"}, ${report.driftedFiles} drifted file(s), ${report.driftedMcpServers} drifted MCP server(s)`,
|
|
125
|
+
];
|
|
126
|
+
for (const finding of report.findings)
|
|
127
|
+
lines.push(`${finding.level === "ok" ? "✓" : finding.level === "error" ? "✗" : finding.level === "warning" ? "!" : "•"} ${finding.message}${finding.fix ? ` ${finding.fix}` : ""}`);
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { buildHealthReport } from "./health.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { loadoutHome } from "./paths.js";
|
|
6
|
+
import { detectSecretKinds } from "./safety.js";
|
|
7
|
+
export function proposeImprovements(report) {
|
|
8
|
+
const proposals = [];
|
|
9
|
+
if (report.driftedFiles)
|
|
10
|
+
proposals.push({
|
|
11
|
+
priority: 100,
|
|
12
|
+
problem: "Managed files have drifted from recorded hashes.",
|
|
13
|
+
evidence: [
|
|
14
|
+
`${report.driftedFiles} drifted file(s) in the health report.`,
|
|
15
|
+
],
|
|
16
|
+
outcome: "Every drifted file is attributed to a package and can be safely reconciled or adopted.",
|
|
17
|
+
acceptanceTests: [
|
|
18
|
+
"Health reports exact owning packages without exposing content.",
|
|
19
|
+
"Sync dry-run explains restore, adopt, and ignore choices.",
|
|
20
|
+
"No unrelated file is changed.",
|
|
21
|
+
],
|
|
22
|
+
requiresHumanReview: true,
|
|
23
|
+
});
|
|
24
|
+
if (report.driftedMcpServers)
|
|
25
|
+
proposals.push({
|
|
26
|
+
priority: 100,
|
|
27
|
+
problem: "Managed MCP configuration has drifted.",
|
|
28
|
+
evidence: [
|
|
29
|
+
`${report.driftedMcpServers} drifted MCP server entry or entries in the health report.`,
|
|
30
|
+
],
|
|
31
|
+
outcome: "Every MCP change is attributed and can be reviewed, restored, adopted, or removed without changing unrelated servers.",
|
|
32
|
+
acceptanceTests: [
|
|
33
|
+
"Health identifies the owning package and config path without exposing secret values.",
|
|
34
|
+
"Sync dry-run preserves unrelated keys and servers.",
|
|
35
|
+
"Recovery restores exact previous configuration and state.",
|
|
36
|
+
],
|
|
37
|
+
requiresHumanReview: true,
|
|
38
|
+
});
|
|
39
|
+
if (report.updatesAvailable)
|
|
40
|
+
proposals.push({
|
|
41
|
+
priority: 80,
|
|
42
|
+
problem: "Installed packages have pending updates.",
|
|
43
|
+
evidence: [`${report.updatesAvailable} update(s) in the health report.`],
|
|
44
|
+
outcome: "Produce reviewable update plans and safely apply approved updates.",
|
|
45
|
+
acceptanceTests: [
|
|
46
|
+
"Every update shows commit and changed components.",
|
|
47
|
+
"New scripts, hooks, domains, or environment names require approval.",
|
|
48
|
+
"Failed verification restores the prior snapshot.",
|
|
49
|
+
],
|
|
50
|
+
requiresHumanReview: true,
|
|
51
|
+
});
|
|
52
|
+
if (!report.installedPackages)
|
|
53
|
+
proposals.push({
|
|
54
|
+
priority: 50,
|
|
55
|
+
problem: "The setup has no Loadout-managed packages.",
|
|
56
|
+
evidence: ["Health report contains zero installed packages."],
|
|
57
|
+
outcome: "Offer an explained tested profile or project-aware recommendations.",
|
|
58
|
+
acceptanceTests: [
|
|
59
|
+
"Recommendations include plain-language reasons.",
|
|
60
|
+
"No package installs without confirmation.",
|
|
61
|
+
],
|
|
62
|
+
requiresHumanReview: false,
|
|
63
|
+
});
|
|
64
|
+
const missingAgents = report.agents.filter((agent) => !agent.installed);
|
|
65
|
+
if (missingAgents.length)
|
|
66
|
+
proposals.push({
|
|
67
|
+
priority: 20,
|
|
68
|
+
problem: "Some supported agents are not available.",
|
|
69
|
+
evidence: [missingAgents.map((agent) => agent.displayName).join(", ")],
|
|
70
|
+
outcome: "Keep unsupported targets disabled and explain how to enable them.",
|
|
71
|
+
acceptanceTests: [
|
|
72
|
+
"Plans never target an undetected agent by default.",
|
|
73
|
+
"Health output names missing agents without treating optional agents as corruption.",
|
|
74
|
+
],
|
|
75
|
+
requiresHumanReview: false,
|
|
76
|
+
});
|
|
77
|
+
return proposals.sort((a, b) => b.priority - a.priority);
|
|
78
|
+
}
|
|
79
|
+
export async function buildImprovementCycle(health = buildHealthReport) {
|
|
80
|
+
const candidates = proposeImprovements(await health());
|
|
81
|
+
const generatedAt = new Date().toISOString();
|
|
82
|
+
const priorOutcomes = await readPriorOutcomes();
|
|
83
|
+
const id = `${generatedAt.replace(/[:.]/g, "-")}-${createHash("sha256")
|
|
84
|
+
.update(JSON.stringify(candidates[0] ?? generatedAt))
|
|
85
|
+
.digest("hex")
|
|
86
|
+
.slice(0, 8)}`;
|
|
87
|
+
return {
|
|
88
|
+
id,
|
|
89
|
+
generatedAt,
|
|
90
|
+
rule: "evidence-first-human-reviewed",
|
|
91
|
+
selected: candidates[0],
|
|
92
|
+
candidates,
|
|
93
|
+
guardrails: [
|
|
94
|
+
"Never execute untrusted package code.",
|
|
95
|
+
"Never expose secrets.",
|
|
96
|
+
"Never weaken safety to make a test pass.",
|
|
97
|
+
"Never apply or publish without the required human approval.",
|
|
98
|
+
"Always prepare and verify rollback before mutation.",
|
|
99
|
+
],
|
|
100
|
+
priorOutcomes,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function formatImprovementCycle(cycle) {
|
|
104
|
+
if (!cycle.selected)
|
|
105
|
+
return "No evidence-backed improvement is currently required.";
|
|
106
|
+
const item = cycle.selected;
|
|
107
|
+
return [
|
|
108
|
+
`Next improvement: ${item.problem}`,
|
|
109
|
+
`Evidence: ${item.evidence.join(" ")}`,
|
|
110
|
+
`Desired outcome: ${item.outcome}`,
|
|
111
|
+
"Acceptance tests:",
|
|
112
|
+
...item.acceptanceTests.map((test) => ` - ${test}`),
|
|
113
|
+
`Human review: ${item.requiresHumanReview ? "required" : "required before release"}`,
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
const improvementHome = () => join(loadoutHome(), "improvements");
|
|
117
|
+
async function readPriorOutcomes() {
|
|
118
|
+
let names = [];
|
|
119
|
+
try {
|
|
120
|
+
names = (await readdir(improvementHome()))
|
|
121
|
+
.filter((name) => name.endsWith(".json"))
|
|
122
|
+
.sort()
|
|
123
|
+
.slice(-20);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const outcomes = [];
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
try {
|
|
131
|
+
const cycle = JSON.parse(await readFile(join(improvementHome(), name), "utf8"));
|
|
132
|
+
const latest = cycle.feedback?.at(-1);
|
|
133
|
+
if (latest)
|
|
134
|
+
outcomes.push({
|
|
135
|
+
id: cycle.id,
|
|
136
|
+
outcome: latest.outcome,
|
|
137
|
+
...(latest.note ? { note: latest.note } : {}),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* corrupt historical entries are ignored by proposal generation */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return outcomes;
|
|
145
|
+
}
|
|
146
|
+
export function improvementPrompt(cycle) {
|
|
147
|
+
const selected = cycle.selected;
|
|
148
|
+
return [
|
|
149
|
+
`# Loadout improvement cycle ${cycle.id}`,
|
|
150
|
+
"",
|
|
151
|
+
"You are improving Loadout using evidence, not guesses. Work only on the authorized developer branch.",
|
|
152
|
+
"",
|
|
153
|
+
"## Selected problem",
|
|
154
|
+
"",
|
|
155
|
+
selected?.problem ?? "No evidence-backed problem is currently selected.",
|
|
156
|
+
"",
|
|
157
|
+
"## Evidence",
|
|
158
|
+
"",
|
|
159
|
+
...(selected?.evidence.map((item) => `- ${item}`) ?? ["- None"]),
|
|
160
|
+
"",
|
|
161
|
+
"## Required outcome",
|
|
162
|
+
"",
|
|
163
|
+
selected?.outcome ?? "Keep the current system stable.",
|
|
164
|
+
"",
|
|
165
|
+
"## Acceptance tests",
|
|
166
|
+
"",
|
|
167
|
+
...(selected?.acceptanceTests.map((item) => `- ${item}`) ?? [
|
|
168
|
+
"- No change required",
|
|
169
|
+
]),
|
|
170
|
+
"",
|
|
171
|
+
"## Loop rules",
|
|
172
|
+
"",
|
|
173
|
+
"1. Inspect current behavior before editing.",
|
|
174
|
+
"2. Implement the smallest complete solution without weakening safety or scope.",
|
|
175
|
+
"3. Add regression tests and plain-language documentation.",
|
|
176
|
+
"4. Run relevant unit, integration, security, build, and demo checks.",
|
|
177
|
+
"5. Stop for human review when trust, permissions, compatibility claims, publishing, or release behavior changes.",
|
|
178
|
+
"6. Never expose secrets, execute untrusted package code, rewrite Git history, force-push, merge, or publish automatically.",
|
|
179
|
+
"7. Record exact evidence, remaining risks, and the next priority.",
|
|
180
|
+
"",
|
|
181
|
+
"## Guardrails",
|
|
182
|
+
"",
|
|
183
|
+
...cycle.guardrails.map((item) => `- ${item}`),
|
|
184
|
+
"",
|
|
185
|
+
].join("\n");
|
|
186
|
+
}
|
|
187
|
+
export async function writeImprovementCycle(cycle, directory = improvementHome()) {
|
|
188
|
+
const root = resolve(directory);
|
|
189
|
+
await mkdir(root, { recursive: true });
|
|
190
|
+
const json = join(root, `${cycle.id}.json`);
|
|
191
|
+
const prompt = join(root, `${cycle.id}.md`);
|
|
192
|
+
await writeFile(json, `${JSON.stringify(cycle, null, 2)}\n`, {
|
|
193
|
+
mode: 0o600,
|
|
194
|
+
flag: "wx",
|
|
195
|
+
});
|
|
196
|
+
await writeFile(prompt, improvementPrompt(cycle), {
|
|
197
|
+
mode: 0o600,
|
|
198
|
+
flag: "wx",
|
|
199
|
+
});
|
|
200
|
+
return { json, prompt };
|
|
201
|
+
}
|
|
202
|
+
export async function recordImprovementOutcome(id, outcome, note, directory = improvementHome()) {
|
|
203
|
+
if (!/^[A-Za-z0-9-]+$/.test(id))
|
|
204
|
+
throw new Error("Invalid improvement cycle id");
|
|
205
|
+
if (note && note.length > 1_000)
|
|
206
|
+
throw new Error("Improvement outcome note is too long");
|
|
207
|
+
if (note && detectSecretKinds(note).length)
|
|
208
|
+
throw new Error("Improvement outcome note appears to contain secret material");
|
|
209
|
+
const path = join(resolve(directory), `${id}.json`);
|
|
210
|
+
const cycle = JSON.parse(await readFile(path, "utf8"));
|
|
211
|
+
if (cycle.id !== id)
|
|
212
|
+
throw new Error("Improvement cycle id does not match its file");
|
|
213
|
+
cycle.feedback = [
|
|
214
|
+
...(cycle.feedback ?? []),
|
|
215
|
+
{
|
|
216
|
+
outcome,
|
|
217
|
+
...(note ? { note } : {}),
|
|
218
|
+
recordedAt: new Date().toISOString(),
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
await writeFile(path, `${JSON.stringify(cycle, null, 2)}\n`, { mode: 0o600 });
|
|
222
|
+
return cycle;
|
|
223
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { cp, lstat, rm } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, posix, relative, win32 } from "node:path";
|
|
4
|
+
import { ensureDirectory, loadoutHome } from "./paths.js";
|
|
5
|
+
import { planAdapterSkillInstall } from "./adapters.js";
|
|
6
|
+
import { applySkillPlan, detectInstallConflicts, validateSkillDirectory, } from "./skills.js";
|
|
7
|
+
import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, } from "./state.js";
|
|
8
|
+
import { runMutationTransaction } from "./transaction.js";
|
|
9
|
+
export function installedAgents(agents, requested) {
|
|
10
|
+
const available = agents.filter((agent) => agent.installed);
|
|
11
|
+
if (!requested || requested.length === 0)
|
|
12
|
+
return available;
|
|
13
|
+
const selected = available.filter((agent) => requested.includes(agent.id));
|
|
14
|
+
if (selected.length !== requested.length) {
|
|
15
|
+
const missing = requested.filter((id) => !selected.some((agent) => agent.id === id));
|
|
16
|
+
throw new Error(`Requested agents are not detected: ${missing.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
return selected;
|
|
19
|
+
}
|
|
20
|
+
async function assertActiveTargetsUnoccupied(plans, options = {}) {
|
|
21
|
+
const occupied = [];
|
|
22
|
+
for (const target of [
|
|
23
|
+
...new Set(plans.flatMap((plan) => plan.files.map((file) => file.target))),
|
|
24
|
+
])
|
|
25
|
+
try {
|
|
26
|
+
await lstat(target);
|
|
27
|
+
occupied.push(target);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!error ||
|
|
31
|
+
typeof error !== "object" ||
|
|
32
|
+
!("code" in error) ||
|
|
33
|
+
error.code !== "ENOENT")
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
if (occupied.length && options.allowManagedReplacement) {
|
|
37
|
+
const state = await readInstallState();
|
|
38
|
+
const allowed = new Set(plans.flatMap((plan) => {
|
|
39
|
+
const install = state.installs.find((record) => record.packageId === plan.packageId);
|
|
40
|
+
const packageActivations = (state.activations ?? []).filter((record) => record.packageId === plan.packageId);
|
|
41
|
+
const active = packageActivations.length === 0 ||
|
|
42
|
+
packageActivations.some((record) => record.installationState === "installed" &&
|
|
43
|
+
record.activationState === "active");
|
|
44
|
+
if (!install || !active)
|
|
45
|
+
return [];
|
|
46
|
+
const recorded = install.files
|
|
47
|
+
.filter((file) => basename(file.path) === "SKILL.md")
|
|
48
|
+
.map((file) => dirname(file.path));
|
|
49
|
+
const legacy = packageActivations.length
|
|
50
|
+
? []
|
|
51
|
+
: plan.files
|
|
52
|
+
.map((file) => file.target)
|
|
53
|
+
.filter((target) => basename(target) === plan.packageId);
|
|
54
|
+
return [...recorded, ...legacy];
|
|
55
|
+
}));
|
|
56
|
+
if (occupied.every((target) => allowed.has(target)))
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (occupied.length)
|
|
60
|
+
throw new Error(`Installation refuses ${occupied.length} occupied skill target(s); scan, compare, or adopt them first. First target: ${occupied[0]}`);
|
|
61
|
+
}
|
|
62
|
+
export async function buildSkillPlan(source, packageId, agents, options = {}) {
|
|
63
|
+
if (!existsSync(source))
|
|
64
|
+
throw new Error(`Package source does not exist: ${source}`);
|
|
65
|
+
const sourceStat = await lstat(source);
|
|
66
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
67
|
+
throw new Error(`Package source must be a real directory: ${source}`);
|
|
68
|
+
}
|
|
69
|
+
// A repository may contain one skill at its root or several nested skills.
|
|
70
|
+
// Validate the root when present; planSkillInstall validates every nested skill.
|
|
71
|
+
if (existsSync(join(source, "SKILL.md")) &&
|
|
72
|
+
(!options.include ||
|
|
73
|
+
options.include({
|
|
74
|
+
path: source,
|
|
75
|
+
targetName: basename(source),
|
|
76
|
+
})))
|
|
77
|
+
await validateSkillDirectory(source);
|
|
78
|
+
const plans = await Promise.all(agents.map((agent) => planAdapterSkillInstall(source, packageId, agent, options)));
|
|
79
|
+
const files = plans.flatMap((plan) => plan.files);
|
|
80
|
+
const conflicts = detectInstallConflicts([
|
|
81
|
+
{
|
|
82
|
+
packageId,
|
|
83
|
+
files,
|
|
84
|
+
targetAgents: agents.map((agent) => agent.id),
|
|
85
|
+
warnings: [],
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
return {
|
|
89
|
+
packageId,
|
|
90
|
+
files,
|
|
91
|
+
targetAgents: agents.map((agent) => agent.id),
|
|
92
|
+
warnings: conflicts
|
|
93
|
+
.filter((item) => item.severity === "warning")
|
|
94
|
+
.map((item) => item.message),
|
|
95
|
+
conflicts,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export async function applySkillInstall(plan, metadata, options = {}) {
|
|
99
|
+
const blocking = (plan.conflicts ?? []).filter((conflict) => conflict.severity === "blocking");
|
|
100
|
+
if (blocking.length > 0)
|
|
101
|
+
throw new Error(`Installation blocked by conflicts: ${blocking.map((conflict) => conflict.message).join("; ")}`);
|
|
102
|
+
const applied = await runMutationTransaction(async () => {
|
|
103
|
+
await options.validateCurrentState?.();
|
|
104
|
+
await assertActiveTargetsUnoccupied([plan], options);
|
|
105
|
+
return {
|
|
106
|
+
targets: [...plan.files.map((file) => file.target), installStatePath()],
|
|
107
|
+
value: plan,
|
|
108
|
+
};
|
|
109
|
+
}, async (freshPlan, snapshot) => {
|
|
110
|
+
if (options.replaceManagedTargets)
|
|
111
|
+
for (const target of [
|
|
112
|
+
...new Set(freshPlan.files.map((file) => file.target)),
|
|
113
|
+
])
|
|
114
|
+
await rm(target, { recursive: true, force: true });
|
|
115
|
+
await applySkillPlan(freshPlan);
|
|
116
|
+
if (options.replaceManagedTargets)
|
|
117
|
+
for (const file of freshPlan.files) {
|
|
118
|
+
const expected = (await hashDirectory(file.source))
|
|
119
|
+
.map((entry) => ({
|
|
120
|
+
path: relative(file.source, entry.path),
|
|
121
|
+
sha256: entry.sha256,
|
|
122
|
+
}))
|
|
123
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
124
|
+
const actual = (await hashDirectory(file.target))
|
|
125
|
+
.map((entry) => ({
|
|
126
|
+
path: relative(file.target, entry.path),
|
|
127
|
+
sha256: entry.sha256,
|
|
128
|
+
}))
|
|
129
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
130
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected))
|
|
131
|
+
throw new Error(`Exact update copy verification failed for ${file.target}`);
|
|
132
|
+
}
|
|
133
|
+
await recordInstall(freshPlan, snapshot.id, metadata);
|
|
134
|
+
await options.verifyBeforeCommit?.(snapshot.id);
|
|
135
|
+
});
|
|
136
|
+
return applied.snapshotId;
|
|
137
|
+
}
|
|
138
|
+
/** Apply all selected packages as one filesystem transaction and one state update. */
|
|
139
|
+
export async function applySkillInstallBatch(entries, extraSnapshotPaths = []) {
|
|
140
|
+
if (!entries.length)
|
|
141
|
+
throw new Error("Installation batch is empty");
|
|
142
|
+
const conflicts = detectInstallConflicts(entries.map((entry) => entry.plan));
|
|
143
|
+
const blocking = conflicts.filter((conflict) => conflict.severity === "blocking");
|
|
144
|
+
if (blocking.length)
|
|
145
|
+
throw new Error(`Installation blocked by conflicts: ${blocking.map((item) => item.message).join("; ")}`);
|
|
146
|
+
const applied = await runMutationTransaction(async () => {
|
|
147
|
+
await assertActiveTargetsUnoccupied(entries.map((entry) => entry.plan));
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
entry.plan.conflicts = [
|
|
150
|
+
...(entry.plan.conflicts ?? []),
|
|
151
|
+
...conflicts.filter((conflict) => conflict.packageIds.includes(entry.plan.packageId)),
|
|
152
|
+
];
|
|
153
|
+
entry.plan.warnings = [
|
|
154
|
+
...new Set([
|
|
155
|
+
...entry.plan.warnings,
|
|
156
|
+
...conflicts
|
|
157
|
+
.filter((conflict) => conflict.severity === "warning" &&
|
|
158
|
+
conflict.packageIds.includes(entry.plan.packageId))
|
|
159
|
+
.map((conflict) => conflict.message),
|
|
160
|
+
]),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
targets: [
|
|
165
|
+
...entries.flatMap((entry) => entry.plan.files.map((file) => file.target)),
|
|
166
|
+
installStatePath(),
|
|
167
|
+
...extraSnapshotPaths,
|
|
168
|
+
],
|
|
169
|
+
value: entries,
|
|
170
|
+
};
|
|
171
|
+
}, async (freshEntries, snapshot) => {
|
|
172
|
+
for (const entry of freshEntries)
|
|
173
|
+
await applySkillPlan(entry.plan);
|
|
174
|
+
await recordInstallBatch(freshEntries, snapshot.id);
|
|
175
|
+
});
|
|
176
|
+
return applied.snapshotId;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Download a batch into the reviewed library without exposing any skill to an
|
|
180
|
+
* agent yet. This is the safe destination for Maximum Library.
|
|
181
|
+
*/
|
|
182
|
+
export async function applySkillLibraryBatch(entries) {
|
|
183
|
+
if (!entries.length)
|
|
184
|
+
throw new Error("Library batch is empty");
|
|
185
|
+
const conflicts = detectInstallConflicts(entries.map((entry) => entry.plan));
|
|
186
|
+
const blocking = conflicts.filter((conflict) => conflict.severity === "blocking");
|
|
187
|
+
if (blocking.length)
|
|
188
|
+
throw new Error(`Library installation blocked by conflicts: ${blocking.map((item) => item.message).join("; ")}`);
|
|
189
|
+
const libraryPaths = [
|
|
190
|
+
...new Set(entries.flatMap((entry) => entry.plan.targetAgents.map((agent) => activationLibraryPath(entry.plan.packageId, agent)))),
|
|
191
|
+
];
|
|
192
|
+
const applied = await runMutationTransaction(async () => {
|
|
193
|
+
const state = await readInstallState();
|
|
194
|
+
const selected = new Set(entries.map((entry) => entry.plan.packageId));
|
|
195
|
+
const active = (state.activations ?? []).filter((record) => selected.has(record.packageId) &&
|
|
196
|
+
record.installationState === "installed" &&
|
|
197
|
+
record.activationState === "active");
|
|
198
|
+
if (active.length)
|
|
199
|
+
throw new Error(`Maximum Library will not relabel ${active.length} active managed skill(s) as disabled. Disable the selected packages first, then retry.`);
|
|
200
|
+
return {
|
|
201
|
+
targets: [...libraryPaths, installStatePath()],
|
|
202
|
+
value: entries,
|
|
203
|
+
};
|
|
204
|
+
}, async (freshEntries, snapshot) => {
|
|
205
|
+
for (const path of libraryPaths)
|
|
206
|
+
await rm(path, { recursive: true, force: true });
|
|
207
|
+
for (const entry of freshEntries) {
|
|
208
|
+
for (const file of entry.plan.files) {
|
|
209
|
+
const agent = file.targetAgent ??
|
|
210
|
+
(entry.plan.targetAgents.length === 1
|
|
211
|
+
? entry.plan.targetAgents[0]
|
|
212
|
+
: undefined);
|
|
213
|
+
if (!agent)
|
|
214
|
+
throw new Error(`Cannot place ${entry.plan.packageId} in the library: target agent is ambiguous`);
|
|
215
|
+
const unitId = basename(file.target);
|
|
216
|
+
const unitLibraryPath = activationLibraryPath(entry.plan.packageId, agent, unitId);
|
|
217
|
+
await ensureDirectory(unitLibraryPath);
|
|
218
|
+
const destination = join(unitLibraryPath, basename(file.target));
|
|
219
|
+
await cp(file.source, destination, {
|
|
220
|
+
recursive: true,
|
|
221
|
+
errorOnExist: true,
|
|
222
|
+
force: false,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
await recordLibraryInstallBatch(freshEntries, snapshot.id);
|
|
227
|
+
});
|
|
228
|
+
return applied.snapshotId;
|
|
229
|
+
}
|
|
230
|
+
export function snapshotPath(snapshotId, environment = process.env, platform = process.platform) {
|
|
231
|
+
const path = platform === "win32" ? win32 : posix;
|
|
232
|
+
return path.join(loadoutHome(environment, platform), "snapshots", `${snapshotId}.json`);
|
|
233
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { intelligenceFeedPayloadSchema, } from "./intelligence-feed.js";
|
|
2
|
+
function publicDiscoveryArtifact(value) {
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4
|
+
throw new Error("Discovery artifact must be an object");
|
|
5
|
+
const artifact = value;
|
|
6
|
+
if (artifact.schemaVersion !== 1 ||
|
|
7
|
+
typeof artifact.generatedAt !== "string" ||
|
|
8
|
+
!Number.isFinite(Date.parse(artifact.generatedAt)) ||
|
|
9
|
+
!Array.isArray(artifact.repositories) ||
|
|
10
|
+
artifact.repositories.length > 5_000)
|
|
11
|
+
throw new Error("Discovery artifact schema is invalid");
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
for (const repository of artifact.repositories) {
|
|
14
|
+
if (!repository ||
|
|
15
|
+
typeof repository.repository !== "string" ||
|
|
16
|
+
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository.repository) ||
|
|
17
|
+
repository.url !== `https://github.com/${repository.repository}` ||
|
|
18
|
+
!["candidate", "reviewed"].includes(repository.catalogStatus))
|
|
19
|
+
throw new Error("Discovery repository contains a non-public or invalid identity");
|
|
20
|
+
const key = repository.repository.toLowerCase();
|
|
21
|
+
if (seen.has(key))
|
|
22
|
+
throw new Error(`Duplicate discovery repository: ${key}`);
|
|
23
|
+
seen.add(key);
|
|
24
|
+
}
|
|
25
|
+
return artifact;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Convert central public discovery metadata into the signed feed's strict public
|
|
29
|
+
* subset. Descriptions, code, prompts, project data, and private identities are
|
|
30
|
+
* deliberately not accepted by this API.
|
|
31
|
+
*/
|
|
32
|
+
export function buildIntelligenceFeedPayload(options) {
|
|
33
|
+
const discovery = publicDiscoveryArtifact(options.discovery);
|
|
34
|
+
return intelligenceFeedPayloadSchema.parse({
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
sequence: options.sequence,
|
|
37
|
+
createdAt: discovery.generatedAt,
|
|
38
|
+
expiresAt: options.expiresAt,
|
|
39
|
+
publicDataOnly: true,
|
|
40
|
+
discoveryObservations: discovery.repositories.map((repository) => ({
|
|
41
|
+
id: `github:${repository.repository.toLowerCase()}`,
|
|
42
|
+
source: "github",
|
|
43
|
+
observedAt: repository.lastObservedAt ??
|
|
44
|
+
repository.firstObservedAt ??
|
|
45
|
+
discovery.generatedAt,
|
|
46
|
+
sourceUrl: repository.url,
|
|
47
|
+
identityKey: `github:${repository.repository.toLowerCase()}`,
|
|
48
|
+
signal: repository.catalogStatus === "reviewed"
|
|
49
|
+
? "public repository observed; separately present in the reviewed catalog"
|
|
50
|
+
: "public repository observed; candidate only, not trusted or recommended",
|
|
51
|
+
})),
|
|
52
|
+
compatibilityNotices: options.compatibilityNotices ?? [],
|
|
53
|
+
candidateSummaries: options.candidateSummaries ?? [],
|
|
54
|
+
benchmarkChanges: options.benchmarkChanges ?? [],
|
|
55
|
+
...(options.catalogRelease
|
|
56
|
+
? { catalogRelease: options.catalogRelease }
|
|
57
|
+
: {}),
|
|
58
|
+
});
|
|
59
|
+
}
|