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.
Files changed (108) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/README.md +160 -315
  3. package/SECURITY.md +21 -1
  4. package/catalog/discovered.json +31269 -26003
  5. package/catalog/packages.json +4 -4
  6. package/dist/src/cli.js +13 -0
  7. package/dist/src/commands/agents.js +3 -1
  8. package/dist/src/commands/catalog-candidate.js +150 -0
  9. package/dist/src/commands/catalog-workflows.js +147 -0
  10. package/dist/src/commands/catalog.js +9 -367
  11. package/dist/src/commands/coordinate.js +586 -0
  12. package/dist/src/commands/coordination-discussions.js +194 -0
  13. package/dist/src/commands/coordination-sessions.js +197 -0
  14. package/dist/src/commands/inventory.js +4 -2
  15. package/dist/src/core/agents/agent-inspection.js +26 -4
  16. package/dist/src/core/{routing → agents}/model-config.js +7 -2
  17. package/dist/src/core/catalog/catalog.js +1 -0
  18. package/dist/src/core/catalog/registry.js +44 -10
  19. package/dist/src/core/catalog/safety.js +36 -7
  20. package/dist/src/core/coordination/adapters/claude-code.js +154 -0
  21. package/dist/src/core/coordination/adapters/codex.js +137 -0
  22. package/dist/src/core/coordination/adapters/types.js +64 -0
  23. package/dist/src/core/coordination/auth.js +59 -0
  24. package/dist/src/core/coordination/bridge-lease.js +79 -0
  25. package/dist/src/core/coordination/conflict-preview.js +167 -0
  26. package/dist/src/core/coordination/contract-diff.js +106 -0
  27. package/dist/src/core/coordination/coordinator.js +552 -0
  28. package/dist/src/core/coordination/crash-recovery.js +169 -0
  29. package/dist/src/core/coordination/daemon.js +667 -0
  30. package/dist/src/core/coordination/discussion.js +340 -0
  31. package/dist/src/core/coordination/events.js +161 -0
  32. package/dist/src/core/coordination/http-api.js +118 -0
  33. package/dist/src/core/coordination/interrupt-policy.js +89 -0
  34. package/dist/src/core/coordination/lock.js +110 -0
  35. package/dist/src/core/coordination/mcp-server.js +424 -0
  36. package/dist/src/core/coordination/redaction.js +85 -0
  37. package/dist/src/core/coordination/replay.js +188 -0
  38. package/dist/src/core/coordination/retention.js +128 -0
  39. package/dist/src/core/coordination/runtime.js +21 -0
  40. package/dist/src/core/coordination/session-manager.js +366 -0
  41. package/dist/src/core/coordination/watcher.js +128 -0
  42. package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
  43. package/dist/src/core/{routing → delegation}/handoff.js +130 -63
  44. package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
  45. package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
  46. package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
  47. package/dist/src/core/discovery/candidate-intelligence.js +3 -214
  48. package/dist/src/core/discovery/community.js +12 -4
  49. package/dist/src/core/discovery/github-discovery.js +6 -2
  50. package/dist/src/core/discovery/private-discovery.js +6 -2
  51. package/dist/src/core/install/reconcile.js +1 -1
  52. package/dist/src/core/install/source.js +21 -7
  53. package/dist/src/core/install/uninstall.js +39 -3
  54. package/dist/src/core/reporting/cli-guide.js +10 -6
  55. package/dist/src/core/reporting/completion.js +61 -109
  56. package/dist/src/core/reporting/doctor.js +4 -6
  57. package/dist/src/core/runtime/bounded-json.js +65 -0
  58. package/dist/src/core/runtime/github.js +30 -16
  59. package/dist/src/core/runtime/mcp-recipes.js +1 -1
  60. package/dist/src/core/workspace/active-policy.js +1 -1
  61. package/docs/CANDIDATE_INTELLIGENCE.md +9 -2
  62. package/docs/CATALOG.md +1 -1
  63. package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +1 -1
  64. package/docs/DEMO_SCRIPT.md +19 -23
  65. package/docs/DISCOVERED.md +251 -250
  66. package/docs/FEATURE_TEST_MATRIX.md +10 -263
  67. package/docs/GITHUB_AUTHORIZATION.md +5 -0
  68. package/docs/LIVE_COLLABORATION.md +234 -0
  69. package/docs/PROVENANCE_AND_COMPARISON.md +1 -1
  70. package/docs/REFERENCE.md +163 -0
  71. package/docs/RELEASE_REVIEW.md +1 -2
  72. package/docs/TESTING.md +17 -1
  73. package/docs/USER_TEST_GUIDE.md +138 -3
  74. package/docs/assets/loadout-discover-activate.webp +0 -0
  75. package/docs/assets/loadout-handoff-coordinate.webp +0 -0
  76. package/docs/assets/loadout-social-preview.png +0 -0
  77. package/docs/decisions/001-coordination-jsonl-locking.md +35 -0
  78. package/docs/decisions/002-local-daemon-authentication.md +32 -0
  79. package/docs/decisions/003-bounded-agent-discussions.md +91 -0
  80. package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
  81. package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
  82. package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
  83. package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -0
  84. package/package.json +18 -7
  85. package/skills/loadout-handoff/SKILL.md +238 -0
  86. package/MASTER_PLAN.md +0 -2207
  87. package/dist/src/core/routing/route.js +0 -539
  88. package/docs/ACTIVE_SET.md +0 -53
  89. package/docs/COMPATIBILITY_POLICY.md +0 -22
  90. package/docs/CONVERSION_AND_SANDBOX.md +0 -27
  91. package/docs/EVALUATION_PROTOCOL_V1.md +0 -300
  92. package/docs/HEAD_TO_HEAD_EVALUATION.md +0 -79
  93. package/docs/PROVIDER_CONFIGURATION.md +0 -45
  94. package/docs/README_RESEARCH.md +0 -36
  95. package/docs/REPOSITORY_STABILIZATION.md +0 -190
  96. package/docs/SAFE_UPDATE_DEMO.md +0 -25
  97. package/docs/SCHEMA_DECISIONS.md +0 -25
  98. package/docs/SUBMISSION_COPY.md +0 -90
  99. package/docs/TEAM_POLICY.md +0 -18
  100. package/docs/assets/loadout-workflow.png +0 -0
  101. package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +0 -283
  102. package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +0 -116
  103. package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +0 -469
  104. package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +0 -80
  105. package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +0 -55
  106. package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +0 -228
  107. package/skills/loadout-router/SKILL.md +0 -120
  108. /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
+ }