loadout-ai 0.8.0 → 0.9.1

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 (98) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +149 -309
  3. package/SECURITY.md +21 -1
  4. package/catalog/discovered.json +29122 -25325
  5. package/catalog/packages.json +4 -4
  6. package/dist/src/cli.js +12 -4
  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 +365 -0
  10. package/dist/src/commands/catalog.js +9 -354
  11. package/dist/src/commands/coordinate.js +730 -0
  12. package/dist/src/commands/coordination-discussions.js +218 -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/{routing → agents}/model-config.js +7 -2
  16. package/dist/src/core/catalog/catalog.js +1 -0
  17. package/dist/src/core/catalog/registry.js +5 -19
  18. package/dist/src/core/coordination/adapters/claude-code.js +154 -0
  19. package/dist/src/core/coordination/adapters/codex.js +160 -0
  20. package/dist/src/core/coordination/adapters/types.js +64 -0
  21. package/dist/src/core/coordination/auth.js +59 -0
  22. package/dist/src/core/coordination/auto-contract.js +457 -0
  23. package/dist/src/core/coordination/bridge-lease.js +79 -0
  24. package/dist/src/core/coordination/conflict-preview.js +167 -0
  25. package/dist/src/core/coordination/contract-diff.js +106 -0
  26. package/dist/src/core/coordination/coordinator.js +552 -0
  27. package/dist/src/core/coordination/crash-recovery.js +169 -0
  28. package/dist/src/core/coordination/daemon.js +670 -0
  29. package/dist/src/core/coordination/discussion-pipeline.js +313 -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/git-ownership.js +217 -0
  33. package/dist/src/core/coordination/http-api.js +118 -0
  34. package/dist/src/core/coordination/interrupt-policy.js +89 -0
  35. package/dist/src/core/coordination/lock.js +140 -0
  36. package/dist/src/core/coordination/mcp-server.js +424 -0
  37. package/dist/src/core/coordination/quick-start.js +200 -0
  38. package/dist/src/core/coordination/redaction.js +85 -0
  39. package/dist/src/core/coordination/replay.js +188 -0
  40. package/dist/src/core/coordination/retention.js +180 -0
  41. package/dist/src/core/coordination/runtime.js +21 -0
  42. package/dist/src/core/coordination/session-manager.js +366 -0
  43. package/dist/src/core/coordination/watcher.js +128 -0
  44. package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
  45. package/dist/src/core/delegation/handoff-bundle.js +253 -0
  46. package/dist/src/core/delegation/handoff-templates.js +222 -0
  47. package/dist/src/core/delegation/handoff-verification.js +117 -0
  48. package/dist/src/core/delegation/handoff.js +537 -0
  49. package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
  50. package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
  51. package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
  52. package/dist/src/core/discovery/candidate-intelligence.js +3 -214
  53. package/dist/src/core/discovery/community.js +12 -4
  54. package/dist/src/core/discovery/github-discovery.js +6 -2
  55. package/dist/src/core/discovery/private-discovery.js +6 -2
  56. package/dist/src/core/install/reconcile.js +1 -1
  57. package/dist/src/core/install/snapshot.js +24 -5
  58. package/dist/src/core/install/uninstall.js +39 -3
  59. package/dist/src/core/reporting/cli-guide.js +9 -5
  60. package/dist/src/core/reporting/completion.js +43 -38
  61. package/dist/src/core/reporting/doctor.js +1 -1
  62. package/dist/src/core/runtime/bounded-json.js +65 -0
  63. package/dist/src/core/runtime/github.js +30 -16
  64. package/dist/src/core/runtime/mcp-recipes.js +1 -1
  65. package/dist/src/core/workspace/active-policy.js +1 -1
  66. package/docs/DEMO_SCRIPT.md +19 -23
  67. package/docs/DISCOVERED.md +250 -251
  68. package/docs/FEATURE_TEST_MATRIX.md +30 -15
  69. package/docs/LIVE_COLLABORATION.md +283 -0
  70. package/docs/REFERENCE.md +263 -0
  71. package/docs/RELEASE_REVIEW.md +1 -1
  72. package/docs/TESTING.md +17 -1
  73. package/docs/USER_TEST_GUIDE.md +211 -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/evidence/coordination-provider-check-2026-09-05.md +33 -0
  81. package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
  82. package/docs/specs/HANDOFF_CONTEXT_BUNDLES.md +139 -0
  83. package/docs/specs/HANDOFF_VERIFICATION.md +83 -0
  84. package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
  85. package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
  86. package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -0
  87. package/docs/superpowers/plans/2026-09-04-handoff-context-bundles.md +109 -0
  88. package/docs/superpowers/plans/2026-09-04-handoff-verification.md +56 -0
  89. package/docs/superpowers/plans/2026-09-05-pre-release-hardening.md +175 -0
  90. package/docs/superpowers/plans/2026-09-05-public-readiness.md +20 -0
  91. package/package.json +15 -5
  92. package/skills/loadout-handoff/SKILL.md +291 -0
  93. package/dist/src/core/routing/handoff.js +0 -314
  94. package/dist/src/core/routing/policy.js +0 -147
  95. package/dist/src/core/routing/route.js +0 -411
  96. package/docs/assets/loadout-workflow.png +0 -0
  97. package/skills/loadout-router/SKILL.md +0 -85
  98. /package/dist/src/core/{routing → agents}/credentials.js +0 -0
