@ifi/oh-pi-ant-colony 0.2.4 โ†’ 0.2.7

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.
@@ -54,6 +54,8 @@ interface ColonyLogEntry {
54
54
  }
55
55
 
56
56
  interface BackgroundColony {
57
+ /** Short identifier for this colony (c1, c2, ...). */
58
+ id: string;
57
59
  goal: string;
58
60
  abortController: AbortController;
59
61
  state: ColonyState | null;
@@ -64,8 +66,30 @@ interface BackgroundColony {
64
66
  }
65
67
 
66
68
  export default function antColonyExtension(pi: ExtensionAPI) {
67
- // Currently running background colony (only one at a time)
68
- let activeColony: BackgroundColony | null = null;
69
+ /** All running background colonies, keyed by short ID. */
70
+ const colonies = new Map<string, BackgroundColony>();
71
+ /** Auto-incrementing colony counter for generating IDs. */
72
+ let colonyCounter = 0;
73
+
74
+ /** Generate a short colony ID like c1, c2, ... */
75
+ function nextColonyId(): string {
76
+ colonyCounter++;
77
+ return `c${colonyCounter}`;
78
+ }
79
+
80
+ /**
81
+ * Resolve a colony by ID. If no ID given and exactly one colony is running,
82
+ * returns that one. Returns null if no match or ambiguous.
83
+ */
84
+ function resolveColony(idArg?: string): BackgroundColony | null {
85
+ if (idArg) {
86
+ return colonies.get(idArg) ?? null;
87
+ }
88
+ if (colonies.size === 1) {
89
+ return colonies.values().next().value ?? null;
90
+ }
91
+ return null;
92
+ }
69
93
 
70
94
  // Prevent main process polling from blocking: only allow explicit manual snapshots with cooldown
71
95
  let lastBgStatusSnapshotAt = 0;
@@ -166,26 +190,30 @@ export default function antColonyExtension(pi: ExtensionAPI) {
166
190
  }
167
191
 
168
192
  renderHandler = () => {
169
- if (!activeColony) {
193
+ if (colonies.size === 0) {
170
194
  return;
171
195
  }
172
- const { state } = activeColony;
173
- const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
174
- const m = state?.metrics;
175
- const phase = state?.status || "scouting";
176
- const progress = calcProgress(m);
177
- const pct = `${Math.round(progress * 100)}%`;
178
- const active = activeColony.antStreams.size;
179
-
180
- const parts = [`๐Ÿœ ${statusIcon(phase)} ${statusLabel(phase)}`];
181
- parts.push(m ? `${m.tasksDone}/${m.tasksTotal} (${pct})` : `0/0 (${pct})`);
182
- parts.push(`โšก${active}`);
183
- if (m) {
184
- parts.push(formatCost(m.totalCost));
196
+ const statusParts: string[] = [];
197
+ for (const colony of colonies.values()) {
198
+ const { state } = colony;
199
+ const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
200
+ const m = state?.metrics;
201
+ const phase = state?.status || "scouting";
202
+ const progress = calcProgress(m);
203
+ const pct = `${Math.round(progress * 100)}%`;
204
+ const active = colony.antStreams.size;
205
+
206
+ const parts = [`๐Ÿœ[${colony.id}] ${statusIcon(phase)} ${statusLabel(phase)}`];
207
+ parts.push(m ? `${m.tasksDone}/${m.tasksTotal} (${pct})` : `0/0 (${pct})`);
208
+ parts.push(`โšก${active}`);
209
+ if (m) {
210
+ parts.push(formatCost(m.totalCost));
211
+ }
212
+ parts.push(elapsed);
213
+ statusParts.push(parts.join(" โ”‚ "));
185
214
  }
186
- parts.push(elapsed);
187
215
 
188
- ctx.ui.setStatus("ant-colony", parts.join(" โ”‚ "));
216
+ ctx.ui.setStatus("ant-colony", statusParts.join(" ยท "));
189
217
  };
190
218
  clearHandler = () => {
191
219
  ctx.ui.setStatus("ant-colony", undefined);
@@ -256,17 +284,11 @@ export default function antColonyExtension(pi: ExtensionAPI) {
256
284
  modelRegistry?: any;
257
285
  },
258
286
  resume = false,
259
- ) {
260
- if (activeColony) {
261
- pi.events.emit("ant-colony:notify", {
262
- msg: "A colony is already running. Use /colony-stop first.",
263
- level: "warning",
264
- });
265
- return;
266
- }
267
-
287
+ ): string {
288
+ const colonyId = nextColonyId();
268
289
  const abortController = new AbortController();
269
290
  const colony: BackgroundColony = {
291
+ id: colonyId,
270
292
  goal: params.goal,
271
293
  abortController,
272
294
  state: null,
@@ -276,7 +298,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
276
298
  promise: null as any, // set below
277
299
  };
278
300
 
279
- pushLog(colony, { level: "info", text: "INITIALIZING ยท Colony launched in background" });
301
+ pushLog(colony, { level: "info", text: `INITIALIZING ยท Colony [${colonyId}] launched in background` });
280
302
 
281
303
  let lastPhase = "";
282
304
 
@@ -291,7 +313,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
291
313
  pi.sendMessage(
292
314
  {
293
315
  customType: "ant-colony-progress",
294
- content: `[COLONY_SIGNAL:${signal.phase.toUpperCase()}] ๐Ÿœ ${signal.message} (${pct}%, ${formatCost(signal.cost)})`,
316
+ content: `[COLONY_SIGNAL:${signal.phase.toUpperCase()}] ๐Ÿœ[${colonyId}] ${signal.message} (${pct}%, ${formatCost(signal.cost)})`,
295
317
  display: true,
296
318
  },
297
319
  { triggerTurn: false, deliverAs: "followUp" },
@@ -327,7 +349,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
327
349
  pi.sendMessage(
328
350
  {
329
351
  customType: "ant-colony-progress",
330
- content: `[COLONY_SIGNAL:TASK_DONE] ๐Ÿœ ${icon} ${task.title.slice(0, 60)} (${progress}, ${cost})`,
352
+ content: `[COLONY_SIGNAL:TASK_DONE] ๐Ÿœ[${colonyId}] ${icon} ${task.title.slice(0, 60)} (${progress}, ${cost})`,
331
353
  display: true,
332
354
  },
333
355
  { triggerTurn: false, deliverAs: "followUp" },
@@ -376,7 +398,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
376
398
  };
377
399
  colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
378
400
 
379
- activeColony = colony;
401
+ colonies.set(colonyId, colony);
380
402
  lastBgStatusSnapshotAt = 0;
381
403
  throttledRender();
382
404
 
@@ -391,39 +413,44 @@ export default function antColonyExtension(pi: ExtensionAPI) {
391
413
  text: `${ok ? "COMPLETE" : "FAILED"} ยท ${m.tasksDone}/${m.tasksTotal} ยท ${formatCost(m.totalCost)}`,
392
414
  });
393
415
 
394
- // Clear UI
395
- pi.events.emit("ant-colony:clear-ui");
396
- activeColony = null;
416
+ colonies.delete(colonyId);
417
+ if (colonies.size === 0) {
418
+ pi.events.emit("ant-colony:clear-ui");
419
+ }
397
420
 
398
421
  // Inject results into conversation
399
422
  pi.sendMessage(
400
423
  {
401
424
  customType: "ant-colony-report",
402
- content: `[COLONY_SIGNAL:COMPLETE]\n${report}`,
425
+ content: `[COLONY_SIGNAL:COMPLETE] [${colonyId}]\n${report}`,
403
426
  display: true,
404
427
  },
405
428
  { triggerTurn: true, deliverAs: "followUp" },
406
429
  );
407
430
 
408
431
  pi.events.emit("ant-colony:notify", {
409
- msg: `๐Ÿœ Colony ${ok ? "completed" : "failed"}: ${m.tasksDone}/${m.tasksTotal} tasks โ”‚ ${formatCost(m.totalCost)}`,
432
+ msg: `๐Ÿœ[${colonyId}] Colony ${ok ? "completed" : "failed"}: ${m.tasksDone}/${m.tasksTotal} tasks โ”‚ ${formatCost(m.totalCost)}`,
410
433
  level: ok ? "success" : "error",
411
434
  });
412
435
  })
413
436
  .catch((e) => {
414
437
  pushLog(colony, { level: "error", text: `CRASHED ยท ${String(e).slice(0, 120)}` });
415
- pi.events.emit("ant-colony:clear-ui");
416
- activeColony = null;
417
- pi.events.emit("ant-colony:notify", { msg: `๐Ÿœ Colony crashed: ${e}`, level: "error" });
438
+ colonies.delete(colonyId);
439
+ if (colonies.size === 0) {
440
+ pi.events.emit("ant-colony:clear-ui");
441
+ }
442
+ pi.events.emit("ant-colony:notify", { msg: `๐Ÿœ[${colonyId}] Colony crashed: ${e}`, level: "error" });
418
443
  pi.sendMessage(
419
444
  {
420
445
  customType: "ant-colony-report",
421
- content: `[COLONY_SIGNAL:FAILED]\n## ๐Ÿœ Colony Crashed\n${e}`,
446
+ content: `[COLONY_SIGNAL:FAILED] [${colonyId}]\n## ๐Ÿœ Colony Crashed\n${e}`,
422
447
  display: true,
423
448
  },
424
449
  { triggerTurn: true, deliverAs: "followUp" },
425
450
  );
426
451
  });
452
+
453
+ return colonyId;
427
454
  }
428
455
 
429
456
  // โ•โ•โ• Custom message renderer for colony progress signals โ•โ•โ•
@@ -496,8 +523,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
496
523
  pi.registerShortcut("ctrl+shift+a", {
497
524
  description: "Show ant colony details",
498
525
  async handler(ctx) {
499
- if (!activeColony) {
500
- ctx.ui.notify("No colony is currently running.", "info");
526
+ if (colonies.size === 0) {
527
+ ctx.ui.notify("No colonies are currently running.", "info");
501
528
  return;
502
529
  }
503
530
 
@@ -507,9 +534,18 @@ export default function antColonyExtension(pi: ExtensionAPI) {
507
534
  let cachedLines: string[] | undefined;
508
535
  let currentTab: "tasks" | "streams" | "log" = "tasks";
509
536
  let taskFilter: "all" | "active" | "done" | "failed" = "all";
537
+ /** Which colony to display (cycles with 'n'). */
538
+ let selectedColonyIdx = 0;
539
+
540
+ const getSelectedColony = (): BackgroundColony | null => {
541
+ const ids = [...colonies.keys()];
542
+ if (ids.length === 0) return null;
543
+ const idx = selectedColonyIdx % ids.length;
544
+ return colonies.get(ids[idx]) ?? null;
545
+ };
510
546
 
511
547
  const buildLines = (width: number): string[] => {
512
- const c = activeColony;
548
+ const c = getSelectedColony();
513
549
  if (!c) return [theme.fg("muted", " No colony running.")];
514
550
 
515
551
  const lines: string[] = [];
@@ -525,8 +561,18 @@ export default function antColonyExtension(pi: ExtensionAPI) {
525
561
  const activeAnts = c.antStreams.size;
526
562
  const barWidth = Math.max(10, Math.min(24, w - 28));
527
563
 
564
+ // Show colony selector if multiple are running
565
+ if (colonies.size > 1) {
566
+ const ids = [...colonies.keys()];
567
+ const idx = selectedColonyIdx % ids.length;
568
+ const selector = ids
569
+ .map((id, i) => (i === idx ? theme.fg("accent", theme.bold(`[${id}]`)) : theme.fg("muted", id)))
570
+ .join(" ");
571
+ lines.push(` ${selector} ${theme.fg("dim", "(n = next colony)")}`);
572
+ }
573
+
528
574
  lines.push(
529
- theme.fg("accent", theme.bold(" ๐Ÿœ Colony Details")) + theme.fg("muted", ` โ”‚ ${elapsed} โ”‚ ${cost}`),
575
+ theme.fg("accent", theme.bold(` ๐Ÿœ Colony [${c.id}]`)) + theme.fg("muted", ` โ”‚ ${elapsed} โ”‚ ${cost}`),
530
576
  );
531
577
  lines.push(theme.fg("muted", ` Goal: ${trim(c.goal, w - 8)}`));
532
578
  lines.push(
@@ -705,6 +751,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
705
751
  else if (data.toLowerCase() === "a") taskFilter = "active";
706
752
  else if (data.toLowerCase() === "d") taskFilter = "done";
707
753
  else if (data.toLowerCase() === "f") taskFilter = "failed";
754
+ else if (data.toLowerCase() === "n") selectedColonyIdx++;
708
755
  else return;
709
756
 
710
757
  cachedWidth = undefined;
@@ -747,18 +794,6 @@ export default function antColonyExtension(pi: ExtensionAPI) {
747
794
  }),
748
795
 
749
796
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
750
- if (activeColony) {
751
- return {
752
- content: [
753
- {
754
- type: "text",
755
- text: "A colony is already running in the background. Use /colony-stop to cancel it first.",
756
- },
757
- ],
758
- isError: true,
759
- };
760
- }
761
-
762
797
  const currentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null;
763
798
  if (!currentModel) {
764
799
  return {
@@ -788,13 +823,13 @@ export default function antColonyExtension(pi: ExtensionAPI) {
788
823
  }
789
824
 
790
825
  // Interactive mode: run in background
791
- launchBackgroundColony(colonyParams);
826
+ const launchedId = launchBackgroundColony(colonyParams);
792
827
 
793
828
  return {
794
829
  content: [
795
830
  {
796
831
  type: "text",
797
- text: `[COLONY_SIGNAL:LAUNCHED]\n๐Ÿœ Colony launched in background.\nGoal: ${params.goal}\n\nThe colony runs autonomously in passive mode. Progress is pushed via [COLONY_SIGNAL:*] follow-up messages. Do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.`,
832
+ text: `[COLONY_SIGNAL:LAUNCHED] [${launchedId}]\n๐Ÿœ Colony [${launchedId}] launched in background (${colonies.size} active).\nGoal: ${params.goal}\n\nThe colony runs autonomously in passive mode. Progress is pushed via [COLONY_SIGNAL:*] follow-up messages. Do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.`,
798
833
  },
799
834
  ],
800
835
  };
@@ -818,9 +853,17 @@ export default function antColonyExtension(pi: ExtensionAPI) {
818
853
  container.addChild(
819
854
  new Text(theme.fg("success", "โœ“ ") + theme.fg("toolTitle", theme.bold("Colony launched in background")), 0, 0),
820
855
  );
821
- if (activeColony) {
822
- container.addChild(new Text(theme.fg("muted", ` Goal: ${activeColony.goal.slice(0, 70)}`), 0, 0));
823
- container.addChild(new Text(theme.fg("muted", " Ctrl+Shift+A for details โ”‚ /colony-stop to cancel"), 0, 0));
856
+ if (colonies.size > 0) {
857
+ for (const colony of colonies.values()) {
858
+ container.addChild(new Text(theme.fg("muted", ` [${colony.id}] ${colony.goal.slice(0, 65)}`), 0, 0));
859
+ }
860
+ container.addChild(
861
+ new Text(
862
+ theme.fg("muted", ` ${colonies.size} active โ”‚ Ctrl+Shift+A for details โ”‚ /colony-stop to cancel`),
863
+ 0,
864
+ 0,
865
+ ),
866
+ );
824
867
  }
825
868
  return container;
826
869
  },
@@ -828,9 +871,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
828
871
 
829
872
  // โ•โ•โ• Helper: build status summary โ•โ•โ•
830
873
 
831
- function buildStatusText(): string {
832
- if (!activeColony) return "No colony is currently running.";
833
- const c = activeColony;
874
+ /** Build a status summary for a single colony. */
875
+ function buildColonyStatusText(c: BackgroundColony): string {
834
876
  const state = c.state;
835
877
  const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
836
878
  const m = state?.metrics;
@@ -853,15 +895,29 @@ export default function antColonyExtension(pi: ExtensionAPI) {
853
895
  return lines.join("\n");
854
896
  }
855
897
 
898
+ /** Build a status summary for all running colonies. */
899
+ function buildStatusText(): string {
900
+ if (colonies.size === 0) return "No colonies are currently running.";
901
+ if (colonies.size === 1) {
902
+ const colony = colonies.values().next().value;
903
+ return colony ? buildColonyStatusText(colony) : "No colonies are currently running.";
904
+ }
905
+ const parts: string[] = [`${colonies.size} colonies running:\n`];
906
+ for (const colony of colonies.values()) {
907
+ parts.push(`โ”€โ”€ [${colony.id}] โ”€โ”€\n${buildColonyStatusText(colony)}\n`);
908
+ }
909
+ return parts.join("\n");
910
+ }
911
+
856
912
  // โ•โ•โ• Tool: bg_colony_status โ•โ•โ•
857
913
  pi.registerTool({
858
914
  name: "bg_colony_status",
859
915
  label: "Colony Status",
860
916
  description:
861
- "Optional manual snapshot for a running colony. Progress is pushed passively via COLONY_SIGNAL follow-up messages; call this only when the user explicitly asks.",
917
+ "Optional manual snapshot for running colonies. Progress is pushed passively via COLONY_SIGNAL follow-up messages; call this only when the user explicitly asks.",
862
918
  parameters: Type.Object({}),
863
919
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
864
- if (!activeColony) {
920
+ if (colonies.size === 0) {
865
921
  return {
866
922
  content: [{ type: "text" as const, text: "No colony is currently running." }],
867
923
  };
@@ -902,70 +958,171 @@ export default function antColonyExtension(pi: ExtensionAPI) {
902
958
  },
903
959
  });
904
960
 
961
+ // โ•โ•โ• Command: /colony โ•โ•โ•
962
+ pi.registerCommand("colony", {
963
+ description: "Launch an ant colony swarm to accomplish a goal",
964
+ async handler(args, ctx) {
965
+ const goal = args.trim();
966
+ if (!goal) {
967
+ ctx.ui.notify("Usage: /colony <goal> โ€” describe what the colony should accomplish", "warning");
968
+ return;
969
+ }
970
+
971
+ const currentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null;
972
+ if (!currentModel) {
973
+ ctx.ui.notify("Colony failed: no model available in current session.", "error");
974
+ return;
975
+ }
976
+
977
+ const id = launchBackgroundColony({
978
+ cwd: ctx.cwd,
979
+ goal,
980
+ currentModel,
981
+ modelOverrides: {},
982
+ modelRegistry: ctx.modelRegistry ?? undefined,
983
+ });
984
+ ctx.ui.notify(
985
+ `๐Ÿœ[${id}] Colony launched (${colonies.size} active): ${goal.slice(0, 70)}${goal.length > 70 ? "..." : ""}`,
986
+ "info",
987
+ );
988
+ },
989
+ });
990
+
991
+ // โ•โ•โ• Command: /colony-count โ•โ•โ•
992
+ pi.registerCommand("colony-count", {
993
+ description: "Show how many colonies are currently running",
994
+ async handler(_args, ctx) {
995
+ if (colonies.size === 0) {
996
+ ctx.ui.notify("No colonies running.", "info");
997
+ } else {
998
+ const ids = [...colonies.values()].map((c) => `[${c.id}] ${c.goal.slice(0, 50)}`).join("\n ");
999
+ ctx.ui.notify(`${colonies.size} active ${colonies.size === 1 ? "colony" : "colonies"}:\n ${ids}`, "info");
1000
+ }
1001
+ },
1002
+ });
1003
+
905
1004
  // โ•โ•โ• Command: /colony-status โ•โ•โ•
906
1005
  pi.registerCommand("colony-status", {
907
- description: "Show current colony progress",
908
- async handler(_args, ctx) {
909
- if (!activeColony) {
910
- ctx.ui.notify("No colony is currently running.", "info");
1006
+ description: "Show current colony progress (optionally specify ID: /colony-status c1)",
1007
+ getArgumentCompletions(prefix) {
1008
+ const items = [...colonies.keys()]
1009
+ .filter((id) => id.startsWith(prefix))
1010
+ .map((id) => {
1011
+ const c = colonies.get(id);
1012
+ return { value: id, label: `${id} โ€” ${c?.goal.slice(0, 50) ?? ""}` };
1013
+ });
1014
+ return items.length > 0 ? items : null;
1015
+ },
1016
+ async handler(args, ctx) {
1017
+ const idArg = args.trim() || undefined;
1018
+ if (colonies.size === 0) {
1019
+ ctx.ui.notify("No colonies are currently running.", "info");
911
1020
  return;
912
1021
  }
913
- ctx.ui.notify(buildStatusText(), "info");
1022
+ if (idArg) {
1023
+ const colony = resolveColony(idArg);
1024
+ if (!colony) {
1025
+ ctx.ui.notify(`Colony "${idArg}" not found. Active: ${[...colonies.keys()].join(", ")}`, "warning");
1026
+ return;
1027
+ }
1028
+ ctx.ui.notify(buildColonyStatusText(colony), "info");
1029
+ } else {
1030
+ ctx.ui.notify(buildStatusText(), "info");
1031
+ }
914
1032
  },
915
1033
  });
916
1034
 
917
1035
  // โ•โ•โ• Command: /colony-stop โ•โ•โ•
918
1036
  pi.registerCommand("colony-stop", {
919
- description: "Stop the running background colony",
920
- async handler(_args, ctx) {
921
- if (!activeColony) {
922
- ctx.ui.notify("No colony is currently running.", "info");
1037
+ description: "Stop a colony (specify ID, or stops all if none given)",
1038
+ getArgumentCompletions(prefix) {
1039
+ const items = [
1040
+ { value: "all", label: "all โ€” Stop all running colonies" },
1041
+ ...[...colonies.keys()]
1042
+ .filter((id) => id.startsWith(prefix))
1043
+ .map((id) => {
1044
+ const c = colonies.get(id);
1045
+ return { value: id, label: `${id} โ€” ${c?.goal.slice(0, 50) ?? ""}` };
1046
+ }),
1047
+ ].filter((i) => i.value.startsWith(prefix));
1048
+ return items.length > 0 ? items : null;
1049
+ },
1050
+ async handler(args, ctx) {
1051
+ const idArg = args.trim() || undefined;
1052
+ if (colonies.size === 0) {
1053
+ ctx.ui.notify("No colonies are currently running.", "info");
923
1054
  return;
924
1055
  }
925
- activeColony.abortController.abort();
926
- ctx.ui.notify("๐Ÿœ Colony abort signal sent. Waiting for ants to finish...", "warning");
1056
+ if (!idArg || idArg === "all") {
1057
+ const count = colonies.size;
1058
+ for (const colony of colonies.values()) {
1059
+ colony.abortController.abort();
1060
+ }
1061
+ ctx.ui.notify(`๐Ÿœ Abort signal sent to ${count} ${count === 1 ? "colony" : "colonies"}.`, "warning");
1062
+ } else {
1063
+ const colony = resolveColony(idArg);
1064
+ if (!colony) {
1065
+ ctx.ui.notify(`Colony "${idArg}" not found. Active: ${[...colonies.keys()].join(", ")}`, "warning");
1066
+ return;
1067
+ }
1068
+ colony.abortController.abort();
1069
+ ctx.ui.notify(`๐Ÿœ[${colony.id}] Abort signal sent. Waiting for ants to finish...`, "warning");
1070
+ }
927
1071
  },
928
1072
  });
929
1073
 
930
1074
  pi.registerCommand("colony-resume", {
931
- description: "Resume a colony from its last checkpoint",
932
- async handler(_args, ctx) {
933
- if (activeColony) {
934
- ctx.ui.notify("A colony is already running.", "warning");
1075
+ description: "Resume colonies from their last checkpoint (resumes all resumable by default)",
1076
+ async handler(args, ctx) {
1077
+ const all = Nest.findAllResumable(ctx.cwd);
1078
+ if (all.length === 0) {
1079
+ ctx.ui.notify("No resumable colonies found.", "info");
935
1080
  return;
936
1081
  }
937
- const found = Nest.findResumable(ctx.cwd);
938
- if (!found) {
939
- ctx.ui.notify("No resumable colony found.", "info");
1082
+
1083
+ // If an argument is given, try to match a specific colony ID
1084
+ const target = args.trim();
1085
+ const toResume = target ? all.filter((r) => r.colonyId === target) : [all[0]];
1086
+
1087
+ if (toResume.length === 0) {
1088
+ ctx.ui.notify(`Colony "${target}" not found. Resumable: ${all.map((r) => r.colonyId).join(", ")}`, "warning");
940
1089
  return;
941
1090
  }
942
- ctx.ui.notify(`๐Ÿœ Resuming colony: ${found.state.goal.slice(0, 60)}...`, "info");
943
- launchBackgroundColony(
944
- {
945
- cwd: ctx.cwd,
946
- goal: found.state.goal,
947
- maxCost: found.state.maxCost ?? undefined,
948
- currentModel: ctx.currentModel,
949
- modelOverrides: {},
950
- modelRegistry: ctx.modelRegistry,
951
- },
952
- true,
953
- );
1091
+
1092
+ for (const found of toResume) {
1093
+ const id = launchBackgroundColony(
1094
+ {
1095
+ cwd: ctx.cwd,
1096
+ goal: found.state.goal,
1097
+ maxCost: found.state.maxCost ?? undefined,
1098
+ currentModel: ctx.currentModel,
1099
+ modelOverrides: {},
1100
+ modelRegistry: ctx.modelRegistry,
1101
+ },
1102
+ true,
1103
+ );
1104
+ ctx.ui.notify(`๐Ÿœ[${id}] Resuming: ${found.state.goal.slice(0, 60)}...`, "info");
1105
+ }
954
1106
  },
955
1107
  });
956
1108
 
957
1109
  // โ•โ•โ• Cleanup on shutdown โ•โ•โ•
958
1110
  pi.on("session_shutdown", async () => {
959
- if (activeColony) {
960
- activeColony.abortController.abort();
961
- // Wait for colony to finish gracefully (max 5s)
1111
+ if (colonies.size > 0) {
1112
+ for (const colony of colonies.values()) {
1113
+ colony.abortController.abort();
1114
+ }
1115
+ // Wait for all colonies to finish gracefully (max 5s)
962
1116
  try {
963
- await Promise.race([activeColony.promise, new Promise((r) => setTimeout(r, 5000))]);
1117
+ await Promise.race([
1118
+ Promise.all([...colonies.values()].map((c) => c.promise)),
1119
+ new Promise((r) => setTimeout(r, 5000)),
1120
+ ]);
964
1121
  } catch {
965
1122
  /* ignore */
966
1123
  }
967
1124
  pi.events.emit("ant-colony:clear-ui");
968
- activeColony = null;
1125
+ colonies.clear();
969
1126
  }
970
1127
  });
971
1128
  }
@@ -494,7 +494,18 @@ export class Nest {
494
494
  * scouting, working, or reviewing and has no `finishedAt` timestamp).
495
495
  */
496
496
  static findResumable(cwd: string): { colonyId: string; state: ColonyState } | null {
497
+ const all = Nest.findAllResumable(cwd);
498
+ return all.length > 0 ? all[0] : null;
499
+ }
500
+
501
+ /**
502
+ * Find all resumable colonies in the working directory.
503
+ * Returns colonies whose state is incomplete (not done/failed/budget_exceeded).
504
+ * Sorted by `createdAt` descending so the most recent colony is first.
505
+ */
506
+ static findAllResumable(cwd: string): Array<{ colonyId: string; state: ColonyState }> {
497
507
  const parentDir = path.join(cwd, ".ant-colony");
508
+ const results: Array<{ colonyId: string; state: ColonyState }> = [];
498
509
  try {
499
510
  for (const dir of fs.readdirSync(parentDir)) {
500
511
  const stateFile = path.join(parentDir, dir, "state.json");
@@ -508,13 +519,14 @@ export class Nest {
508
519
  state.status !== "failed" &&
509
520
  state.status !== "budget_exceeded"
510
521
  ) {
511
- return { colonyId: dir, state };
522
+ results.push({ colonyId: dir, state });
512
523
  }
513
524
  }
514
525
  } catch {
515
526
  // No .ant-colony directory โ€” nothing to resume
516
527
  }
517
- return null;
528
+ results.sort((a, b) => (b.state.createdAt ?? 0) - (a.state.createdAt ?? 0));
529
+ return results;
518
530
  }
519
531
 
520
532
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifi/oh-pi-ant-colony",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "description": "Autonomous multi-agent swarm extension for pi โ€” adaptive concurrency, pheromone communication.",
5
5
  "keywords": [
6
6
  "pi-package"