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
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { emit, readAfterCursor, readCoordLog, snapshot, claimOwnership, releaseOwnership, OwnershipConflictError, publishContract, getContracts, getOwnership, getAckState, formatSnapshot, formatConflicts, } from "../core/coordination/coordinator.js";
|
|
2
|
+
import { watchCoordination, formatLiveEvent, } from "../core/coordination/watcher.js";
|
|
3
|
+
import { compact, logSize } from "../core/coordination/retention.js";
|
|
4
|
+
import { startDaemon } from "../core/coordination/daemon.js";
|
|
5
|
+
import { ensureDaemonToken } from "../core/coordination/auth.js";
|
|
6
|
+
import { getDaemonStatus, stopDaemon, activateKillSwitch, deactivateKillSwitch, isKillSwitchActive, } from "../core/coordination/crash-recovery.js";
|
|
7
|
+
import { previewConflicts, formatConflictPreview, } from "../core/coordination/conflict-preview.js";
|
|
8
|
+
import { getContractHistory, diffLatestContract, diffContracts, formatContractDelta, } from "../core/coordination/contract-diff.js";
|
|
9
|
+
import { buildReplay, formatReplay } from "../core/coordination/replay.js";
|
|
10
|
+
import { registerCoordinationSessions } from "./coordination-sessions.js";
|
|
11
|
+
import { registerCoordinationDiscussions } from "./coordination-discussions.js";
|
|
12
|
+
export function registerCoordinate(program) {
|
|
13
|
+
const coord = program
|
|
14
|
+
.command("coordinate")
|
|
15
|
+
.alias("coord")
|
|
16
|
+
.description("Live coordination between agents — contracts, ownership, updates, and snapshots");
|
|
17
|
+
registerCoordinationSessions(coord);
|
|
18
|
+
registerCoordinationDiscussions(coord);
|
|
19
|
+
coord
|
|
20
|
+
.command("snapshot")
|
|
21
|
+
.description("Current coordination state for an agent")
|
|
22
|
+
.argument("<agent>", "agent requesting the snapshot")
|
|
23
|
+
.option("--json", "machine-readable JSON output")
|
|
24
|
+
.action(async (agent, options) => {
|
|
25
|
+
const snap = await snapshot(process.cwd(), agent);
|
|
26
|
+
if (options.json) {
|
|
27
|
+
console.log(JSON.stringify(snap, null, 2));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log(formatSnapshot(snap));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
coord
|
|
34
|
+
.command("contract")
|
|
35
|
+
.description("Publish or list API contracts")
|
|
36
|
+
.argument("[name]", "contract name to publish or inspect")
|
|
37
|
+
.option("--agent <agent>", "publishing agent", "user")
|
|
38
|
+
.option("--revision <n>", "revision number", parseInt)
|
|
39
|
+
.option("--body <text>", "contract body")
|
|
40
|
+
.option("--format <fmt>", "format hint: typescript, openapi-yaml, sql")
|
|
41
|
+
.option("--json", "machine-readable JSON output")
|
|
42
|
+
.action(async (name, options) => {
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
// No name: list active contracts
|
|
45
|
+
if (!name) {
|
|
46
|
+
const contracts = await getContracts(cwd);
|
|
47
|
+
if (options.json) {
|
|
48
|
+
console.log(JSON.stringify([...contracts.values()], null, 2));
|
|
49
|
+
}
|
|
50
|
+
else if (contracts.size === 0) {
|
|
51
|
+
console.log("No active contracts.");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(`Active contracts (${contracts.size}):`);
|
|
55
|
+
for (const c of contracts.values()) {
|
|
56
|
+
console.log(` ${c.name} rev${c.revision} by ${c.publisher}${c.format ? ` (${c.format})` : ""}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Name without body: inspect
|
|
62
|
+
if (!options.body) {
|
|
63
|
+
const contracts = await getContracts(cwd);
|
|
64
|
+
const contract = contracts.get(name);
|
|
65
|
+
if (!contract) {
|
|
66
|
+
console.log(`No contract named '${name}'.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (options.json) {
|
|
70
|
+
console.log(JSON.stringify(contract, null, 2));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(`${contract.name} rev${contract.revision} by ${contract.publisher}`);
|
|
74
|
+
if (contract.format)
|
|
75
|
+
console.log(`Format: ${contract.format}`);
|
|
76
|
+
console.log("---");
|
|
77
|
+
console.log(contract.body);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const event = await publishContract(cwd, {
|
|
82
|
+
from: options.agent,
|
|
83
|
+
name,
|
|
84
|
+
body: options.body,
|
|
85
|
+
revision: options.revision,
|
|
86
|
+
format: options.format,
|
|
87
|
+
});
|
|
88
|
+
const revision = event.payload.revision;
|
|
89
|
+
console.log(`Published '${name}' rev${revision} (seq ${event.seq})`);
|
|
90
|
+
});
|
|
91
|
+
coord
|
|
92
|
+
.command("own")
|
|
93
|
+
.description("Claim file/directory ownership")
|
|
94
|
+
.argument("<agent>", "claiming agent")
|
|
95
|
+
.argument("<paths...>", "paths to claim")
|
|
96
|
+
.option("--shared", "allow shared access (default is exclusive)")
|
|
97
|
+
.option("--json", "machine-readable JSON output")
|
|
98
|
+
.action(async (agent, paths, options) => {
|
|
99
|
+
const cwd = process.cwd();
|
|
100
|
+
const mode = options.shared ? "shared" : "exclusive";
|
|
101
|
+
let event;
|
|
102
|
+
try {
|
|
103
|
+
event = await claimOwnership(cwd, { agent, paths, mode });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof OwnershipConflictError) {
|
|
107
|
+
console.error(formatConflicts(error.conflicts));
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
if (options.json) {
|
|
114
|
+
console.log(JSON.stringify(event, null, 2));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.log(`${agent} now owns (${mode}): ${paths.join(", ")} (seq ${event.seq})`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
coord
|
|
121
|
+
.command("release")
|
|
122
|
+
.description("Release exact file/directory ownership paths")
|
|
123
|
+
.argument("<agent>", "releasing agent")
|
|
124
|
+
.argument("<paths...>", "exact paths to release")
|
|
125
|
+
.option("--json", "machine-readable JSON output")
|
|
126
|
+
.action(async (agent, paths, options) => {
|
|
127
|
+
const event = await releaseOwnership(process.cwd(), { agent, paths });
|
|
128
|
+
if (options.json) {
|
|
129
|
+
console.log(JSON.stringify(event, null, 2));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.log(`${agent} released: ${paths.join(", ")} (seq ${event.seq})`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
coord
|
|
136
|
+
.command("decide")
|
|
137
|
+
.description("Record a design decision")
|
|
138
|
+
.argument("<agent>", "deciding agent")
|
|
139
|
+
.argument("<title>", "decision title")
|
|
140
|
+
.option("--rationale <text>", "why this decision was made", "")
|
|
141
|
+
.option("--supersedes <id>", "ID of the decision this replaces")
|
|
142
|
+
.option("--json", "machine-readable JSON output")
|
|
143
|
+
.action(async (agent, title, options) => {
|
|
144
|
+
const event = await emit(process.cwd(), {
|
|
145
|
+
from: agent,
|
|
146
|
+
to: "*",
|
|
147
|
+
type: "decision",
|
|
148
|
+
description: title,
|
|
149
|
+
payload: {
|
|
150
|
+
title,
|
|
151
|
+
rationale: options.rationale,
|
|
152
|
+
...(options.supersedes ? { supersedes: options.supersedes } : {}),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
if (options.json) {
|
|
156
|
+
console.log(JSON.stringify(event, null, 2));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log(`Decision recorded: ${title} (seq ${event.seq})`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
coord
|
|
163
|
+
.command("update")
|
|
164
|
+
.description("Publish a progress update")
|
|
165
|
+
.argument("<agent>", "reporting agent")
|
|
166
|
+
.option("--note <text>", "progress note")
|
|
167
|
+
.option("--files <paths...>", "files modified")
|
|
168
|
+
.option("--blockers <items...>", "blockers")
|
|
169
|
+
.option("--next <text>", "what to do next")
|
|
170
|
+
.option("--json", "machine-readable JSON output")
|
|
171
|
+
.action(async (agent, options) => {
|
|
172
|
+
const payload = {};
|
|
173
|
+
if (options.note)
|
|
174
|
+
payload.note = options.note;
|
|
175
|
+
if (options.files)
|
|
176
|
+
payload.files = options.files;
|
|
177
|
+
if (options.blockers)
|
|
178
|
+
payload.blockers = options.blockers;
|
|
179
|
+
if (options.next)
|
|
180
|
+
payload.next = options.next;
|
|
181
|
+
const event = await emit(process.cwd(), {
|
|
182
|
+
from: agent,
|
|
183
|
+
to: "*",
|
|
184
|
+
type: "update",
|
|
185
|
+
description: options.note ?? "Progress update",
|
|
186
|
+
payload,
|
|
187
|
+
});
|
|
188
|
+
if (options.json) {
|
|
189
|
+
console.log(JSON.stringify(event, null, 2));
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(`Update published (seq ${event.seq})`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
coord
|
|
196
|
+
.command("subscribe")
|
|
197
|
+
.description("Read events after a cursor")
|
|
198
|
+
.argument("<agent>", "subscribing agent")
|
|
199
|
+
.option("--cursor <n>", "sequence number to read after", parseInt, -1)
|
|
200
|
+
.option("--json", "machine-readable JSON output")
|
|
201
|
+
.action(async (agent, options) => {
|
|
202
|
+
const { events, highSeq } = await readAfterCursor(process.cwd(), options.cursor);
|
|
203
|
+
const relevant = events.filter((e) => e.to === agent || e.to === "*" || e.from === agent);
|
|
204
|
+
if (options.json) {
|
|
205
|
+
console.log(JSON.stringify({ events: relevant, highSeq }, null, 2));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
if (!relevant.length) {
|
|
209
|
+
console.log(`No events after seq ${options.cursor} for ${agent}.`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
for (const e of relevant) {
|
|
213
|
+
console.log(` seq=${e.seq} [${e.type}] ${e.from}→${e.to}: ${e.description}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
console.log(`High watermark: ${highSeq}`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
coord
|
|
220
|
+
.command("ack")
|
|
221
|
+
.description("Acknowledge events through a sequence number")
|
|
222
|
+
.argument("<agent>", "acknowledging agent")
|
|
223
|
+
.argument("<seq>", "sequence number to acknowledge through", parseInt)
|
|
224
|
+
.option("--note <text>", "what the agent did with these events")
|
|
225
|
+
.action(async (agent, seq, options) => {
|
|
226
|
+
const event = await emit(process.cwd(), {
|
|
227
|
+
from: agent,
|
|
228
|
+
to: "*",
|
|
229
|
+
type: "ack",
|
|
230
|
+
description: `Acknowledged through seq ${seq}`,
|
|
231
|
+
payload: {
|
|
232
|
+
eventSeq: seq,
|
|
233
|
+
...(options.note ? { note: options.note } : {}),
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
console.log(`${agent} acknowledged through seq ${seq} (ack seq ${event.seq})`);
|
|
237
|
+
});
|
|
238
|
+
coord
|
|
239
|
+
.command("watch")
|
|
240
|
+
.description("Watch for coordination events in real time — live terminal feed")
|
|
241
|
+
.argument("[agent]", "filter events for this agent")
|
|
242
|
+
.option("--cursor <n>", "start from this sequence number", parseInt)
|
|
243
|
+
.action(async (agent, options) => {
|
|
244
|
+
const cwd = process.cwd();
|
|
245
|
+
console.log(`\x1b[1mWatching coordination events${agent ? ` for ${agent}` : ""}...\x1b[0m`);
|
|
246
|
+
console.log("Press Ctrl+C to stop.\n");
|
|
247
|
+
// Show recent events first
|
|
248
|
+
const log = await readCoordLog(cwd);
|
|
249
|
+
const startCursor = options.cursor ?? Math.max(log.highSeq - 10, -1);
|
|
250
|
+
const recent = log.events.filter((e) => e.seq > startCursor);
|
|
251
|
+
const filtered = agent
|
|
252
|
+
? recent.filter((e) => e.to === agent || e.to === "*" || e.from === agent)
|
|
253
|
+
: recent;
|
|
254
|
+
if (filtered.length) {
|
|
255
|
+
console.log("\x1b[90m── recent ──\x1b[0m");
|
|
256
|
+
for (const e of filtered) {
|
|
257
|
+
console.log(formatLiveEvent(e));
|
|
258
|
+
}
|
|
259
|
+
console.log("\x1b[90m── live ──\x1b[0m\n");
|
|
260
|
+
}
|
|
261
|
+
const watcher = await watchCoordination(cwd, {
|
|
262
|
+
agent,
|
|
263
|
+
cursor: log.highSeq,
|
|
264
|
+
onEvents(events) {
|
|
265
|
+
for (const e of events) {
|
|
266
|
+
console.log(formatLiveEvent(e));
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
onError(error) {
|
|
270
|
+
console.error(`Watch error: ${error.message}`);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
// Keep alive until Ctrl+C
|
|
274
|
+
process.on("SIGINT", () => {
|
|
275
|
+
watcher.stop();
|
|
276
|
+
console.log("\nStopped watching.");
|
|
277
|
+
process.exit(0);
|
|
278
|
+
});
|
|
279
|
+
// Prevent Node from exiting
|
|
280
|
+
await new Promise(() => { });
|
|
281
|
+
});
|
|
282
|
+
coord
|
|
283
|
+
.command("status")
|
|
284
|
+
.description("Show live coordination status — ownership, contracts, acks")
|
|
285
|
+
.option("--json", "machine-readable JSON output")
|
|
286
|
+
.action(async (options) => {
|
|
287
|
+
const cwd = process.cwd();
|
|
288
|
+
const log = await readCoordLog(cwd);
|
|
289
|
+
const contracts = await getContracts(cwd);
|
|
290
|
+
const ownership = await getOwnership(cwd);
|
|
291
|
+
const ackState = await getAckState(cwd);
|
|
292
|
+
if (options.json) {
|
|
293
|
+
console.log(JSON.stringify({
|
|
294
|
+
events: log.events.length,
|
|
295
|
+
highSeq: log.highSeq,
|
|
296
|
+
corrupt: log.corrupt.length,
|
|
297
|
+
contracts: [...contracts.values()],
|
|
298
|
+
ownership: [...ownership.values()],
|
|
299
|
+
ackCursors: Object.fromEntries(ackState.cursors),
|
|
300
|
+
unacked: ackState.unacked.length,
|
|
301
|
+
}, null, 2));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (log.events.length === 0) {
|
|
305
|
+
console.log("No coordination events yet.");
|
|
306
|
+
console.log("Start with: loadout coord own <agent> <paths...>");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log(`\x1b[1mCoordination status\x1b[0m (${log.events.length} events, seq ${log.highSeq})`);
|
|
310
|
+
if (contracts.size) {
|
|
311
|
+
console.log(`\n\x1b[36mContracts (${contracts.size}):\x1b[0m`);
|
|
312
|
+
for (const c of contracts.values()) {
|
|
313
|
+
console.log(` ${c.name} rev${c.revision} by ${c.publisher}${c.format ? ` (${c.format})` : ""}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (ownership.size) {
|
|
317
|
+
console.log(`\n\x1b[33mFile ownership (${ownership.size} paths):\x1b[0m`);
|
|
318
|
+
const byAgent = new Map();
|
|
319
|
+
for (const claim of ownership.values()) {
|
|
320
|
+
const existing = byAgent.get(claim.agent);
|
|
321
|
+
if (existing) {
|
|
322
|
+
existing.paths.push(...claim.paths);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
byAgent.set(claim.agent, {
|
|
326
|
+
paths: [...claim.paths],
|
|
327
|
+
mode: claim.mode,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const [agent, info] of byAgent) {
|
|
332
|
+
console.log(` ${agent} (${info.mode}): ${info.paths.join(", ")}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (ackState.cursors.size) {
|
|
336
|
+
console.log(`\n\x1b[32mAck cursors:\x1b[0m`);
|
|
337
|
+
for (const [agent, cursor] of ackState.cursors) {
|
|
338
|
+
const behind = log.highSeq - cursor;
|
|
339
|
+
console.log(` ${agent}: seq ${cursor}${behind > 0 ? ` (${behind} behind)` : " (up to date)"}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (ackState.unacked.length) {
|
|
343
|
+
console.log(`\n\x1b[31m${ackState.unacked.length} unacknowledged event(s)\x1b[0m`);
|
|
344
|
+
}
|
|
345
|
+
if (log.corrupt.length) {
|
|
346
|
+
console.log(`\n\x1b[31mWarning: ${log.corrupt.length} corrupt line(s)\x1b[0m`);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
coord
|
|
350
|
+
.command("conflicts")
|
|
351
|
+
.alias("preview")
|
|
352
|
+
.description("Preview file conflicts before writing — shows what other agents changed")
|
|
353
|
+
.argument("<agent>", "agent about to write")
|
|
354
|
+
.argument("<paths...>", "file paths the agent intends to write")
|
|
355
|
+
.option("--json", "machine-readable JSON output")
|
|
356
|
+
.action(async (agent, paths, options) => {
|
|
357
|
+
const preview = await previewConflicts(process.cwd(), agent, paths);
|
|
358
|
+
if (options.json) {
|
|
359
|
+
console.log(JSON.stringify(preview, null, 2));
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
console.log(formatConflictPreview(preview));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
coord
|
|
366
|
+
.command("diff")
|
|
367
|
+
.description("Diff contract revisions — shows exactly what changed between versions")
|
|
368
|
+
.argument("<name>", "contract name to diff")
|
|
369
|
+
.option("--from <n>", "from revision (defaults to previous)", parseInt)
|
|
370
|
+
.option("--to <n>", "to revision (defaults to latest)", parseInt)
|
|
371
|
+
.option("--history", "show full revision history")
|
|
372
|
+
.option("--json", "machine-readable JSON output")
|
|
373
|
+
.action(async (name, options) => {
|
|
374
|
+
const cwd = process.cwd();
|
|
375
|
+
if (options.history) {
|
|
376
|
+
const history = await getContractHistory(cwd, name);
|
|
377
|
+
if (history.length === 0) {
|
|
378
|
+
console.log(`No contract named '${name}'.`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (options.json) {
|
|
382
|
+
console.log(JSON.stringify(history, null, 2));
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
console.log(`\x1b[1m${name}\x1b[0m — ${history.length} revision(s):`);
|
|
386
|
+
for (const rev of history) {
|
|
387
|
+
console.log(` rev${rev.revision} by ${rev.publisher} at ${rev.timestamp} (seq ${rev.seq})`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Specific revision range or latest diff
|
|
393
|
+
if (options.from && options.to) {
|
|
394
|
+
const history = await getContractHistory(cwd, name);
|
|
395
|
+
const fromRev = history.find((r) => r.revision === options.from);
|
|
396
|
+
const toRev = history.find((r) => r.revision === options.to);
|
|
397
|
+
if (!fromRev || !toRev) {
|
|
398
|
+
console.log(`Could not find rev${options.from} or rev${options.to} for '${name}'.`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const delta = diffContracts(fromRev, toRev);
|
|
402
|
+
if (options.json) {
|
|
403
|
+
console.log(JSON.stringify(delta, null, 2));
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
console.log(formatContractDelta(delta));
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const delta = await diffLatestContract(cwd, name);
|
|
411
|
+
if (!delta) {
|
|
412
|
+
console.log(`Need at least 2 revisions of '${name}' to diff. Use --history to see revisions.`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (options.json) {
|
|
416
|
+
console.log(JSON.stringify(delta, null, 2));
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
console.log(formatContractDelta(delta));
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
coord
|
|
423
|
+
.command("replay")
|
|
424
|
+
.description("Replay the full coordination timeline as a narrative story")
|
|
425
|
+
.option("--json", "machine-readable JSON output")
|
|
426
|
+
.action(async (options) => {
|
|
427
|
+
const timeline = await buildReplay(process.cwd());
|
|
428
|
+
if (options.json) {
|
|
429
|
+
console.log(JSON.stringify(timeline, null, 2));
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
console.log(formatReplay(timeline));
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
coord
|
|
436
|
+
.command("compact")
|
|
437
|
+
.description("Compact the coordination log — archive old events")
|
|
438
|
+
.option("--max-events <n>", "max events to keep", parseInt, 10000)
|
|
439
|
+
.option("--max-age <days>", "max age in days", parseInt, 30)
|
|
440
|
+
.option("--json", "machine-readable JSON output")
|
|
441
|
+
.action(async (options) => {
|
|
442
|
+
const result = await compact(process.cwd(), {
|
|
443
|
+
maxEvents: options.maxEvents,
|
|
444
|
+
maxAgeDays: options.maxAge,
|
|
445
|
+
});
|
|
446
|
+
if (options.json) {
|
|
447
|
+
console.log(JSON.stringify(result, null, 2));
|
|
448
|
+
}
|
|
449
|
+
else if (!result.compacted) {
|
|
450
|
+
const size = await logSize(process.cwd());
|
|
451
|
+
console.log(`Log has ${size.events} events (${(size.bytes / 1024).toFixed(1)} KB) — under threshold, nothing to compact.`);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
console.log(`Compacted: ${result.before} → ${result.after} events (removed ${result.removed})`);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
// `loadout daemon` — start/stop the coordination daemon
|
|
458
|
+
const daemon = program
|
|
459
|
+
.command("daemon")
|
|
460
|
+
.description("Local coordination daemon with REST API, SSE, and dashboard");
|
|
461
|
+
daemon
|
|
462
|
+
.command("start")
|
|
463
|
+
.description("Start the coordination daemon — HTTP server with live dashboard")
|
|
464
|
+
.option("--port <n>", "port to listen on", parseInt, 4510)
|
|
465
|
+
.action(async (options) => {
|
|
466
|
+
const cwd = process.cwd();
|
|
467
|
+
console.log(`Starting coordination daemon on port ${options.port}...`);
|
|
468
|
+
try {
|
|
469
|
+
const d = await startDaemon(cwd, options.port);
|
|
470
|
+
console.log(`\x1b[32m✓\x1b[0m Daemon running at http://127.0.0.1:${d.port}`);
|
|
471
|
+
console.log(` Dashboard: ${d.dashboardUrl}`);
|
|
472
|
+
console.log(" API/SSE: bearer-authenticated (token stored in .handoff/daemon.token)");
|
|
473
|
+
console.log(`\nPress Ctrl+C to stop.`);
|
|
474
|
+
process.on("SIGINT", () => {
|
|
475
|
+
console.log("\nStopping daemon...");
|
|
476
|
+
d.close();
|
|
477
|
+
process.exit(0);
|
|
478
|
+
});
|
|
479
|
+
// Keep alive
|
|
480
|
+
await new Promise(() => { });
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
console.error(`Failed to start daemon: ${error instanceof Error ? error.message : error}`);
|
|
484
|
+
process.exitCode = 1;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
daemon
|
|
488
|
+
.command("stop")
|
|
489
|
+
.description("Stop a running coordination daemon")
|
|
490
|
+
.action(async () => {
|
|
491
|
+
const result = await stopDaemon(process.cwd());
|
|
492
|
+
if (result.stopped) {
|
|
493
|
+
console.log(`\x1b[32m✓\x1b[0m Daemon stopped (pid ${result.pid})`);
|
|
494
|
+
}
|
|
495
|
+
else if (result.stale) {
|
|
496
|
+
console.log(`Cleaned up stale PID file (pid ${result.pid} was not running)`);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
console.log("No daemon running.");
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
daemon
|
|
503
|
+
.command("status")
|
|
504
|
+
.description("Check if the coordination daemon is running")
|
|
505
|
+
.option("--json", "machine-readable JSON output")
|
|
506
|
+
.action(async (options) => {
|
|
507
|
+
const cwd = process.cwd();
|
|
508
|
+
const status = await getDaemonStatus(cwd);
|
|
509
|
+
const ks = await isKillSwitchActive(cwd);
|
|
510
|
+
if (options.json) {
|
|
511
|
+
console.log(JSON.stringify({ ...status, killSwitch: ks }, null, 2));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (ks.active) {
|
|
515
|
+
console.log(`\x1b[31m⛔ Kill switch active: ${ks.reason}\x1b[0m`);
|
|
516
|
+
console.log(` Activated: ${ks.activatedAt}`);
|
|
517
|
+
console.log(" Resume with: loadout daemon resume");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (status.running && status.info) {
|
|
521
|
+
const token = await ensureDaemonToken(cwd);
|
|
522
|
+
console.log(`\x1b[32m✓\x1b[0m Daemon running`);
|
|
523
|
+
console.log(` PID: ${status.info.pid}`);
|
|
524
|
+
console.log(` Port: ${status.info.port}`);
|
|
525
|
+
console.log(` Started: ${status.info.startedAt}`);
|
|
526
|
+
console.log(` Dashboard: http://127.0.0.1:${status.info.port}/#token=${encodeURIComponent(token)}`);
|
|
527
|
+
}
|
|
528
|
+
else if (status.stale) {
|
|
529
|
+
console.log("Daemon not running (cleaned up stale PID file)");
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
console.log("Daemon not running.");
|
|
533
|
+
console.log("Start with: loadout daemon start");
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
daemon
|
|
537
|
+
.command("kill")
|
|
538
|
+
.description("Emergency kill switch — halt all coordination immediately")
|
|
539
|
+
.argument("<reason>", "why coordination is being halted")
|
|
540
|
+
.action(async (reason) => {
|
|
541
|
+
await activateKillSwitch(process.cwd(), reason);
|
|
542
|
+
console.log("\x1b[31m⛔ Kill switch activated\x1b[0m");
|
|
543
|
+
console.log(` Reason: ${reason}`);
|
|
544
|
+
console.log(" Daemon stopped, all coordination halted.");
|
|
545
|
+
console.log(" Resume with: loadout daemon resume");
|
|
546
|
+
});
|
|
547
|
+
daemon
|
|
548
|
+
.command("resume")
|
|
549
|
+
.description("Deactivate the kill switch and resume coordination")
|
|
550
|
+
.action(async () => {
|
|
551
|
+
const deactivated = await deactivateKillSwitch(process.cwd());
|
|
552
|
+
if (deactivated) {
|
|
553
|
+
console.log("\x1b[32m✓\x1b[0m Kill switch deactivated. Coordination resumed.");
|
|
554
|
+
console.log(" Start daemon with: loadout daemon start");
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
console.log("Kill switch was not active.");
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
// `loadout serve` — start the MCP coordination server
|
|
561
|
+
program
|
|
562
|
+
.command("serve")
|
|
563
|
+
.description("Start the coordination MCP server for live agent collaboration")
|
|
564
|
+
.action(async () => {
|
|
565
|
+
// Dynamic import avoids a hard dependency on @modelcontextprotocol/sdk.
|
|
566
|
+
// The string concatenation prevents tsc from resolving the specifier.
|
|
567
|
+
const modulePath = ["../core/coordination", "mcp-server.js"].join("/");
|
|
568
|
+
try {
|
|
569
|
+
const mod = (await import(modulePath));
|
|
570
|
+
await mod.startMcpServer(process.cwd());
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
if (error instanceof Error &&
|
|
574
|
+
(error.message.includes("Cannot find module") ||
|
|
575
|
+
error.message.includes("ERR_MODULE_NOT_FOUND"))) {
|
|
576
|
+
console.error("The MCP server requires @modelcontextprotocol/sdk.\n" +
|
|
577
|
+
"Install it with: npm install @modelcontextprotocol/sdk\n" +
|
|
578
|
+
"Then run: loadout serve");
|
|
579
|
+
process.exitCode = 1;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|