@@ -0,0 +1,730 @@
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 { quickStart, formatQuickStart, } from "../core/coordination/quick-start.js";
11
+ import { detectContracts, formatDetectionResult, } from "../core/coordination/auto-contract.js";
12
+ import { applyGitOwnership, formatGitOwnership, } from "../core/coordination/git-ownership.js";
13
+ import { registerCoordinationSessions } from "./coordination-sessions.js";
14
+ import { registerCoordinationDiscussions } from "./coordination-discussions.js";
15
+ export function registerCoordinate(program) {
16
+ const coord = program
17
+ .command("coordinate")
18
+ .alias("coord")
19
+ .description("Live coordination between agents — contracts, ownership, updates, and snapshots");
20
+ registerCoordinationSessions(coord);
21
+ registerCoordinationDiscussions(coord);
22
+ coord
23
+ .command("start")
24
+ .description("Set up coordination in one command — detect project structure, assign ownership, ready to go")
25
+ .requiredOption("--agents <agents>", "two agents, comma-separated (e.g. claude-code,codex)")
26
+ .option("--split <pattern>", "split strategy: backend/frontend or core/tests")
27
+ .option("--yes", "apply ownership (default is dry run)")
28
+ .option("--json", "machine-readable output")
29
+ .action(async (opts) => {
30
+ const agents = opts.agents.split(",").map((a) => a.trim());
31
+ if (agents.length !== 2) {
32
+ console.error("Error: --agents requires exactly two agents.");
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ try {
37
+ const result = await quickStart(process.cwd(), agents, {
38
+ split: opts.split,
39
+ dryRun: !opts.yes,
40
+ });
41
+ if (opts.json) {
42
+ const jsonResult = {
43
+ strategy: result.split.strategy,
44
+ assignments: Object.fromEntries(result.split.assignments),
45
+ unassigned: result.split.unassigned,
46
+ ownershipClaimed: result.ownershipClaimed,
47
+ existingOwnership: result.existingOwnership,
48
+ };
49
+ console.log(JSON.stringify(jsonResult, null, 2));
50
+ }
51
+ else {
52
+ console.log(formatQuickStart(result));
53
+ }
54
+ }
55
+ catch (error) {
56
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
57
+ process.exitCode = 1;
58
+ }
59
+ });
60
+ coord
61
+ .command("detect")
62
+ .description("Auto-detect cross-boundary exports that should be contracts")
63
+ .option("--scope <dirs...>", "only scan these directories (default: whole project)")
64
+ .option("--publish", "prepare publication of detected contracts")
65
+ .option("--yes", "publish exact candidates after previewing")
66
+ .option("--json", "machine-readable output")
67
+ .action(async (opts) => {
68
+ const cwd = process.cwd();
69
+ try {
70
+ const result = await detectContracts(cwd, { scope: opts.scope });
71
+ if (opts.json) {
72
+ console.log(JSON.stringify({
73
+ filesScanned: result.filesScanned,
74
+ crossBoundaryImports: result.crossBoundaryImports.length,
75
+ candidates: result.candidates.map((c) => ({
76
+ name: c.name,
77
+ sourceFile: c.sourceFile,
78
+ sourceAgent: c.sourceAgent,
79
+ consumers: c.consumers,
80
+ symbols: c.sharedSymbols.map((s) => s.name),
81
+ coverageState: c.coverageState,
82
+ publishable: c.publishable,
83
+ sourceHash: c.sourceHash,
84
+ })),
85
+ }, null, 2));
86
+ }
87
+ else {
88
+ console.log(formatDetectionResult(result));
89
+ }
90
+ if (opts.publish) {
91
+ const uncovered = result.candidates.filter((c) => c.coverageState !== "current");
92
+ if (uncovered.length === 0) {
93
+ if (!opts.json)
94
+ console.log("\nAll boundaries already covered by contracts.");
95
+ return;
96
+ }
97
+ const manual = uncovered.filter((c) => !c.publishable);
98
+ if (manual.length > 0) {
99
+ throw new Error(`${manual.length} candidate(s) require manual declarations: ${manual.map((c) => c.name).join(", ")}`);
100
+ }
101
+ if (!opts.yes) {
102
+ if (!opts.json)
103
+ console.log(`\nPreview only — ${uncovered.length} exact contract(s) ready. Re-run with --publish --yes.`);
104
+ return;
105
+ }
106
+ for (const c of uncovered) {
107
+ const event = await publishContract(cwd, {
108
+ from: c.sourceAgent,
109
+ name: c.name,
110
+ body: c.suggestedBody,
111
+ format: "typescript",
112
+ });
113
+ const rev = event.payload.revision;
114
+ if (!opts.json) {
115
+ console.log(`\x1b[32m✓\x1b[0m Published '${c.name}' rev${rev}`);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ catch (error) {
121
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
122
+ process.exitCode = 1;
123
+ }
124
+ });
125
+ coord
126
+ .command("git-ownership")
127
+ .description("Infer directory ownership from git commit history and optionally apply it")
128
+ .requiredOption("--agents <agents>", "agent=Git Author mappings, comma-separated (exact names also work)")
129
+ .option("--depth <n>", "directory grouping depth (default 1)", (_v) => Number(_v), 1)
130
+ .option("--threshold <n>", "minimum % of commits to suggest ownership (default 60)", (_v) => Number(_v), 60)
131
+ .option("--max-commits <n>", "max commits to scan (default 200)", parseInt)
132
+ .option("--yes", "apply suggested ownership (default is dry run)")
133
+ .option("--json", "machine-readable output")
134
+ .action(async (opts) => {
135
+ const agents = opts.agents.split(",").map((a) => a.trim());
136
+ try {
137
+ const result = await applyGitOwnership(process.cwd(), agents, {
138
+ depth: opts.depth,
139
+ threshold: opts.threshold,
140
+ maxCommits: opts.maxCommits,
141
+ dryRun: !opts.yes,
142
+ });
143
+ if (opts.json) {
144
+ console.log(JSON.stringify({
145
+ suggestions: result.suggestions,
146
+ ownershipApplied: result.ownershipApplied,
147
+ authorStats: result.authorStats.map((s) => ({
148
+ author: s.author,
149
+ totalCommits: s.totalCommits,
150
+ directories: Object.fromEntries(s.directories),
151
+ })),
152
+ }, null, 2));
153
+ }
154
+ else {
155
+ console.log(formatGitOwnership(result, !opts.yes));
156
+ }
157
+ }
158
+ catch (error) {
159
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
160
+ process.exitCode = 1;
161
+ }
162
+ });
163
+ coord
164
+ .command("snapshot")
165
+ .description("Current coordination state for an agent")
166
+ .argument("<agent>", "agent requesting the snapshot")
167
+ .option("--json", "machine-readable JSON output")
168
+ .action(async (agent, options) => {
169
+ const snap = await snapshot(process.cwd(), agent);
170
+ if (options.json) {
171
+ console.log(JSON.stringify(snap, null, 2));
172
+ }
173
+ else {
174
+ console.log(formatSnapshot(snap));
175
+ }
176
+ });
177
+ coord
178
+ .command("contract")
179
+ .description("Publish or list API contracts")
180
+ .argument("[name]", "contract name to publish or inspect")
181
+ .option("--agent <agent>", "publishing agent", "user")
182
+ .option("--revision <n>", "revision number", parseInt)
183
+ .option("--body <text>", "contract body")
184
+ .option("--format <fmt>", "format hint: typescript, openapi-yaml, sql")
185
+ .option("--json", "machine-readable JSON output")
186
+ .action(async (name, options) => {
187
+ const cwd = process.cwd();
188
+ // No name: list active contracts
189
+ if (!name) {
190
+ const contracts = await getContracts(cwd);
191
+ if (options.json) {
192
+ console.log(JSON.stringify([...contracts.values()], null, 2));
193
+ }
194
+ else if (contracts.size === 0) {
195
+ console.log("No active contracts.");
196
+ }
197
+ else {
198
+ console.log(`Active contracts (${contracts.size}):`);
199
+ for (const c of contracts.values()) {
200
+ console.log(` ${c.name} rev${c.revision} by ${c.publisher}${c.format ? ` (${c.format})` : ""}`);
201
+ }
202
+ }
203
+ return;
204
+ }
205
+ // Name without body: inspect
206
+ if (!options.body) {
207
+ const contracts = await getContracts(cwd);
208
+ const contract = contracts.get(name);
209
+ if (!contract) {
210
+ console.log(`No contract named '${name}'.`);
211
+ return;
212
+ }
213
+ if (options.json) {
214
+ console.log(JSON.stringify(contract, null, 2));
215
+ }
216
+ else {
217
+ console.log(`${contract.name} rev${contract.revision} by ${contract.publisher}`);
218
+ if (contract.format)
219
+ console.log(`Format: ${contract.format}`);
220
+ console.log("---");
221
+ console.log(contract.body);
222
+ }
223
+ return;
224
+ }
225
+ const event = await publishContract(cwd, {
226
+ from: options.agent,
227
+ name,
228
+ body: options.body,
229
+ revision: options.revision,
230
+ format: options.format,
231
+ });
232
+ const revision = event.payload.revision;
233
+ console.log(`Published '${name}' rev${revision} (seq ${event.seq})`);
234
+ });
235
+ coord
236
+ .command("own")
237
+ .description("Claim file/directory ownership")
238
+ .argument("<agent>", "claiming agent")
239
+ .argument("<paths...>", "paths to claim")
240
+ .option("--shared", "allow shared access (default is exclusive)")
241
+ .option("--json", "machine-readable JSON output")
242
+ .action(async (agent, paths, options) => {
243
+ const cwd = process.cwd();
244
+ const mode = options.shared ? "shared" : "exclusive";
245
+ let event;
246
+ try {
247
+ event = await claimOwnership(cwd, { agent, paths, mode });
248
+ }
249
+ catch (error) {
250
+ if (error instanceof OwnershipConflictError) {
251
+ console.error(formatConflicts(error.conflicts));
252
+ process.exitCode = 1;
253
+ return;
254
+ }
255
+ throw error;
256
+ }
257
+ if (options.json) {
258
+ console.log(JSON.stringify(event, null, 2));
259
+ }
260
+ else {
261
+ console.log(`${agent} now owns (${mode}): ${paths.join(", ")} (seq ${event.seq})`);
262
+ }
263
+ });
264
+ coord
265
+ .command("release")
266
+ .description("Release exact file/directory ownership paths")
267
+ .argument("<agent>", "releasing agent")
268
+ .argument("<paths...>", "exact paths to release")
269
+ .option("--json", "machine-readable JSON output")
270
+ .action(async (agent, paths, options) => {
271
+ const event = await releaseOwnership(process.cwd(), { agent, paths });
272
+ if (options.json) {
273
+ console.log(JSON.stringify(event, null, 2));
274
+ }
275
+ else {
276
+ console.log(`${agent} released: ${paths.join(", ")} (seq ${event.seq})`);
277
+ }
278
+ });
279
+ coord
280
+ .command("decide")
281
+ .description("Record a design decision")
282
+ .argument("<agent>", "deciding agent")
283
+ .argument("<title>", "decision title")
284
+ .option("--rationale <text>", "why this decision was made", "")
285
+ .option("--supersedes <id>", "ID of the decision this replaces")
286
+ .option("--json", "machine-readable JSON output")
287
+ .action(async (agent, title, options) => {
288
+ const event = await emit(process.cwd(), {
289
+ from: agent,
290
+ to: "*",
291
+ type: "decision",
292
+ description: title,
293
+ payload: {
294
+ title,
295
+ rationale: options.rationale,
296
+ ...(options.supersedes ? { supersedes: options.supersedes } : {}),
297
+ },
298
+ });
299
+ if (options.json) {
300
+ console.log(JSON.stringify(event, null, 2));
301
+ }
302
+ else {
303
+ console.log(`Decision recorded: ${title} (seq ${event.seq})`);
304
+ }
305
+ });
306
+ coord
307
+ .command("update")
308
+ .description("Publish a progress update")
309
+ .argument("<agent>", "reporting agent")
310
+ .option("--note <text>", "progress note")
311
+ .option("--files <paths...>", "files modified")
312
+ .option("--blockers <items...>", "blockers")
313
+ .option("--next <text>", "what to do next")
314
+ .option("--json", "machine-readable JSON output")
315
+ .action(async (agent, options) => {
316
+ const payload = {};
317
+ if (options.note)
318
+ payload.note = options.note;
319
+ if (options.files)
320
+ payload.files = options.files;
321
+ if (options.blockers)
322
+ payload.blockers = options.blockers;
323
+ if (options.next)
324
+ payload.next = options.next;
325
+ const event = await emit(process.cwd(), {
326
+ from: agent,
327
+ to: "*",
328
+ type: "update",
329
+ description: options.note ?? "Progress update",
330
+ payload,
331
+ });
332
+ if (options.json) {
333
+ console.log(JSON.stringify(event, null, 2));
334
+ }
335
+ else {
336
+ console.log(`Update published (seq ${event.seq})`);
337
+ }
338
+ });
339
+ coord
340
+ .command("subscribe")
341
+ .description("Read events after a cursor")
342
+ .argument("<agent>", "subscribing agent")
343
+ .option("--cursor <n>", "sequence number to read after", (_v) => Number(_v), -1)
344
+ .option("--json", "machine-readable JSON output")
345
+ .action(async (agent, options) => {
346
+ const { events, highSeq } = await readAfterCursor(process.cwd(), options.cursor);
347
+ const relevant = events.filter((e) => e.to === agent || e.to === "*" || e.from === agent);
348
+ if (options.json) {
349
+ console.log(JSON.stringify({ events: relevant, highSeq }, null, 2));
350
+ }
351
+ else {
352
+ if (!relevant.length) {
353
+ console.log(`No events after seq ${options.cursor} for ${agent}.`);
354
+ }
355
+ else {
356
+ for (const e of relevant) {
357
+ console.log(` seq=${e.seq} [${e.type}] ${e.from}→${e.to}: ${e.description}`);
358
+ }
359
+ }
360
+ console.log(`High watermark: ${highSeq}`);
361
+ }
362
+ });
363
+ coord
364
+ .command("ack")
365
+ .description("Acknowledge events through a sequence number")
366
+ .argument("<agent>", "acknowledging agent")
367
+ .argument("<seq>", "sequence number to acknowledge through", parseInt)
368
+ .option("--note <text>", "what the agent did with these events")
369
+ .action(async (agent, seq, options) => {
370
+ const event = await emit(process.cwd(), {
371
+ from: agent,
372
+ to: "*",
373
+ type: "ack",
374
+ description: `Acknowledged through seq ${seq}`,
375
+ payload: {
376
+ eventSeq: seq,
377
+ ...(options.note ? { note: options.note } : {}),
378
+ },
379
+ });
380
+ console.log(`${agent} acknowledged through seq ${seq} (ack seq ${event.seq})`);
381
+ });
382
+ coord
383
+ .command("watch")
384
+ .description("Watch for coordination events in real time — live terminal feed")
385
+ .argument("[agent]", "filter events for this agent")
386
+ .option("--cursor <n>", "start from this sequence number", parseInt)
387
+ .action(async (agent, options) => {
388
+ const cwd = process.cwd();
389
+ console.log(`\x1b[1mWatching coordination events${agent ? ` for ${agent}` : ""}...\x1b[0m`);
390
+ console.log("Press Ctrl+C to stop.\n");
391
+ // Show recent events first
392
+ const log = await readCoordLog(cwd);
393
+ const startCursor = options.cursor ?? Math.max(log.highSeq - 10, -1);
394
+ const recent = log.events.filter((e) => e.seq > startCursor);
395
+ const filtered = agent
396
+ ? recent.filter((e) => e.to === agent || e.to === "*" || e.from === agent)
397
+ : recent;
398
+ if (filtered.length) {
399
+ console.log("\x1b[90m── recent ──\x1b[0m");
400
+ for (const e of filtered) {
401
+ console.log(formatLiveEvent(e));
402
+ }
403
+ console.log("\x1b[90m── live ──\x1b[0m\n");
404
+ }
405
+ const watcher = await watchCoordination(cwd, {
406
+ agent,
407
+ cursor: log.highSeq,
408
+ onEvents(events) {
409
+ for (const e of events) {
410
+ console.log(formatLiveEvent(e));
411
+ }
412
+ },
413
+ onError(error) {
414
+ console.error(`Watch error: ${error.message}`);
415
+ },
416
+ });
417
+ // Keep alive until Ctrl+C
418
+ process.on("SIGINT", () => {
419
+ watcher.stop();
420
+ console.log("\nStopped watching.");
421
+ process.exit(0);
422
+ });
423
+ // Prevent Node from exiting
424
+ await new Promise(() => { });
425
+ });
426
+ coord
427
+ .command("status")
428
+ .description("Show live coordination status — ownership, contracts, acks")
429
+ .option("--json", "machine-readable JSON output")
430
+ .action(async (options) => {
431
+ const cwd = process.cwd();
432
+ const log = await readCoordLog(cwd);
433
+ const contracts = await getContracts(cwd);
434
+ const ownership = await getOwnership(cwd);
435
+ const ackState = await getAckState(cwd);
436
+ if (options.json) {
437
+ console.log(JSON.stringify({
438
+ events: log.events.length,
439
+ highSeq: log.highSeq,
440
+ corrupt: log.corrupt.length,
441
+ contracts: [...contracts.values()],
442
+ ownership: [...ownership.values()],
443
+ ackCursors: Object.fromEntries(ackState.cursors),
444
+ unacked: ackState.unacked.length,
445
+ }, null, 2));
446
+ return;
447
+ }
448
+ if (log.events.length === 0) {
449
+ console.log("No coordination events yet.");
450
+ console.log("Start with: loadout coord own <agent> <paths...>");
451
+ return;
452
+ }
453
+ console.log(`\x1b[1mCoordination status\x1b[0m (${log.events.length} events, seq ${log.highSeq})`);
454
+ if (contracts.size) {
455
+ console.log(`\n\x1b[36mContracts (${contracts.size}):\x1b[0m`);
456
+ for (const c of contracts.values()) {
457
+ console.log(` ${c.name} rev${c.revision} by ${c.publisher}${c.format ? ` (${c.format})` : ""}`);
458
+ }
459
+ }
460
+ if (ownership.size) {
461
+ console.log(`\n\x1b[33mFile ownership (${ownership.size} paths):\x1b[0m`);
462
+ const byAgent = new Map();
463
+ for (const claim of ownership.values()) {
464
+ const existing = byAgent.get(claim.agent);
465
+ if (existing) {
466
+ existing.paths.push(...claim.paths);
467
+ }
468
+ else {
469
+ byAgent.set(claim.agent, {
470
+ paths: [...claim.paths],
471
+ mode: claim.mode,
472
+ });
473
+ }
474
+ }
475
+ for (const [agent, info] of byAgent) {
476
+ console.log(` ${agent} (${info.mode}): ${info.paths.join(", ")}`);
477
+ }
478
+ }
479
+ if (ackState.cursors.size) {
480
+ console.log(`\n\x1b[32mAck cursors:\x1b[0m`);
481
+ for (const [agent, cursor] of ackState.cursors) {
482
+ const behind = log.highSeq - cursor;
483
+ console.log(` ${agent}: seq ${cursor}${behind > 0 ? ` (${behind} behind)` : " (up to date)"}`);
484
+ }
485
+ }
486
+ if (ackState.unacked.length) {
487
+ console.log(`\n\x1b[31m${ackState.unacked.length} unacknowledged event(s)\x1b[0m`);
488
+ }
489
+ if (log.corrupt.length) {
490
+ console.log(`\n\x1b[31mWarning: ${log.corrupt.length} corrupt line(s)\x1b[0m`);
491
+ }
492
+ });
493
+ coord
494
+ .command("conflicts")
495
+ .alias("preview")
496
+ .description("Preview file conflicts before writing — shows what other agents changed")
497
+ .argument("<agent>", "agent about to write")
498
+ .argument("<paths...>", "file paths the agent intends to write")
499
+ .option("--json", "machine-readable JSON output")
500
+ .action(async (agent, paths, options) => {
501
+ const preview = await previewConflicts(process.cwd(), agent, paths);
502
+ if (options.json) {
503
+ console.log(JSON.stringify(preview, null, 2));
504
+ }
505
+ else {
506
+ console.log(formatConflictPreview(preview));
507
+ }
508
+ });
509
+ coord
510
+ .command("diff")
511
+ .description("Diff contract revisions — shows exactly what changed between versions")
512
+ .argument("<name>", "contract name to diff")
513
+ .option("--from <n>", "from revision (defaults to previous)", parseInt)
514
+ .option("--to <n>", "to revision (defaults to latest)", parseInt)
515
+ .option("--history", "show full revision history")
516
+ .option("--json", "machine-readable JSON output")
517
+ .action(async (name, options) => {
518
+ const cwd = process.cwd();
519
+ if (options.history) {
520
+ const history = await getContractHistory(cwd, name);
521
+ if (history.length === 0) {
522
+ console.log(`No contract named '${name}'.`);
523
+ return;
524
+ }
525
+ if (options.json) {
526
+ console.log(JSON.stringify(history, null, 2));
527
+ }
528
+ else {
529
+ console.log(`\x1b[1m${name}\x1b[0m — ${history.length} revision(s):`);
530
+ for (const rev of history) {
531
+ console.log(` rev${rev.revision} by ${rev.publisher} at ${rev.timestamp} (seq ${rev.seq})`);
532
+ }
533
+ }
534
+ return;
535
+ }
536
+ // Specific revision range or latest diff
537
+ if (options.from && options.to) {
538
+ const history = await getContractHistory(cwd, name);
539
+ const fromRev = history.find((r) => r.revision === options.from);
540
+ const toRev = history.find((r) => r.revision === options.to);
541
+ if (!fromRev || !toRev) {
542
+ console.log(`Could not find rev${options.from} or rev${options.to} for '${name}'.`);
543
+ return;
544
+ }
545
+ const delta = diffContracts(fromRev, toRev);
546
+ if (options.json) {
547
+ console.log(JSON.stringify(delta, null, 2));
548
+ }
549
+ else {
550
+ console.log(formatContractDelta(delta));
551
+ }
552
+ return;
553
+ }
554
+ const delta = await diffLatestContract(cwd, name);
555
+ if (!delta) {
556
+ console.log(`Need at least 2 revisions of '${name}' to diff. Use --history to see revisions.`);
557
+ return;
558
+ }
559
+ if (options.json) {
560
+ console.log(JSON.stringify(delta, null, 2));
561
+ }
562
+ else {
563
+ console.log(formatContractDelta(delta));
564
+ }
565
+ });
566
+ coord
567
+ .command("replay")
568
+ .description("Replay the full coordination timeline as a narrative story")
569
+ .option("--json", "machine-readable JSON output")
570
+ .action(async (options) => {
571
+ const timeline = await buildReplay(process.cwd());
572
+ if (options.json) {
573
+ console.log(JSON.stringify(timeline, null, 2));
574
+ }
575
+ else {
576
+ console.log(formatReplay(timeline));
577
+ }
578
+ });
579
+ coord
580
+ .command("compact")
581
+ .description("Compact the coordination log — archive old events")
582
+ .option("--max-events <n>", "max events to keep", (v) => Number(v), 10000)
583
+ .option("--max-age <days>", "max age in days", (v) => Number(v), 30)
584
+ .option("--json", "machine-readable JSON output")
585
+ .action(async (options) => {
586
+ const result = await compact(process.cwd(), {
587
+ maxEvents: options.maxEvents,
588
+ maxAgeDays: options.maxAge,
589
+ });
590
+ if (options.json) {
591
+ console.log(JSON.stringify(result, null, 2));
592
+ }
593
+ else if (!result.compacted) {
594
+ const size = await logSize(process.cwd());
595
+ console.log(`Log has ${size.events} events (${(size.bytes / 1024).toFixed(1)} KB) — under threshold, nothing to compact.`);
596
+ }
597
+ else {
598
+ console.log(`Compacted: ${result.before} → ${result.after} events (removed ${result.removed})`);
599
+ }
600
+ });
601
+ // `loadout daemon` — start/stop the coordination daemon
602
+ const daemon = program
603
+ .command("daemon")
604
+ .description("Local coordination daemon with REST API, SSE, and dashboard");
605
+ daemon
606
+ .command("start")
607
+ .description("Start the coordination daemon — HTTP server with live dashboard")
608
+ .option("--port <n>", "port to listen on", (_v) => Number(_v), 4510)
609
+ .action(async (options) => {
610
+ const cwd = process.cwd();
611
+ console.log(`Starting coordination daemon on port ${options.port}...`);
612
+ try {
613
+ const d = await startDaemon(cwd, options.port);
614
+ console.log(`\x1b[32m✓\x1b[0m Daemon running at http://127.0.0.1:${d.port}`);
615
+ console.log(` Dashboard: ${d.dashboardUrl}`);
616
+ console.log(" API/SSE: bearer-authenticated (token stored in .handoff/daemon.token)");
617
+ console.log(`\nPress Ctrl+C to stop.`);
618
+ process.on("SIGINT", () => {
619
+ console.log("\nStopping daemon...");
620
+ d.close();
621
+ process.exit(0);
622
+ });
623
+ // Keep alive
624
+ await new Promise(() => { });
625
+ }
626
+ catch (error) {
627
+ console.error(`Failed to start daemon: ${error instanceof Error ? error.message : error}`);
628
+ process.exitCode = 1;
629
+ }
630
+ });
631
+ daemon
632
+ .command("stop")
633
+ .description("Stop a running coordination daemon")
634
+ .action(async () => {
635
+ const result = await stopDaemon(process.cwd());
636
+ if (result.stopped) {
637
+ console.log(`\x1b[32m✓\x1b[0m Daemon stopped (pid ${result.pid})`);
638
+ }
639
+ else if (result.stale) {
640
+ console.log(`Cleaned up stale PID file (pid ${result.pid} was not running)`);
641
+ }
642
+ else {
643
+ console.log("No daemon running.");
644
+ }
645
+ });
646
+ daemon
647
+ .command("status")
648
+ .description("Check if the coordination daemon is running")
649
+ .option("--json", "machine-readable JSON output")
650
+ .action(async (options) => {
651
+ const cwd = process.cwd();
652
+ const status = await getDaemonStatus(cwd);
653
+ const ks = await isKillSwitchActive(cwd);
654
+ if (options.json) {
655
+ console.log(JSON.stringify({ ...status, killSwitch: ks }, null, 2));
656
+ return;
657
+ }
658
+ if (ks.active) {
659
+ console.log(`\x1b[31m⛔ Kill switch active: ${ks.reason}\x1b[0m`);
660
+ console.log(` Activated: ${ks.activatedAt}`);
661
+ console.log(" Resume with: loadout daemon resume");
662
+ return;
663
+ }
664
+ if (status.running && status.info) {
665
+ const token = await ensureDaemonToken(cwd);
666
+ console.log(`\x1b[32m✓\x1b[0m Daemon running`);
667
+ console.log(` PID: ${status.info.pid}`);
668
+ console.log(` Port: ${status.info.port}`);
669
+ console.log(` Started: ${status.info.startedAt}`);
670
+ console.log(` Dashboard: http://127.0.0.1:${status.info.port}/#token=${encodeURIComponent(token)}`);
671
+ }
672
+ else if (status.stale) {
673
+ console.log("Daemon not running (cleaned up stale PID file)");
674
+ }
675
+ else {
676
+ console.log("Daemon not running.");
677
+ console.log("Start with: loadout daemon start");
678
+ }
679
+ });
680
+ daemon
681
+ .command("kill")
682
+ .description("Emergency kill switch — halt all coordination immediately")
683
+ .argument("<reason>", "why coordination is being halted")
684
+ .action(async (reason) => {
685
+ await activateKillSwitch(process.cwd(), reason);
686
+ console.log("\x1b[31m⛔ Kill switch activated\x1b[0m");
687
+ console.log(` Reason: ${reason}`);
688
+ console.log(" Daemon stopped, all coordination halted.");
689
+ console.log(" Resume with: loadout daemon resume");
690
+ });
691
+ daemon
692
+ .command("resume")
693
+ .description("Deactivate the kill switch and resume coordination")
694
+ .action(async () => {
695
+ const deactivated = await deactivateKillSwitch(process.cwd());
696
+ if (deactivated) {
697
+ console.log("\x1b[32m✓\x1b[0m Kill switch deactivated. Coordination resumed.");
698
+ console.log(" Start daemon with: loadout daemon start");
699
+ }
700
+ else {
701
+ console.log("Kill switch was not active.");
702
+ }
703
+ });
704
+ // `loadout serve` — start the MCP coordination server
705
+ program
706
+ .command("serve")
707
+ .description("Start the coordination MCP server for live agent collaboration")
708
+ .action(async () => {
709
+ // Dynamic import avoids a hard dependency on @modelcontextprotocol/sdk.
710
+ // The string concatenation prevents tsc from resolving the specifier.
711
+ const modulePath = ["../core/coordination", "mcp-server.js"].join("/");
712
+ try {
713
+ const mod = (await import(modulePath));
714
+ await mod.startMcpServer(process.cwd());
715
+ }
716
+ catch (error) {
717
+ if (error instanceof Error &&
718
+ (error.message.includes("Cannot find module") ||
719
+ error.message.includes("ERR_MODULE_NOT_FOUND"))) {
720
+ console.error("The MCP server requires @modelcontextprotocol/sdk.\n" +
721
+ "Install it with: npm install @modelcontextprotocol/sdk\n" +
722
+ "Then run: loadout serve");
723
+ process.exitCode = 1;
724
+ }
725
+ else {
726
+ throw error;
727
+ }
728
+ }
729
+ });
730
+ }