loadout-ai 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -1
- package/README.md +160 -315
- package/SECURITY.md +21 -1
- package/catalog/discovered.json +31269 -26003
- package/catalog/packages.json +4 -4
- package/dist/src/cli.js +13 -0
- package/dist/src/commands/agents.js +3 -1
- package/dist/src/commands/catalog-candidate.js +150 -0
- package/dist/src/commands/catalog-workflows.js +147 -0
- package/dist/src/commands/catalog.js +9 -367
- package/dist/src/commands/coordinate.js +586 -0
- package/dist/src/commands/coordination-discussions.js +194 -0
- package/dist/src/commands/coordination-sessions.js +197 -0
- package/dist/src/commands/inventory.js +4 -2
- package/dist/src/core/agents/agent-inspection.js +26 -4
- package/dist/src/core/{routing → agents}/model-config.js +7 -2
- package/dist/src/core/catalog/catalog.js +1 -0
- package/dist/src/core/catalog/registry.js +44 -10
- package/dist/src/core/catalog/safety.js +36 -7
- package/dist/src/core/coordination/adapters/claude-code.js +154 -0
- package/dist/src/core/coordination/adapters/codex.js +137 -0
- package/dist/src/core/coordination/adapters/types.js +64 -0
- package/dist/src/core/coordination/auth.js +59 -0
- package/dist/src/core/coordination/bridge-lease.js +79 -0
- package/dist/src/core/coordination/conflict-preview.js +167 -0
- package/dist/src/core/coordination/contract-diff.js +106 -0
- package/dist/src/core/coordination/coordinator.js +552 -0
- package/dist/src/core/coordination/crash-recovery.js +169 -0
- package/dist/src/core/coordination/daemon.js +667 -0
- package/dist/src/core/coordination/discussion.js +340 -0
- package/dist/src/core/coordination/events.js +161 -0
- package/dist/src/core/coordination/http-api.js +118 -0
- package/dist/src/core/coordination/interrupt-policy.js +89 -0
- package/dist/src/core/coordination/lock.js +110 -0
- package/dist/src/core/coordination/mcp-server.js +424 -0
- package/dist/src/core/coordination/redaction.js +85 -0
- package/dist/src/core/coordination/replay.js +188 -0
- package/dist/src/core/coordination/retention.js +128 -0
- package/dist/src/core/coordination/runtime.js +21 -0
- package/dist/src/core/coordination/session-manager.js +366 -0
- package/dist/src/core/coordination/watcher.js +128 -0
- package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
- package/dist/src/core/{routing → delegation}/handoff.js +130 -63
- package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
- package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
- package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
- package/dist/src/core/discovery/candidate-intelligence.js +3 -214
- package/dist/src/core/discovery/community.js +12 -4
- package/dist/src/core/discovery/github-discovery.js +6 -2
- package/dist/src/core/discovery/private-discovery.js +6 -2
- package/dist/src/core/install/reconcile.js +1 -1
- package/dist/src/core/install/source.js +21 -7
- package/dist/src/core/install/uninstall.js +39 -3
- package/dist/src/core/reporting/cli-guide.js +10 -6
- package/dist/src/core/reporting/completion.js +61 -109
- package/dist/src/core/reporting/doctor.js +4 -6
- package/dist/src/core/runtime/bounded-json.js +65 -0
- package/dist/src/core/runtime/github.js +30 -16
- package/dist/src/core/runtime/mcp-recipes.js +1 -1
- package/dist/src/core/workspace/active-policy.js +1 -1
- package/docs/CANDIDATE_INTELLIGENCE.md +9 -2
- package/docs/CATALOG.md +1 -1
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +1 -1
- package/docs/DEMO_SCRIPT.md +19 -23
- package/docs/DISCOVERED.md +251 -250
- package/docs/FEATURE_TEST_MATRIX.md +10 -263
- package/docs/GITHUB_AUTHORIZATION.md +5 -0
- package/docs/LIVE_COLLABORATION.md +234 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +1 -1
- package/docs/REFERENCE.md +163 -0
- package/docs/RELEASE_REVIEW.md +1 -2
- package/docs/TESTING.md +17 -1
- package/docs/USER_TEST_GUIDE.md +138 -3
- package/docs/assets/loadout-discover-activate.webp +0 -0
- package/docs/assets/loadout-handoff-coordinate.webp +0 -0
- package/docs/assets/loadout-social-preview.png +0 -0
- package/docs/decisions/001-coordination-jsonl-locking.md +35 -0
- package/docs/decisions/002-local-daemon-authentication.md +32 -0
- package/docs/decisions/003-bounded-agent-discussions.md +91 -0
- package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
- package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
- package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
- package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -0
- package/package.json +18 -7
- package/skills/loadout-handoff/SKILL.md +238 -0
- package/MASTER_PLAN.md +0 -2207
- package/dist/src/core/routing/route.js +0 -539
- package/docs/ACTIVE_SET.md +0 -53
- package/docs/COMPATIBILITY_POLICY.md +0 -22
- package/docs/CONVERSION_AND_SANDBOX.md +0 -27
- package/docs/EVALUATION_PROTOCOL_V1.md +0 -300
- package/docs/HEAD_TO_HEAD_EVALUATION.md +0 -79
- package/docs/PROVIDER_CONFIGURATION.md +0 -45
- package/docs/README_RESEARCH.md +0 -36
- package/docs/REPOSITORY_STABILIZATION.md +0 -190
- package/docs/SAFE_UPDATE_DEMO.md +0 -25
- package/docs/SCHEMA_DECISIONS.md +0 -25
- package/docs/SUBMISSION_COPY.md +0 -90
- package/docs/TEAM_POLICY.md +0 -18
- package/docs/assets/loadout-workflow.png +0 -0
- package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +0 -283
- package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +0 -116
- package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +0 -469
- package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +0 -80
- package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +0 -55
- package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +0 -228
- package/skills/loadout-router/SKILL.md +0 -120
- /package/dist/src/core/{routing → agents}/credentials.js +0 -0
|
@@ -2,6 +2,32 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { writeFileAtomically } from "../install/atomic-file.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
const handoffMessageSchema = z.object({
|
|
7
|
+
id: z.string().trim().min(1),
|
|
8
|
+
type: z.enum([
|
|
9
|
+
"task",
|
|
10
|
+
"handoff",
|
|
11
|
+
"question",
|
|
12
|
+
"done",
|
|
13
|
+
"status",
|
|
14
|
+
"error",
|
|
15
|
+
"cancel",
|
|
16
|
+
]),
|
|
17
|
+
from: z.string().trim().min(1),
|
|
18
|
+
to: z.string().trim().min(1),
|
|
19
|
+
description: z.string().trim().min(1),
|
|
20
|
+
context: z.string().optional(),
|
|
21
|
+
timestamp: z.iso.datetime({ offset: true }),
|
|
22
|
+
resolves: z.string().trim().min(1).optional(),
|
|
23
|
+
});
|
|
24
|
+
function handoffValidationReason(error) {
|
|
25
|
+
return error.issues
|
|
26
|
+
.map((issue) => `${issue.path.join(".") || "message"}: ${issue.message}`)
|
|
27
|
+
.join("; ");
|
|
28
|
+
}
|
|
29
|
+
/** Message types that settle a task, so it stops appearing in an inbox. */
|
|
30
|
+
const TERMINAL_TYPES = new Set(["done", "error", "cancel"]);
|
|
5
31
|
const HANDOFF_DIR = ".handoff";
|
|
6
32
|
const MESSAGES_FILE = "messages.jsonl";
|
|
7
33
|
const PROTOCOL_FILE = "PROTOCOL.md";
|
|
@@ -24,46 +50,30 @@ export async function initHandoff(projectRoot) {
|
|
|
24
50
|
const dir = handoffDir(projectRoot);
|
|
25
51
|
await mkdir(dir, { recursive: true });
|
|
26
52
|
const protocol = [
|
|
27
|
-
"# Handoff
|
|
28
|
-
"",
|
|
29
|
-
"This directory enables asynchronous task handoff between AI coding agents.",
|
|
30
|
-
"Generated by `loadout handoff init`. Compatible with the JSONL handoff convention.",
|
|
31
|
-
"",
|
|
32
|
-
"## Message types",
|
|
33
|
-
"",
|
|
34
|
-
"- **task**: assign work to another agent",
|
|
35
|
-
"- **handoff**: transfer in-progress work with context",
|
|
36
|
-
"- **question**: ask another agent for input",
|
|
37
|
-
"- **done**: signal completion",
|
|
38
|
-
"- **status**: progress update",
|
|
39
|
-
"- **error**: report a failure",
|
|
40
|
-
"- **cancel**: withdraw a task",
|
|
41
|
-
"",
|
|
42
|
-
"## Files",
|
|
53
|
+
"# Handoff",
|
|
43
54
|
"",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
55
|
+
"A shared task log so two AI coding agents can pass work between them.",
|
|
56
|
+
"Created by `loadout handoff`. Append-only JSONL; no server, no daemon.",
|
|
46
57
|
"",
|
|
47
|
-
"##
|
|
58
|
+
"## Using it",
|
|
48
59
|
"",
|
|
49
60
|
"```",
|
|
50
|
-
"loadout handoff
|
|
51
|
-
"loadout handoff
|
|
52
|
-
"loadout handoff
|
|
53
|
-
"loadout handoff done <
|
|
61
|
+
"loadout handoff codex 'write unit tests for auth' --context 'see src/auth.ts'",
|
|
62
|
+
"loadout handoff codex # what is waiting for codex",
|
|
63
|
+
"loadout handoff # everything pending, both directions",
|
|
64
|
+
"loadout handoff --done <id> # finished",
|
|
54
65
|
"```",
|
|
55
66
|
"",
|
|
56
|
-
"
|
|
67
|
+
"Sending sets this directory up on first use and adds a short block to",
|
|
68
|
+
"CLAUDE.md and AGENTS.md telling each agent to check its inbox at session",
|
|
69
|
+
"start. Only the text between the loadout:handoff markers is managed.",
|
|
57
70
|
"",
|
|
58
|
-
"
|
|
71
|
+
"## Files",
|
|
59
72
|
"",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"```",
|
|
73
|
+
"- `messages.jsonl` — the log, one JSON object per line",
|
|
74
|
+
"- `PROTOCOL.md` — this file",
|
|
63
75
|
"",
|
|
64
|
-
"
|
|
65
|
-
"run `loadout handoff inbox <agent>` at session start and act on what it finds.",
|
|
66
|
-
"Only the block between the loadout:handoff markers is managed.",
|
|
76
|
+
"Commit both if you want the log shared across machines.",
|
|
67
77
|
"",
|
|
68
78
|
].join("\n");
|
|
69
79
|
await writeFileAtomically(join(dir, PROTOCOL_FILE), protocol);
|
|
@@ -78,66 +88,106 @@ export async function initHandoff(projectRoot) {
|
|
|
78
88
|
}
|
|
79
89
|
export async function sendHandoff(projectRoot, to, description, options = {}) {
|
|
80
90
|
if (!(await isHandoffInitialized(projectRoot))) {
|
|
81
|
-
throw new Error("Handoff not
|
|
91
|
+
throw new Error("Handoff is not set up here. Send a task and it will create itself: loadout handoff <agent> '<task>'");
|
|
82
92
|
}
|
|
83
|
-
const
|
|
93
|
+
const candidate = {
|
|
84
94
|
id: randomUUID().slice(0, 8),
|
|
85
95
|
type: options.type ?? "task",
|
|
86
96
|
from: options.from ?? "user",
|
|
87
97
|
to,
|
|
88
98
|
description,
|
|
89
99
|
...(options.context ? { context: options.context } : {}),
|
|
100
|
+
...(options.resolves ? { resolves: options.resolves } : {}),
|
|
90
101
|
timestamp: new Date().toISOString(),
|
|
91
102
|
};
|
|
103
|
+
const parsed = handoffMessageSchema.safeParse(candidate);
|
|
104
|
+
if (!parsed.success)
|
|
105
|
+
throw new Error(`Invalid handoff message: ${handoffValidationReason(parsed.error)}`);
|
|
106
|
+
const message = parsed.data;
|
|
92
107
|
const path = messagesPath(projectRoot);
|
|
93
108
|
const line = JSON.stringify(message) + "\n";
|
|
94
109
|
await writeFile(path, line, { flag: "a" });
|
|
95
110
|
return message;
|
|
96
111
|
}
|
|
97
112
|
export async function markDone(projectRoot, messageId) {
|
|
98
|
-
const
|
|
99
|
-
const original = messages.find((m) => m.id === messageId);
|
|
113
|
+
const state = await getHandoffState(projectRoot);
|
|
114
|
+
const original = state.messages.find((m) => m.id === messageId);
|
|
100
115
|
if (!original)
|
|
101
116
|
throw new Error(`Message '${messageId}' not found`);
|
|
102
|
-
if (original.type
|
|
103
|
-
throw new Error(`Message '${messageId}' is
|
|
117
|
+
if (original.type !== "task")
|
|
118
|
+
throw new Error(`Message '${messageId}' is not a task`);
|
|
119
|
+
if (state.done.some((message) => message.id === messageId))
|
|
120
|
+
throw new Error(`Message '${messageId}' is already settled`);
|
|
104
121
|
return sendHandoff(projectRoot, original.from, `Completed: ${original.description}`, {
|
|
105
122
|
from: original.to,
|
|
106
123
|
type: "done",
|
|
107
|
-
|
|
124
|
+
resolves: messageId,
|
|
108
125
|
});
|
|
109
126
|
}
|
|
110
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Parse the log one line at a time. A single truncated write previously made
|
|
129
|
+
* the whole inbox look empty, which is the worst possible failure for a queue:
|
|
130
|
+
* silent and total. Bad lines are collected and reported instead.
|
|
131
|
+
*/
|
|
132
|
+
export async function readMessagesDetailed(projectRoot) {
|
|
111
133
|
if (!(await isHandoffInitialized(projectRoot)))
|
|
112
|
-
return [];
|
|
134
|
+
return { messages: [], corrupt: [] };
|
|
135
|
+
let content;
|
|
113
136
|
try {
|
|
114
|
-
|
|
115
|
-
return content
|
|
116
|
-
.split("\n")
|
|
117
|
-
.filter(Boolean)
|
|
118
|
-
.map((line) => JSON.parse(line));
|
|
137
|
+
content = await readFile(messagesPath(projectRoot), "utf8");
|
|
119
138
|
}
|
|
120
139
|
catch {
|
|
121
|
-
return [];
|
|
140
|
+
return { messages: [], corrupt: [] };
|
|
122
141
|
}
|
|
142
|
+
const messages = [];
|
|
143
|
+
const corrupt = [];
|
|
144
|
+
content.split("\n").forEach((raw, index) => {
|
|
145
|
+
if (!raw.trim())
|
|
146
|
+
return;
|
|
147
|
+
try {
|
|
148
|
+
const parsed = handoffMessageSchema.safeParse(JSON.parse(raw));
|
|
149
|
+
if (!parsed.success) {
|
|
150
|
+
throw new Error(handoffValidationReason(parsed.error));
|
|
151
|
+
}
|
|
152
|
+
messages.push(parsed.data);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
corrupt.push({
|
|
156
|
+
line: index + 1,
|
|
157
|
+
reason: error instanceof Error ? error.message : "unparseable",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return { messages, corrupt };
|
|
162
|
+
}
|
|
163
|
+
export async function readMessages(projectRoot) {
|
|
164
|
+
return (await readMessagesDetailed(projectRoot)).messages;
|
|
123
165
|
}
|
|
124
166
|
export async function getHandoffState(projectRoot) {
|
|
125
167
|
const initialized = await isHandoffInitialized(projectRoot);
|
|
126
|
-
const messages = initialized
|
|
127
|
-
|
|
128
|
-
|
|
168
|
+
const { messages, corrupt } = initialized
|
|
169
|
+
? await readMessagesDetailed(projectRoot)
|
|
170
|
+
: { messages: [], corrupt: [] };
|
|
171
|
+
// A task is settled by completion, failure, or withdrawal. Treating only
|
|
172
|
+
// `done` as terminal left failed tasks pending forever.
|
|
173
|
+
const resolvedIds = new Set(messages
|
|
174
|
+
.filter((m) => TERMINAL_TYPES.has(m.type))
|
|
129
175
|
.flatMap((m) => {
|
|
176
|
+
if (m.resolves)
|
|
177
|
+
return [m.resolves];
|
|
178
|
+
// Logs written before `resolves` existed encode it in the context.
|
|
130
179
|
const match = m.context?.match(/Resolves (\w+)/);
|
|
131
180
|
return match ? [match[1]] : [];
|
|
132
181
|
}));
|
|
133
|
-
const pending = messages.filter((m) => m.type === "task" && !
|
|
134
|
-
const done = messages.filter((m) => m.type === "task" &&
|
|
182
|
+
const pending = messages.filter((m) => m.type === "task" && !resolvedIds.has(m.id));
|
|
183
|
+
const done = messages.filter((m) => m.type === "task" && resolvedIds.has(m.id));
|
|
135
184
|
return {
|
|
136
185
|
initialized,
|
|
137
186
|
directory: handoffDir(projectRoot),
|
|
138
187
|
messages,
|
|
139
188
|
pending,
|
|
140
189
|
done,
|
|
190
|
+
corrupt,
|
|
141
191
|
};
|
|
142
192
|
}
|
|
143
193
|
// ---------------------------------------------------------------------------
|
|
@@ -150,7 +200,7 @@ export async function readInbox(projectRoot, agent) {
|
|
|
150
200
|
}
|
|
151
201
|
/**
|
|
152
202
|
* Render an agent's inbox as instructions the agent itself can act on. This is
|
|
153
|
-
* what `loadout handoff
|
|
203
|
+
* what `loadout handoff <agent>` prints, and what the generated pickup block
|
|
154
204
|
* tells each agent to run, so the message log is consumed rather than merely
|
|
155
205
|
* written.
|
|
156
206
|
*/
|
|
@@ -166,7 +216,7 @@ export function formatInbox(agent, messages) {
|
|
|
166
216
|
lines.push(` ${m.description}`);
|
|
167
217
|
if (m.context)
|
|
168
218
|
lines.push(` context: ${m.context}`);
|
|
169
|
-
lines.push(` when finished: loadout handoff done ${m.id}`);
|
|
219
|
+
lines.push(` when finished: loadout handoff --done ${m.id}`);
|
|
170
220
|
lines.push("");
|
|
171
221
|
}
|
|
172
222
|
lines.push("Work these in order. Mark each done as you complete it so the sender sees progress.");
|
|
@@ -184,10 +234,10 @@ export function pickupBlock(agent) {
|
|
|
184
234
|
`At the start of a session, and whenever you finish a task, run:`,
|
|
185
235
|
"",
|
|
186
236
|
"```bash",
|
|
187
|
-
`loadout handoff
|
|
237
|
+
`loadout handoff ${agent}`,
|
|
188
238
|
"```",
|
|
189
239
|
"",
|
|
190
|
-
"If it lists pending tasks, work them in order and run the `loadout handoff done`",
|
|
240
|
+
"If it lists pending tasks, work them in order and run the `loadout handoff --done`",
|
|
191
241
|
"command it prints for each one. If it reports none, continue as normal.",
|
|
192
242
|
"",
|
|
193
243
|
PICKUP_END,
|
|
@@ -198,6 +248,10 @@ const AGENT_CONTEXT_FILES = {
|
|
|
198
248
|
"claude-code": "CLAUDE.md",
|
|
199
249
|
codex: "AGENTS.md",
|
|
200
250
|
};
|
|
251
|
+
/** True when this agent has a context file Loadout knows how to write. */
|
|
252
|
+
export function isPickupTarget(agent) {
|
|
253
|
+
return agent in AGENT_CONTEXT_FILES;
|
|
254
|
+
}
|
|
201
255
|
export function agentContextFile(agent) {
|
|
202
256
|
const file = AGENT_CONTEXT_FILES[agent];
|
|
203
257
|
if (!file)
|
|
@@ -225,12 +279,16 @@ export async function planPickup(projectRoot, agent) {
|
|
|
225
279
|
const start = existing.indexOf(PICKUP_START);
|
|
226
280
|
const end = existing.indexOf(PICKUP_END);
|
|
227
281
|
const replacing = start !== -1 && end !== -1 && end > start;
|
|
282
|
+
// A block written by an older release still tells the agent to run commands
|
|
283
|
+
// that no longer exist, so a rewrite is a migration, not a no-op.
|
|
284
|
+
const stale = replacing &&
|
|
285
|
+
/loadout handoff (?:inbox|send|init|status|done)\b/.test(existing.slice(start, end));
|
|
228
286
|
const content = replacing
|
|
229
287
|
? existing.slice(0, start) + block + existing.slice(end + PICKUP_END.length)
|
|
230
288
|
: exists
|
|
231
289
|
? `${existing.replace(/\s*$/, "")}\n\n${block}\n`
|
|
232
290
|
: `${block}\n`;
|
|
233
|
-
return { path, agent, exists, replacing, content };
|
|
291
|
+
return { path, agent, exists, replacing, stale, content };
|
|
234
292
|
}
|
|
235
293
|
export async function applyPickup(plan) {
|
|
236
294
|
await writeFileAtomically(plan.path, plan.content);
|
|
@@ -238,11 +296,13 @@ export async function applyPickup(plan) {
|
|
|
238
296
|
export function formatPickupPlan(plans) {
|
|
239
297
|
const lines = ["Handoff pickup instructions:", ""];
|
|
240
298
|
for (const plan of plans) {
|
|
241
|
-
const action = plan.
|
|
242
|
-
? "
|
|
243
|
-
: plan.
|
|
244
|
-
? "
|
|
245
|
-
:
|
|
299
|
+
const action = plan.stale
|
|
300
|
+
? "migrate outdated block in"
|
|
301
|
+
: plan.replacing
|
|
302
|
+
? "refresh managed block in"
|
|
303
|
+
: plan.exists
|
|
304
|
+
? "append managed block to"
|
|
305
|
+
: "create";
|
|
246
306
|
lines.push(` ${action} ${plan.path}`);
|
|
247
307
|
}
|
|
248
308
|
lines.push("", "This teaches each agent to check its handoff inbox at session start.", "Only the block between the loadout:handoff markers is managed; the rest of", "each file is preserved.");
|
|
@@ -250,9 +310,9 @@ export function formatPickupPlan(plans) {
|
|
|
250
310
|
}
|
|
251
311
|
export function formatHandoffStatus(state) {
|
|
252
312
|
if (!state.initialized)
|
|
253
|
-
return "
|
|
313
|
+
return "No handoff log here yet. Send a task and it creates itself: loadout handoff <agent> '<task>'";
|
|
254
314
|
if (state.messages.length === 0)
|
|
255
|
-
return "No handoff messages yet. Send one with `loadout handoff
|
|
315
|
+
return "No handoff messages yet. Send one with `loadout handoff <agent> '<task>'`.";
|
|
256
316
|
const lines = [];
|
|
257
317
|
if (state.pending.length) {
|
|
258
318
|
lines.push(`Pending (${state.pending.length}):`);
|
|
@@ -268,6 +328,13 @@ export function formatHandoffStatus(state) {
|
|
|
268
328
|
lines.push(` ${m.id} ${m.from} → ${m.to} ${m.description}`);
|
|
269
329
|
}
|
|
270
330
|
}
|
|
331
|
+
if (state.corrupt.length) {
|
|
332
|
+
if (lines.length)
|
|
333
|
+
lines.push("");
|
|
334
|
+
lines.push(`Warning: ${state.corrupt.length} unreadable line(s) in the log — ${state.corrupt
|
|
335
|
+
.map((entry) => `line ${entry.line}`)
|
|
336
|
+
.join(", ")}.`, "The other messages are shown; repair or delete those lines to clear this.");
|
|
337
|
+
}
|
|
271
338
|
const other = state.messages.filter((m) => m.type !== "task" && m.type !== "done");
|
|
272
339
|
if (other.length) {
|
|
273
340
|
if (lines.length)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
export function installabilityFor(components) {
|
|
3
|
+
if (components.some((component) => ["skill", "rule", "command", "agent"].includes(component)))
|
|
4
|
+
return "portable-components";
|
|
5
|
+
if (components.some((component) => component === "mcp" || component === "plugin"))
|
|
6
|
+
return "explicit-runtime-setup";
|
|
7
|
+
return "unsupported-source-shape";
|
|
8
|
+
}
|
|
9
|
+
export function componentsFor(inspection) {
|
|
10
|
+
const result = [];
|
|
11
|
+
if (inspection.counts.skills)
|
|
12
|
+
result.push("skill");
|
|
13
|
+
if (inspection.counts.rules)
|
|
14
|
+
result.push("rule");
|
|
15
|
+
if (inspection.counts.commands)
|
|
16
|
+
result.push("command");
|
|
17
|
+
if (inspection.counts.agents)
|
|
18
|
+
result.push("agent");
|
|
19
|
+
if (inspection.plugins.some((plugin) => !plugin.warnings.some((warning) => warning.startsWith("invalid plugin manifest"))))
|
|
20
|
+
result.push("plugin");
|
|
21
|
+
if (inspection.mcpServers.length)
|
|
22
|
+
result.push("mcp");
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
export function evidencePathsFor(inspection) {
|
|
26
|
+
return [
|
|
27
|
+
...inspection.skills.map((item) => join(item.path, "SKILL.md")),
|
|
28
|
+
...inspection.resources.map((item) => item.path),
|
|
29
|
+
...inspection.plugins
|
|
30
|
+
.filter((item) => !item.warnings.some((warning) => warning.startsWith("invalid plugin manifest")))
|
|
31
|
+
.map((item) => item.path),
|
|
32
|
+
...inspection.mcpServers.map((item) => item.path),
|
|
33
|
+
]
|
|
34
|
+
.map((path) => path.split(/[\\/]/).join("/"))
|
|
35
|
+
.filter((path) => path !== "." && !path.startsWith("../"))
|
|
36
|
+
.filter((path, index, paths) => paths.indexOf(path) === index)
|
|
37
|
+
.sort();
|
|
38
|
+
}
|
|
39
|
+
const STOP_WORDS = new Set([
|
|
40
|
+
"the",
|
|
41
|
+
"and",
|
|
42
|
+
"for",
|
|
43
|
+
"with",
|
|
44
|
+
"from",
|
|
45
|
+
"that",
|
|
46
|
+
"this",
|
|
47
|
+
"your",
|
|
48
|
+
"into",
|
|
49
|
+
"agent",
|
|
50
|
+
"agents",
|
|
51
|
+
"skill",
|
|
52
|
+
"skills",
|
|
53
|
+
"mcp",
|
|
54
|
+
"server",
|
|
55
|
+
"servers",
|
|
56
|
+
"workflow",
|
|
57
|
+
"workflows",
|
|
58
|
+
"code",
|
|
59
|
+
"coding",
|
|
60
|
+
"developer",
|
|
61
|
+
"development",
|
|
62
|
+
"platform",
|
|
63
|
+
"context",
|
|
64
|
+
"open",
|
|
65
|
+
"source",
|
|
66
|
+
"using",
|
|
67
|
+
"across",
|
|
68
|
+
"tool",
|
|
69
|
+
"tools",
|
|
70
|
+
"github",
|
|
71
|
+
]);
|
|
72
|
+
function tokens(...values) {
|
|
73
|
+
return new Set(values
|
|
74
|
+
.join(" ")
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.split(/[^a-z0-9]+/)
|
|
77
|
+
.filter((word) => word.length >= 3 && !STOP_WORDS.has(word)));
|
|
78
|
+
}
|
|
79
|
+
export function overlapFor(discovery, inspection, catalog) {
|
|
80
|
+
const candidateTokens = tokens(discovery.repository, discovery.description, discovery.topics.join(" "), ...inspection.skills.flatMap((item) => [item.name, item.description]), ...inspection.resources.map((item) => item.name), ...inspection.plugins.flatMap((item) => [item.name, item.description]), ...inspection.mcpServers.map((item) => item.name));
|
|
81
|
+
const candidateComponents = new Set(componentsFor(inspection));
|
|
82
|
+
return catalog
|
|
83
|
+
.map((pkg) => {
|
|
84
|
+
const packageTokens = tokens(pkg.id, pkg.displayName, pkg.description, pkg.category, pkg.topics?.join(" "));
|
|
85
|
+
const common = [...candidateTokens]
|
|
86
|
+
.filter((word) => packageTokens.has(word))
|
|
87
|
+
.sort();
|
|
88
|
+
// Containment is more useful than union similarity here: a large skill
|
|
89
|
+
// collection can overlap a focused package even when most collection
|
|
90
|
+
// tokens describe unrelated capabilities.
|
|
91
|
+
const smallerVocabulary = Math.min(candidateTokens.size, packageTokens.size);
|
|
92
|
+
const score = smallerVocabulary ? common.length / smallerVocabulary : 0;
|
|
93
|
+
const componentMatch = pkg.components?.some((component) => candidateComponents.has(component)) ?? false;
|
|
94
|
+
return {
|
|
95
|
+
packageId: pkg.id,
|
|
96
|
+
repository: pkg.repository,
|
|
97
|
+
score: Math.round(score * 1000) / 1000,
|
|
98
|
+
relationship: componentMatch && common.length >= 2 && score >= 0.2
|
|
99
|
+
? "possible-overlap"
|
|
100
|
+
: "same-tooling-area",
|
|
101
|
+
evidence: common.slice(0, 8).map((word) => `shared term: ${word}`),
|
|
102
|
+
};
|
|
103
|
+
})
|
|
104
|
+
.filter((item) => item.evidence.length >= 1 && item.score >= 0.1)
|
|
105
|
+
.sort((left, right) => right.score - left.score ||
|
|
106
|
+
left.packageId.localeCompare(right.packageId))
|
|
107
|
+
.slice(0, 5);
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { componentsFor, evidencePathsFor, installabilityFor, } from "./candidate-intelligence-evidence.js";
|
|
2
|
+
export function isTextArray(value) {
|
|
3
|
+
return (Array.isArray(value) && value.every((item) => typeof item === "string"));
|
|
4
|
+
}
|
|
5
|
+
export function finiteNonnegative(value) {
|
|
6
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
7
|
+
}
|
|
8
|
+
export function validDate(value) {
|
|
9
|
+
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
10
|
+
}
|
|
11
|
+
export function validPersistedEvaluation(value) {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
13
|
+
return false;
|
|
14
|
+
const evaluation = value;
|
|
15
|
+
const categories = evaluation.categories;
|
|
16
|
+
return (evaluation.evaluatorVersion === 1 &&
|
|
17
|
+
typeof evaluation.uncertainty === "string" &&
|
|
18
|
+
Array.isArray(categories) &&
|
|
19
|
+
categories.length === 2 &&
|
|
20
|
+
new Set(categories.map((item) => item.category)).size === 2 &&
|
|
21
|
+
categories.every((item) => (item.category === "skills" || item.category === "mcp") &&
|
|
22
|
+
["ready", "needs-review", "blocked", "not-applicable"].includes(item.status) &&
|
|
23
|
+
isTextArray(item.findings)));
|
|
24
|
+
}
|
|
25
|
+
function safeEvidencePath(value) {
|
|
26
|
+
return (typeof value === "string" &&
|
|
27
|
+
Boolean(value) &&
|
|
28
|
+
!value.startsWith("/") &&
|
|
29
|
+
!value.split(/[\\/]/).includes(".."));
|
|
30
|
+
}
|
|
31
|
+
export function validPersistedInspection(value) {
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
33
|
+
return false;
|
|
34
|
+
const inspection = value;
|
|
35
|
+
const validCount = (count) => typeof count === "number" && Number.isSafeInteger(count) && count >= 0;
|
|
36
|
+
const validNamedPath = (item, type) => Boolean(item &&
|
|
37
|
+
typeof item === "object" &&
|
|
38
|
+
!Array.isArray(item) &&
|
|
39
|
+
item.type === type &&
|
|
40
|
+
typeof item.name === "string" &&
|
|
41
|
+
safeEvidencePath(item.path));
|
|
42
|
+
if (!Array.isArray(inspection.skills) ||
|
|
43
|
+
!inspection.skills.every((item) => validNamedPath(item, "skill") &&
|
|
44
|
+
(item.description === undefined ||
|
|
45
|
+
typeof item.description === "string")) ||
|
|
46
|
+
!Array.isArray(inspection.resources) ||
|
|
47
|
+
!inspection.resources.every((item) => ["rule", "command", "agent"].includes(item.type) &&
|
|
48
|
+
validNamedPath(item, item.type)) ||
|
|
49
|
+
!Array.isArray(inspection.plugins) ||
|
|
50
|
+
!inspection.plugins.every((item) => validNamedPath(item, "plugin") &&
|
|
51
|
+
(item.description === undefined ||
|
|
52
|
+
typeof item.description === "string") &&
|
|
53
|
+
(item.version === undefined || typeof item.version === "string") &&
|
|
54
|
+
(item.author === undefined || typeof item.author === "string") &&
|
|
55
|
+
Array.isArray(item.components) &&
|
|
56
|
+
item.components.every((component) => [
|
|
57
|
+
"skill",
|
|
58
|
+
"rule",
|
|
59
|
+
"command",
|
|
60
|
+
"agent",
|
|
61
|
+
"mcp",
|
|
62
|
+
"plugin",
|
|
63
|
+
"root",
|
|
64
|
+
].includes(component)) &&
|
|
65
|
+
isTextArray(item.hookEvents) &&
|
|
66
|
+
isTextArray(item.mcpServers) &&
|
|
67
|
+
isTextArray(item.warnings)) ||
|
|
68
|
+
!Array.isArray(inspection.mcpServers) ||
|
|
69
|
+
!inspection.mcpServers.every((item) => validNamedPath(item, "mcp") &&
|
|
70
|
+
["command", "url", "unknown"].includes(item.transport) &&
|
|
71
|
+
(item.command === undefined || typeof item.command === "string") &&
|
|
72
|
+
(item.url === undefined || typeof item.url === "string") &&
|
|
73
|
+
validCount(item.argumentCount) &&
|
|
74
|
+
validCount(item.environmentVariableCount) &&
|
|
75
|
+
isTextArray(item.warnings)) ||
|
|
76
|
+
!inspection.counts ||
|
|
77
|
+
!validCount(inspection.counts.skills) ||
|
|
78
|
+
!validCount(inspection.counts.rules) ||
|
|
79
|
+
!validCount(inspection.counts.commands) ||
|
|
80
|
+
!validCount(inspection.counts.agents) ||
|
|
81
|
+
!validCount(inspection.counts.plugins) ||
|
|
82
|
+
!validCount(inspection.counts.mcpServers) ||
|
|
83
|
+
!validCount(inspection.counts.manifests) ||
|
|
84
|
+
!isTextArray(inspection.warnings))
|
|
85
|
+
return false;
|
|
86
|
+
return (inspection.counts.skills === inspection.skills.length &&
|
|
87
|
+
inspection.counts.rules ===
|
|
88
|
+
inspection.resources.filter((item) => item.type === "rule").length &&
|
|
89
|
+
inspection.counts.commands ===
|
|
90
|
+
inspection.resources.filter((item) => item.type === "command").length &&
|
|
91
|
+
inspection.counts.agents ===
|
|
92
|
+
inspection.resources.filter((item) => item.type === "agent").length &&
|
|
93
|
+
inspection.counts.plugins === inspection.plugins.length &&
|
|
94
|
+
inspection.counts.mcpServers === inspection.mcpServers.length);
|
|
95
|
+
}
|
|
96
|
+
export function assertDossierEvidence(dossier) {
|
|
97
|
+
if (!validPersistedInspection(dossier.inspection))
|
|
98
|
+
throw new Error("Candidate dossier inspection evidence is invalid");
|
|
99
|
+
const inspection = { root: ".", ...dossier.inspection };
|
|
100
|
+
const components = componentsFor(inspection);
|
|
101
|
+
const evidencePaths = evidencePathsFor(inspection);
|
|
102
|
+
if (JSON.stringify(dossier.components) !== JSON.stringify(components))
|
|
103
|
+
throw new Error("Candidate dossier components do not match inspection evidence");
|
|
104
|
+
if (dossier.installability !== installabilityFor(components))
|
|
105
|
+
throw new Error("Candidate dossier installability does not match inspection evidence");
|
|
106
|
+
if (JSON.stringify(dossier.evidencePaths) !== JSON.stringify(evidencePaths))
|
|
107
|
+
throw new Error("Candidate dossier evidencePaths do not match inspection evidence");
|
|
108
|
+
}
|