gitswarm 0.0.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.
@@ -0,0 +1,840 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * gitswarm — standalone CLI for local multi-agent federation coordination.
5
+ *
6
+ * Usage:
7
+ * gitswarm init [--name <name>] [--model solo|guild|open] [--mode swarm|review|gated]
8
+ * gitswarm agent register <name> [--desc <description>]
9
+ * gitswarm agent list
10
+ * gitswarm agent info <name|id>
11
+ * gitswarm workspace create --as <agent> [--name <n>] [--task <id>] [--fork <stream>]
12
+ * gitswarm workspace list
13
+ * gitswarm workspace destroy <agent> [--abandon]
14
+ * gitswarm commit --as <agent> -m <message>
15
+ * gitswarm stream list [--status <s>]
16
+ * gitswarm stream info <stream-id>
17
+ * gitswarm stream diff <stream-id>
18
+ * gitswarm review submit <stream-id> approve|request_changes --as <agent> [--feedback <f>]
19
+ * gitswarm review list <stream-id>
20
+ * gitswarm review check <stream-id>
21
+ * gitswarm merge <stream-id> --as <agent>
22
+ * gitswarm stabilize
23
+ * gitswarm promote [--tag <t>]
24
+ * gitswarm task create <title> [--desc <d>] [--priority <p>] [--as <agent>]
25
+ * gitswarm task list [--status <s>]
26
+ * gitswarm task claim <id> --as <agent>
27
+ * gitswarm task submit <claim-id> --as <agent> [--notes <n>]
28
+ * gitswarm task review <claim-id> approve|reject --as <agent> [--notes <n>]
29
+ * gitswarm council create [--min-karma <n>] [--quorum <n>]
30
+ * gitswarm council propose <type> <title> --as <agent>
31
+ * gitswarm council vote <proposal-id> for|against --as <agent>
32
+ * gitswarm council status
33
+ * gitswarm status
34
+ * gitswarm log [--limit <n>]
35
+ * gitswarm config [key] [value]
36
+ */
37
+
38
+ import { resolve } from 'path';
39
+ import { Federation } from '../src/federation.js';
40
+
41
+ // ── Argument parsing ───────────────────────────────────────
42
+
43
+ function parseArgs(argv) {
44
+ const args = argv.slice(2);
45
+ const positional = [];
46
+ const flags = {};
47
+
48
+ for (let i = 0; i < args.length; i++) {
49
+ if (args[i] === '-m' && i + 1 < args.length) {
50
+ // Special handling for -m <message>
51
+ flags.m = args[++i];
52
+ } else if (args[i].startsWith('--')) {
53
+ const key = args[i].slice(2);
54
+ if (i + 1 >= args.length || args[i + 1].startsWith('--')) {
55
+ flags[key] = true;
56
+ } else {
57
+ flags[key] = args[++i];
58
+ }
59
+ } else {
60
+ positional.push(args[i]);
61
+ }
62
+ }
63
+
64
+ return { positional, flags };
65
+ }
66
+
67
+ // ── Formatting helpers ─────────────────────────────────────
68
+
69
+ function table(rows, columns) {
70
+ if (rows.length === 0) { console.log(' (none)'); return; }
71
+ const widths = columns.map(c =>
72
+ Math.max(c.label.length, ...rows.map(r => String(c.get(r) ?? '').length))
73
+ );
74
+ const header = columns.map((c, i) => c.label.padEnd(widths[i])).join(' ');
75
+ console.log(` ${header}`);
76
+ console.log(` ${widths.map(w => '─'.repeat(w)).join('──')}`);
77
+ for (const row of rows) {
78
+ const line = columns.map((c, i) => String(c.get(row) ?? '').padEnd(widths[i])).join(' ');
79
+ console.log(` ${line}`);
80
+ }
81
+ }
82
+
83
+ function short(id) {
84
+ return id ? id.slice(0, 8) : '—';
85
+ }
86
+
87
+ function timeAgo(ts) {
88
+ if (!ts) return '—';
89
+ const ms = typeof ts === 'number' ? Date.now() - ts : Date.now() - new Date(ts).getTime();
90
+ const mins = Math.floor(ms / 60000);
91
+ if (mins < 1) return 'just now';
92
+ if (mins < 60) return `${mins}m ago`;
93
+ const hours = Math.floor(mins / 60);
94
+ if (hours < 24) return `${hours}h ago`;
95
+ return `${Math.floor(hours / 24)}d ago`;
96
+ }
97
+
98
+ // ── Commands ───────────────────────────────────────────────
99
+
100
+ const commands = {};
101
+
102
+ // --- init ---
103
+ commands.init = (_fed, { flags }) => {
104
+ const cwd = resolve(process.cwd());
105
+ try {
106
+ const { config } = Federation.init(cwd, {
107
+ name: flags.name,
108
+ merge_mode: flags.mode,
109
+ ownership_model: flags.model,
110
+ agent_access: flags.access,
111
+ consensus_threshold: flags.threshold ? parseFloat(flags.threshold) : undefined,
112
+ min_reviews: flags['min-reviews'] ? parseInt(flags['min-reviews']) : undefined,
113
+ buffer_branch: flags['buffer-branch'],
114
+ promote_target: flags['promote-target'],
115
+ stabilize_command: flags['stabilize-command'],
116
+ });
117
+ console.log(`Initialised gitswarm federation: ${config.name}`);
118
+ console.log(` mode: ${config.merge_mode}`);
119
+ console.log(` model: ${config.ownership_model}`);
120
+ console.log(` access: ${config.agent_access}`);
121
+ console.log(` consensus: ${config.consensus_threshold}`);
122
+ console.log(` store: .gitswarm/federation.db`);
123
+ console.log('\nNext steps:');
124
+ console.log(' gitswarm agent register <name> Register an agent');
125
+ console.log(' gitswarm workspace create --as <a> Create an isolated workspace');
126
+ console.log(' gitswarm task create <title> Create a task');
127
+ } catch (e) {
128
+ console.error(`Error: ${e.message}`);
129
+ process.exit(1);
130
+ }
131
+ };
132
+
133
+ // --- agent ---
134
+ commands.agent = async (fed, { positional, flags }) => {
135
+ const sub = positional[0];
136
+
137
+ if (sub === 'register') {
138
+ const name = positional[1] || flags.name;
139
+ if (!name) { console.error('Usage: gitswarm agent register <name>'); process.exit(1); }
140
+ const { agent, api_key } = await fed.registerAgent(name, flags.desc || '');
141
+ console.log(`Agent registered: ${agent.name}`);
142
+ console.log(` id: ${agent.id}`);
143
+ console.log(` api_key: ${api_key} (save this — shown only once)`);
144
+ return;
145
+ }
146
+
147
+ if (sub === 'list') {
148
+ const agents = await fed.listAgents();
149
+ table(agents, [
150
+ { label: 'ID', get: r => short(r.id) },
151
+ { label: 'NAME', get: r => r.name },
152
+ { label: 'KARMA', get: r => r.karma },
153
+ { label: 'STATUS', get: r => r.status },
154
+ { label: 'CREATED', get: r => r.created_at?.slice(0, 10) },
155
+ ]);
156
+ return;
157
+ }
158
+
159
+ if (sub === 'info') {
160
+ const ref = positional[1];
161
+ if (!ref) { console.error('Usage: gitswarm agent info <name|id>'); process.exit(1); }
162
+ const agent = await fed.getAgent(ref);
163
+ if (!agent) { console.error(`Agent not found: ${ref}`); process.exit(1); }
164
+ console.log(`Agent: ${agent.name}`);
165
+ console.log(` id: ${agent.id}`);
166
+ console.log(` karma: ${agent.karma}`);
167
+ console.log(` status: ${agent.status}`);
168
+ console.log(` description: ${agent.description || '—'}`);
169
+ console.log(` created: ${agent.created_at}`);
170
+ return;
171
+ }
172
+
173
+ console.error('Usage: gitswarm agent <register|list|info>');
174
+ process.exit(1);
175
+ };
176
+
177
+ // --- workspace ---
178
+ commands.workspace = async (fed, { positional, flags }) => {
179
+ const sub = positional[0];
180
+
181
+ if (sub === 'create') {
182
+ if (!flags.as) {
183
+ console.error('Usage: gitswarm workspace create --as <agent> [--name <n>] [--task <id>] [--fork <stream>]');
184
+ process.exit(1);
185
+ }
186
+ const agent = await fed.resolveAgent(flags.as);
187
+ const taskId = flags.task ? await resolveId(fed, 'tasks', flags.task) : undefined;
188
+ const ws = await fed.createWorkspace({
189
+ agentId: agent.id,
190
+ name: flags.name,
191
+ taskId,
192
+ dependsOn: flags.fork,
193
+ });
194
+ console.log(`Workspace created for ${agent.name}`);
195
+ console.log(` stream: ${ws.streamId}`);
196
+ console.log(` path: ${ws.path}`);
197
+ return;
198
+ }
199
+
200
+ if (sub === 'list') {
201
+ const workspaces = await fed.listWorkspaces();
202
+ table(workspaces, [
203
+ { label: 'AGENT', get: r => r.agentName },
204
+ { label: 'STREAM', get: r => short(r.streamId) },
205
+ { label: 'NAME', get: r => r.streamName || '—' },
206
+ { label: 'STATUS', get: r => r.streamStatus || '—' },
207
+ { label: 'ACTIVE', get: r => timeAgo(r.lastActive) },
208
+ { label: 'PATH', get: r => r.path },
209
+ ]);
210
+ return;
211
+ }
212
+
213
+ if (sub === 'destroy') {
214
+ const agentRef = positional[1];
215
+ if (!agentRef) { console.error('Usage: gitswarm workspace destroy <agent> [--abandon]'); process.exit(1); }
216
+ const agent = await fed.resolveAgent(agentRef);
217
+ await fed.destroyWorkspace(agent.id, { abandonStream: !!flags.abandon });
218
+ console.log(`Workspace destroyed for ${agent.name}`);
219
+ return;
220
+ }
221
+
222
+ console.error('Usage: gitswarm workspace <create|list|destroy>');
223
+ process.exit(1);
224
+ };
225
+
226
+ // --- commit ---
227
+ commands.commit = async (fed, { flags }) => {
228
+ if (!flags.as || !flags.m) {
229
+ console.error('Usage: gitswarm commit --as <agent> -m <message>');
230
+ process.exit(1);
231
+ }
232
+ const agent = await fed.resolveAgent(flags.as);
233
+ const result = await fed.commit({
234
+ agentId: agent.id,
235
+ message: flags.m,
236
+ streamId: flags.stream,
237
+ });
238
+ console.log(`Committed: ${result.commit?.slice(0, 8)}`);
239
+ console.log(` Change-Id: ${result.changeId}`);
240
+ if (result.merged) console.log(` Auto-merged to buffer (swarm mode)`);
241
+ if (result.conflicts) console.log(` Merge conflicts: ${result.conflicts.length} files`);
242
+ if (result.mergeError) console.log(` Merge error: ${result.mergeError}`);
243
+ };
244
+
245
+ // --- stream ---
246
+ commands.stream = async (fed, { positional, flags }) => {
247
+ const sub = positional[0];
248
+
249
+ if (sub === 'list') {
250
+ const streams = fed._ensureTracker().listStreams({
251
+ status: flags.status || undefined,
252
+ });
253
+ table(streams, [
254
+ { label: 'ID', get: r => short(r.id) },
255
+ { label: 'NAME', get: r => r.name },
256
+ { label: 'AGENT', get: r => short(r.agentId) },
257
+ { label: 'STATUS', get: r => r.status },
258
+ { label: 'PARENT', get: r => r.parentStream ? short(r.parentStream) : '—' },
259
+ { label: 'UPDATED', get: r => timeAgo(r.updatedAt) },
260
+ ]);
261
+ return;
262
+ }
263
+
264
+ if (sub === 'info') {
265
+ const streamId = positional[1];
266
+ if (!streamId) { console.error('Usage: gitswarm stream info <stream-id>'); process.exit(1); }
267
+ const info = fed.getStreamInfo(streamId);
268
+ if (!info) { console.error(`Stream not found: ${streamId}`); process.exit(1); }
269
+ const { stream, changes, operations, dependencies, children } = info;
270
+ console.log(`Stream: ${stream.name}`);
271
+ console.log(` id: ${stream.id}`);
272
+ console.log(` agent: ${stream.agentId}`);
273
+ console.log(` status: ${stream.status}`);
274
+ console.log(` base: ${stream.baseCommit?.slice(0, 8) || '—'}`);
275
+ console.log(` parent: ${stream.parentStream || '—'}`);
276
+ console.log(` changes: ${changes.length}`);
277
+ console.log(` operations: ${operations.length}`);
278
+ console.log(` deps: ${dependencies.length}`);
279
+ console.log(` children: ${children.length}`);
280
+ return;
281
+ }
282
+
283
+ if (sub === 'diff') {
284
+ const streamId = positional[1];
285
+ if (!streamId) { console.error('Usage: gitswarm stream diff <stream-id>'); process.exit(1); }
286
+ const diff = flags.full ? fed.getStreamDiffFull(streamId) : fed.getStreamDiff(streamId);
287
+ console.log(diff || '(no changes)');
288
+ return;
289
+ }
290
+
291
+ console.error('Usage: gitswarm stream <list|info|diff>');
292
+ process.exit(1);
293
+ };
294
+
295
+ // --- review (stream-based) ---
296
+ commands.review = async (fed, { positional, flags }) => {
297
+ const sub = positional[0];
298
+
299
+ if (sub === 'submit') {
300
+ const streamId = positional[1];
301
+ const verdict = positional[2];
302
+ if (!streamId || !verdict || !flags.as) {
303
+ console.error('Usage: gitswarm review submit <stream-id> approve|request_changes --as <agent> [--feedback <f>]');
304
+ process.exit(1);
305
+ }
306
+ const agent = await fed.resolveAgent(flags.as);
307
+ await fed.submitReview(streamId, agent.id, verdict, flags.feedback || '', {
308
+ isHuman: !!flags.human,
309
+ });
310
+ console.log(`Review submitted: ${verdict}`);
311
+ return;
312
+ }
313
+
314
+ if (sub === 'list') {
315
+ const streamId = positional[1];
316
+ if (!streamId) { console.error('Usage: gitswarm review list <stream-id>'); process.exit(1); }
317
+ const reviews = await fed.getReviews(streamId);
318
+ table(reviews, [
319
+ { label: 'REVIEWER', get: r => r.reviewer_name || short(r.reviewer_id) },
320
+ { label: 'VERDICT', get: r => r.verdict },
321
+ { label: 'HUMAN', get: r => r.is_human ? 'yes' : 'no' },
322
+ { label: 'FEEDBACK', get: r => (r.feedback || '').slice(0, 50) },
323
+ { label: 'DATE', get: r => r.reviewed_at?.slice(0, 10) },
324
+ ]);
325
+ return;
326
+ }
327
+
328
+ if (sub === 'check') {
329
+ const streamId = positional[1];
330
+ if (!streamId) { console.error('Usage: gitswarm review check <stream-id>'); process.exit(1); }
331
+ const result = await fed.checkConsensus(streamId);
332
+ console.log(`Consensus: ${result.reached ? 'REACHED' : 'NOT REACHED'}`);
333
+ console.log(` reason: ${result.reason}`);
334
+ if (result.ratio !== undefined) console.log(` ratio: ${result.ratio} (threshold: ${result.threshold})`);
335
+ if (result.approvals !== undefined) console.log(` approvals: ${result.approvals} rejections: ${result.rejections}`);
336
+ return;
337
+ }
338
+
339
+ console.error('Usage: gitswarm review <submit|list|check>');
340
+ process.exit(1);
341
+ };
342
+
343
+ // --- merge ---
344
+ commands.merge = async (fed, { positional, flags }) => {
345
+ const streamId = positional[0];
346
+ if (!streamId || !flags.as) {
347
+ console.error('Usage: gitswarm merge <stream-id> --as <agent>');
348
+ process.exit(1);
349
+ }
350
+ const agent = await fed.resolveAgent(flags.as);
351
+ const result = await fed.mergeToBuffer(streamId, agent.id);
352
+ console.log(`Merge queued: ${short(result.entryId)}`);
353
+ if (result.queueResult) {
354
+ console.log(` processed: ${result.queueResult.processed || 0}`);
355
+ console.log(` merged: ${result.queueResult.merged || 0}`);
356
+ }
357
+ };
358
+
359
+ // --- stabilize ---
360
+ commands.stabilize = async (fed) => {
361
+ const result = await fed.stabilize();
362
+ if (result.success) {
363
+ console.log(`Stabilization: GREEN`);
364
+ console.log(` tag: ${result.tag}`);
365
+ if (result.promoted) console.log(` promoted to main`);
366
+ } else {
367
+ console.log(`Stabilization: RED`);
368
+ if (result.output) console.log(` output: ${result.output.slice(0, 200)}`);
369
+ if (result.reverted) console.log(` reverted stream: ${result.reverted.streamId}`);
370
+ if (result.revertError) console.log(` revert error: ${result.revertError}`);
371
+ }
372
+ };
373
+
374
+ // --- promote ---
375
+ commands.promote = async (fed, { flags }) => {
376
+ const result = await fed.promote({ tag: flags.tag });
377
+ console.log(`Promoted: ${result.from} → ${result.to}`);
378
+ };
379
+
380
+ // --- task ---
381
+ commands.task = async (fed, { positional, flags }) => {
382
+ const repo = await fed.repo();
383
+ if (!repo) { console.error('No repo found. Run gitswarm init first.'); process.exit(1); }
384
+ const sub = positional[0];
385
+
386
+ if (sub === 'create') {
387
+ const title = positional.slice(1).join(' ') || flags.title;
388
+ if (!title) { console.error('Usage: gitswarm task create <title> [--as <agent>]'); process.exit(1); }
389
+ const agent = flags.as ? await fed.resolveAgent(flags.as) : null;
390
+ const task = await fed.tasks.create(repo.id, {
391
+ title,
392
+ description: flags.desc || '',
393
+ priority: flags.priority || 'medium',
394
+ amount: flags.amount ? parseInt(flags.amount) : 0,
395
+ difficulty: flags.difficulty,
396
+ }, agent?.id);
397
+ console.log(`Task created: ${short(task.id)} ${task.title}`);
398
+ return;
399
+ }
400
+
401
+ if (sub === 'list') {
402
+ const tasks = await fed.tasks.list(repo.id, { status: flags.status });
403
+ table(tasks, [
404
+ { label: 'ID', get: r => short(r.id) },
405
+ { label: 'STATUS', get: r => r.status },
406
+ { label: 'PRIORITY', get: r => r.priority },
407
+ { label: 'TITLE', get: r => r.title },
408
+ { label: 'CLAIMS', get: r => r.active_claims || 0 },
409
+ { label: 'CREATOR', get: r => r.creator_name || '—' },
410
+ ]);
411
+ return;
412
+ }
413
+
414
+ if (sub === 'claim') {
415
+ const taskId = positional[1];
416
+ if (!taskId || !flags.as) { console.error('Usage: gitswarm task claim <id> --as <agent>'); process.exit(1); }
417
+ const agent = await fed.resolveAgent(flags.as);
418
+ const fullId = await resolveId(fed, 'tasks', taskId);
419
+ const claim = await fed.tasks.claim(fullId, agent.id, flags.stream);
420
+ console.log(`Task claimed: ${short(claim.id)}`);
421
+ if (claim.stream_id) console.log(` stream: ${claim.stream_id}`);
422
+
423
+ // Mode B: sync task claim to server
424
+ if (fed.sync) {
425
+ try {
426
+ await fed.sync.claimTask(repo.id, fullId, { streamId: flags.stream });
427
+ } catch {
428
+ fed.sync._queueEvent({ type: 'task_claim', data: {
429
+ repoId: repo.id, taskId: fullId, streamId: flags.stream,
430
+ }});
431
+ }
432
+ }
433
+ return;
434
+ }
435
+
436
+ if (sub === 'submit') {
437
+ const claimId = positional[1];
438
+ if (!claimId || !flags.as) { console.error('Usage: gitswarm task submit <claim-id> --as <agent>'); process.exit(1); }
439
+ const agent = await fed.resolveAgent(flags.as);
440
+ const fullId = await resolveId(fed, 'task_claims', claimId);
441
+ const result = await fed.tasks.submit(fullId, agent.id, {
442
+ stream_id: flags.stream,
443
+ notes: flags.notes || '',
444
+ });
445
+ console.log(`Submission recorded: ${short(result.id)}`);
446
+
447
+ // Mode B: sync task submission to server
448
+ if (fed.sync) {
449
+ try {
450
+ await fed.sync.syncTaskSubmission(repo.id, result.task_id, fullId, {
451
+ streamId: result.stream_id,
452
+ notes: flags.notes || '',
453
+ });
454
+ } catch {
455
+ fed.sync._queueEvent({ type: 'task_submission', data: {
456
+ repoId: repo.id, taskId: result.task_id, claimId: fullId,
457
+ streamId: result.stream_id, notes: flags.notes || '',
458
+ }});
459
+ }
460
+ }
461
+ return;
462
+ }
463
+
464
+ if (sub === 'review') {
465
+ const claimId = positional[1];
466
+ const decision = positional[2];
467
+ if (!claimId || !decision || !flags.as) {
468
+ console.error('Usage: gitswarm task review <claim-id> approve|reject --as <agent>');
469
+ process.exit(1);
470
+ }
471
+ const agent = await fed.resolveAgent(flags.as);
472
+ const fullId = await resolveId(fed, 'task_claims', claimId);
473
+ const result = await fed.tasks.review(fullId, agent.id, decision, flags.notes);
474
+ console.log(`Review: ${result.action}`);
475
+ return;
476
+ }
477
+
478
+ console.error('Usage: gitswarm task <create|list|claim|submit|review>');
479
+ process.exit(1);
480
+ };
481
+
482
+ // --- council ---
483
+ commands.council = async (fed, { positional, flags }) => {
484
+ const repo = await fed.repo();
485
+ if (!repo) { console.error('No repo found.'); process.exit(1); }
486
+ const sub = positional[0];
487
+
488
+ if (sub === 'create') {
489
+ const council = await fed.council.create(repo.id, {
490
+ min_karma: flags['min-karma'] ? parseInt(flags['min-karma']) : undefined,
491
+ min_contributions: flags['min-contribs'] ? parseInt(flags['min-contribs']) : undefined,
492
+ min_members: flags['min-members'] ? parseInt(flags['min-members']) : undefined,
493
+ max_members: flags['max-members'] ? parseInt(flags['max-members']) : undefined,
494
+ standard_quorum: flags.quorum ? parseInt(flags.quorum) : undefined,
495
+ critical_quorum: flags['critical-quorum'] ? parseInt(flags['critical-quorum']) : undefined,
496
+ });
497
+ console.log(`Council created: ${short(council.id)} status: ${council.status}`);
498
+ return;
499
+ }
500
+
501
+ if (sub === 'status') {
502
+ const council = await fed.council.getCouncil(repo.id);
503
+ if (!council) { console.log('No council for this repo.'); return; }
504
+ const members = await fed.council.getMembers(council.id);
505
+ const proposals = await fed.council.listProposals(council.id, 'open');
506
+ console.log(`Council: ${council.status}`);
507
+ console.log(` members: ${members.length}/${council.max_members} (min: ${council.min_members})`);
508
+ console.log(` quorum: ${council.standard_quorum} (critical: ${council.critical_quorum})`);
509
+ console.log(` open proposals: ${proposals.length}`);
510
+ if (members.length > 0) {
511
+ console.log('\nMembers:');
512
+ table(members, [
513
+ { label: 'NAME', get: r => r.agent_name },
514
+ { label: 'ROLE', get: r => r.role },
515
+ { label: 'KARMA', get: r => r.karma },
516
+ { label: 'VOTES', get: r => r.votes_cast },
517
+ ]);
518
+ }
519
+ return;
520
+ }
521
+
522
+ if (sub === 'add-member') {
523
+ const agentRef = positional[1];
524
+ if (!agentRef) { console.error('Usage: gitswarm council add-member <agent>'); process.exit(1); }
525
+ const council = await fed.council.getCouncil(repo.id);
526
+ if (!council) { console.error('No council. Run gitswarm council create first.'); process.exit(1); }
527
+ const agent = await fed.resolveAgent(agentRef);
528
+ const member = await fed.council.addMember(council.id, agent.id, flags.role || 'member');
529
+ console.log(`Added ${agent.name} to council as ${member.role}`);
530
+ return;
531
+ }
532
+
533
+ if (sub === 'propose') {
534
+ const type = positional[1];
535
+ const title = positional.slice(2).join(' ') || flags.title;
536
+ if (!type || !title || !flags.as) {
537
+ console.error('Usage: gitswarm council propose <type> <title> --as <agent> [--target <agent>]');
538
+ process.exit(1);
539
+ }
540
+ const council = await fed.council.getCouncil(repo.id);
541
+ if (!council) { console.error('No council.'); process.exit(1); }
542
+ const agent = await fed.resolveAgent(flags.as);
543
+
544
+ const action_data = {};
545
+ if (flags.target) {
546
+ const target = await fed.resolveAgent(flags.target);
547
+ action_data.agent_id = target.id;
548
+ }
549
+ if (flags.role) action_data.role = flags.role;
550
+ if (flags['access-level']) action_data.access_level = flags['access-level'];
551
+ if (flags.stream) action_data.stream_id = flags.stream;
552
+ if (flags.priority) action_data.priority = parseInt(flags.priority);
553
+ if (flags.tag) action_data.tag = flags.tag;
554
+
555
+ const proposal = await fed.council.createProposal(council.id, agent.id, {
556
+ title,
557
+ description: flags.desc || '',
558
+ proposal_type: type,
559
+ action_data,
560
+ });
561
+ console.log(`Proposal created: ${short(proposal.id)} "${proposal.title}"`);
562
+ console.log(` type: ${proposal.proposal_type} quorum: ${proposal.quorum_required}`);
563
+ return;
564
+ }
565
+
566
+ if (sub === 'vote') {
567
+ const proposalId = positional[1];
568
+ const vote = positional[2];
569
+ if (!proposalId || !vote || !flags.as) {
570
+ console.error('Usage: gitswarm council vote <proposal-id> for|against|abstain --as <agent>');
571
+ process.exit(1);
572
+ }
573
+ const agent = await fed.resolveAgent(flags.as);
574
+ const fullId = await resolveId(fed, 'council_proposals', proposalId);
575
+ await fed.council.vote(fullId, agent.id, vote, flags.comment);
576
+ console.log(`Vote '${vote}' recorded.`);
577
+ return;
578
+ }
579
+
580
+ if (sub === 'proposals') {
581
+ const council = await fed.council.getCouncil(repo.id);
582
+ if (!council) { console.log('No council.'); return; }
583
+ const proposals = await fed.council.listProposals(council.id, flags.status);
584
+ table(proposals, [
585
+ { label: 'ID', get: r => short(r.id) },
586
+ { label: 'STATUS', get: r => r.status },
587
+ { label: 'TYPE', get: r => r.proposal_type },
588
+ { label: 'TITLE', get: r => r.title },
589
+ { label: 'VOTES', get: r => `+${r.votes_for} -${r.votes_against}` },
590
+ { label: 'QUORUM', get: r => r.quorum_required },
591
+ { label: 'BY', get: r => r.proposer_name || '—' },
592
+ ]);
593
+ return;
594
+ }
595
+
596
+ console.error('Usage: gitswarm council <create|status|add-member|propose|vote|proposals>');
597
+ process.exit(1);
598
+ };
599
+
600
+ // --- status ---
601
+ commands.status = async (fed) => {
602
+ const repo = await fed.repo();
603
+ const config = fed.config();
604
+ const agents = await fed.listAgents();
605
+
606
+ console.log(`Federation: ${config.name || '(unnamed)'}`);
607
+ console.log(` path: ${fed.repoPath}`);
608
+ console.log(` mode: ${repo?.merge_mode || config.merge_mode || 'review'}`);
609
+ console.log(` model: ${repo?.ownership_model || config.ownership_model}`);
610
+ console.log(` access: ${repo?.agent_access || config.agent_access}`);
611
+ console.log(` stage: ${repo?.stage || 'seed'}`);
612
+ console.log(` agents: ${agents.length}`);
613
+
614
+ if (repo) {
615
+ console.log(` buffer: ${repo.buffer_branch || 'buffer'} → ${repo.promote_target || 'main'}`);
616
+
617
+ const { metrics } = await fed.stages.getMetrics(repo.id);
618
+ console.log(` contributors: ${metrics.contributor_count}`);
619
+ console.log(` streams: ${metrics.patch_count}`);
620
+ console.log(` maintainers: ${metrics.maintainer_count}`);
621
+ console.log(` council: ${metrics.has_council ? 'yes' : 'no'}`);
622
+
623
+ const elig = await fed.stages.checkEligibility(repo.id);
624
+ if (elig.next_stage) {
625
+ console.log(`\n Next stage: ${elig.next_stage} (${elig.eligible ? 'ELIGIBLE' : 'not yet'})`);
626
+ if (elig.unmet && elig.unmet.length > 0) {
627
+ for (const u of elig.unmet) {
628
+ console.log(` - ${u.requirement}: ${u.current}/${u.required}`);
629
+ }
630
+ }
631
+ }
632
+ }
633
+
634
+ // Streams summary
635
+ try {
636
+ const activeStreams = fed.listActiveStreams();
637
+ const workspaces = await fed.listWorkspaces();
638
+ console.log(`\nStreams: ${activeStreams.length} active`);
639
+ console.log(`Workspaces: ${workspaces.length} active`);
640
+
641
+ const queue = fed.tracker.getMergeQueue({ status: 'pending' });
642
+ if (queue.length > 0) {
643
+ console.log(`Merge queue: ${queue.length} pending`);
644
+ }
645
+ } catch {
646
+ // Tracker may not be available
647
+ }
648
+
649
+ // Check for Tier 2/3 plugin compatibility
650
+ try {
651
+ const pluginWarnings = fed.checkPluginCompatibility();
652
+ if (pluginWarnings.length > 0) {
653
+ console.log('\nWarnings:');
654
+ for (const w of pluginWarnings) {
655
+ console.log(` ! ${w}`);
656
+ }
657
+ console.log(' Connect to a server (Mode B) to enable these plugins.');
658
+ }
659
+ } catch {
660
+ // Non-fatal
661
+ }
662
+ };
663
+
664
+ // --- log ---
665
+ commands.log = async (fed, { flags }) => {
666
+ const events = await fed.activity.recent({
667
+ limit: flags.limit ? parseInt(flags.limit) : 20,
668
+ });
669
+ if (events.length === 0) { console.log('No activity yet.'); return; }
670
+ for (const e of events) {
671
+ const ts = e.created_at?.slice(0, 19) || '';
672
+ const agent = e.agent_name || short(e.agent_id);
673
+ console.log(` ${ts} ${agent} ${e.event_type} ${e.target_type || ''}:${short(e.target_id)}`);
674
+ }
675
+ };
676
+
677
+ // --- config ---
678
+ commands.config = async (fed, { positional, flags }) => {
679
+ // Pull config from server
680
+ if (flags.pull) {
681
+ const result = await fed.pullConfig();
682
+ if (!result) {
683
+ console.error('Not connected to server. Use gitswarm config --pull after connecting (Mode B).');
684
+ process.exit(1);
685
+ }
686
+ if (result.updated.length === 0) {
687
+ console.log('Config is up to date with server.');
688
+ } else {
689
+ console.log(`Updated ${result.updated.length} fields from server:`);
690
+ for (const field of result.updated) {
691
+ console.log(` ${field} = ${result.config[field]}`);
692
+ }
693
+ }
694
+ return;
695
+ }
696
+
697
+ const config = fed.config();
698
+ if (positional.length === 0) {
699
+ console.log(JSON.stringify(config, null, 2));
700
+ return;
701
+ }
702
+ if (positional.length === 1) {
703
+ console.log(config[positional[0]] ?? '(not set)');
704
+ return;
705
+ }
706
+ const { writeFileSync: write } = await import('fs');
707
+ const { join: joinPath } = await import('path');
708
+ config[positional[0]] = positional[1];
709
+ write(joinPath(fed.swarmDir, 'config.json'), JSON.stringify(config, null, 2));
710
+ console.log(`Set ${positional[0]} = ${positional[1]}`);
711
+ };
712
+
713
+ // --- sync ---
714
+ commands.sync = async (fed) => {
715
+ if (!fed.sync) {
716
+ console.error('Not connected to server. Connect with Mode B first.');
717
+ process.exit(1);
718
+ }
719
+
720
+ // Push: flush queued events to server
721
+ console.log('Pushing local events to server...');
722
+ try {
723
+ await fed.sync.flushQueue();
724
+ console.log(' Queue flushed.');
725
+ } catch (err) {
726
+ console.error(` Push failed: ${err.message}`);
727
+ }
728
+
729
+ // Pull: poll for server updates
730
+ console.log('Pulling updates from server...');
731
+ const updates = await fed.pollUpdates();
732
+ if (!updates) {
733
+ console.error(' Failed to poll updates.');
734
+ return;
735
+ }
736
+
737
+ const counts = [];
738
+ if (updates.tasks?.length > 0) counts.push(`${updates.tasks.length} new tasks`);
739
+ if (updates.access_changes?.length > 0) counts.push(`${updates.access_changes.length} access changes`);
740
+ if (updates.proposals?.length > 0) counts.push(`${updates.proposals.length} proposals`);
741
+ if (updates.reviews?.length > 0) counts.push(`${updates.reviews.length} reviews`);
742
+ if (updates.merges?.length > 0) counts.push(`${updates.merges.length} merges`);
743
+ if (updates.config_changes?.length > 0) counts.push(`${updates.config_changes.length} config changes`);
744
+
745
+ if (counts.length === 0) {
746
+ console.log(' Up to date.');
747
+ } else {
748
+ console.log(` Received: ${counts.join(', ')}`);
749
+ }
750
+ };
751
+
752
+ // ── ID resolution helper ───────────────────────────────────
753
+
754
+ async function resolveId(fed, tableName, prefix) {
755
+ if (prefix.length >= 32) return prefix;
756
+ const r = await fed.store.query(
757
+ `SELECT id FROM ${tableName} WHERE id LIKE ?`, [`${prefix}%`]
758
+ );
759
+ if (r.rows.length === 0) throw new Error(`No match for ID prefix: ${prefix}`);
760
+ if (r.rows.length > 1) throw new Error(`Ambiguous ID prefix: ${prefix} (${r.rows.length} matches)`);
761
+ return r.rows[0].id;
762
+ }
763
+
764
+ // ── Main ───────────────────────────────────────────────────
765
+
766
+ async function main() {
767
+ const { positional, flags } = parseArgs(process.argv);
768
+
769
+ if (positional.length === 0 || flags.help) {
770
+ console.log(`gitswarm — local multi-agent federation coordinator
771
+
772
+ Usage:
773
+ gitswarm <command> [subcommand] [options]
774
+
775
+ Commands:
776
+ init Initialise a federation in the current repo
777
+ agent Manage agents (register, list, info)
778
+ workspace Manage agent workspaces (create, list, destroy)
779
+ commit Commit from an agent's workspace
780
+ stream Inspect streams (list, info, diff)
781
+ review Stream reviews (submit, list, check consensus)
782
+ merge Merge a stream to buffer
783
+ stabilize Run tests and tag green/revert red
784
+ promote Promote buffer to main
785
+ task Task distribution (create, list, claim, submit, review)
786
+ council Governance (create, status, propose, vote, add-member)
787
+ status Show federation status
788
+ log View activity log
789
+ config View/set federation config
790
+
791
+ Options:
792
+ --as <agent> Act as a specific agent (name or ID)
793
+ --help Show this help
794
+
795
+ Examples:
796
+ gitswarm init --name my-project --mode review --model guild
797
+ gitswarm agent register architect --desc "System architect agent"
798
+ gitswarm workspace create --as coder --name "feature/auth"
799
+ gitswarm commit --as coder -m "Add auth module"
800
+ gitswarm review submit abc123 approve --as reviewer --feedback "LGTM"
801
+ gitswarm merge abc123 --as reviewer
802
+ gitswarm stabilize
803
+ gitswarm promote
804
+ gitswarm status`);
805
+ process.exit(0);
806
+ }
807
+
808
+ const cmd = positional[0];
809
+ const rest = { positional: positional.slice(1), flags };
810
+
811
+ // `init` doesn't require an existing federation
812
+ if (cmd === 'init') {
813
+ commands.init(null, rest);
814
+ return;
815
+ }
816
+
817
+ // All other commands need an open federation
818
+ let fed;
819
+ try {
820
+ fed = Federation.open();
821
+ } catch (e) {
822
+ console.error(e.message);
823
+ process.exit(1);
824
+ }
825
+
826
+ try {
827
+ if (!commands[cmd]) {
828
+ console.error(`Unknown command: ${cmd}. Run 'gitswarm --help' for usage.`);
829
+ process.exit(1);
830
+ }
831
+ await commands[cmd](fed, rest);
832
+ } catch (e) {
833
+ console.error(`Error: ${e.message}`);
834
+ process.exit(1);
835
+ } finally {
836
+ fed.close();
837
+ }
838
+ }
839
+
840
+ main();